Compare commits
10 Commits
01dc30c06a
...
3526ccb304
| Author | SHA1 | Date | |
|---|---|---|---|
| 3526ccb304 | |||
| 3569451ab6 | |||
| 24c57308b7 | |||
| 1a31352bda | |||
| f637d7813e | |||
| 95e28ed3b9 | |||
| 57f67332eb | |||
| a7495bfbe6 | |||
| 5f5d90b93e | |||
| 18562a16c9 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
site/
|
||||
.cache/
|
||||
__pycache__/
|
||||
_to_delete/
|
||||
|
||||
16
README.md
16
README.md
@ -18,7 +18,7 @@ mkdocs serve # http://127.0.0.1:8000
|
||||
# 3. commit + push de este repo (y bump del submódulo en Drogheda)
|
||||
|
||||
# 4. publicar al servidor
|
||||
./scripts/deploy.sh # git@git.helgen.online:/var/www/hbox-docs (puerto 222)
|
||||
./scripts/deploy.sh # leo@git.helgen.online:/srv/projects/drogheda/hbox-docs/site (puerto 22)
|
||||
```
|
||||
|
||||
**Política de publicación:** solo se publica lo que está en la lista de
|
||||
@ -30,8 +30,8 @@ agregar un documento al sitio se agrega a esa lista (y con eso queda auditado en
|
||||
En el servidor (`git.helgen.online` u otro), como root:
|
||||
|
||||
```bash
|
||||
mkdir -p /var/www/hbox-docs
|
||||
chown git /var/www/hbox-docs # el usuario con el que hace rsync deploy.sh
|
||||
mkdir -p /srv/projects/drogheda/hbox-docs/site
|
||||
chown -R leo:leo /srv/projects/drogheda/hbox-docs # el usuario con el que hace rsync deploy.sh
|
||||
|
||||
# usuarios para los técnicos (HTTP Basic)
|
||||
apt install -y apache2-utils
|
||||
@ -40,17 +40,17 @@ htpasswd /etc/nginx/hbox-docs.htpasswd leo
|
||||
```
|
||||
|
||||
Bloque de nginx (por ejemplo `/etc/nginx/sites-available/hbox-docs`, con el certificado
|
||||
que ya tenga el servidor o uno nuevo de certbot para `docs.helgen.online`):
|
||||
que ya tenga el servidor o uno nuevo de certbot para `drogheda.helgen.online`):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name docs.helgen.online;
|
||||
server_name drogheda.helgen.online;
|
||||
|
||||
# ssl_certificate ... ; # certbot --nginx -d docs.helgen.online
|
||||
# ssl_certificate ... ; # certbot --nginx -d drogheda.helgen.online
|
||||
# ssl_certificate_key ... ;
|
||||
|
||||
root /var/www/hbox-docs;
|
||||
root /srv/projects/drogheda/hbox-docs/site;
|
||||
index index.html;
|
||||
|
||||
auth_basic "Helgen HBOX docs";
|
||||
@ -67,7 +67,7 @@ ln -s /etc/nginx/sites-available/hbox-docs /etc/nginx/sites-enabled/
|
||||
nginx -t && systemctl reload nginx
|
||||
```
|
||||
|
||||
Los técnicos entran a `https://docs.helgen.online` con su usuario y contraseña.
|
||||
Los técnicos entran a `https://drogheda.helgen.online` con su usuario y contraseña.
|
||||
Alternativa sin subdominio: servirlo como `location /hbox/` dentro del server block
|
||||
existente, con el mismo `auth_basic`.
|
||||
|
||||
|
||||
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
|
||||
229
backend/app.py
Normal file
229
backend/app.py
Normal file
@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Actas HBOX — servicio para recibir y consultar formularios firmados de los técnicos.
|
||||
|
||||
Reglas:
|
||||
- Un acta real (serie que NO contiene PRUEBA) se puede enviar UNA sola vez por serie.
|
||||
Un segundo envío se rechaza (409) hasta que un administrador la borre.
|
||||
- Al seleccionar una serie ya enviada, el formulario la vuelve a cargar (llena, bloqueada).
|
||||
- Solo el usuario 'leo' (administrador) puede borrar/reabrir un acta.
|
||||
- Las series PRUEBA se guardan aparte (_pruebas), no cuentan y se pueden reenviar.
|
||||
|
||||
El usuario autenticado llega en la cabecera X-Remote-User (la fija nginx desde $remote_user;
|
||||
el cliente no puede falsificarla porque nginx la sobrescribe).
|
||||
|
||||
Rutas:
|
||||
GET /api/salud
|
||||
GET /api/whoami -> {user, admin}
|
||||
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); 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
|
||||
|
||||
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]
|
||||
|
||||
def current_user():
|
||||
return (request.headers.get("X-Remote-User") or "").strip()
|
||||
|
||||
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)
|
||||
if not os.path.isdir(carpeta):
|
||||
return None
|
||||
pref = serie + "_"
|
||||
cand = sorted(fn for fn in os.listdir(carpeta)
|
||||
if fn.startswith(pref) and fn.endswith(".json"))
|
||||
return os.path.join(carpeta, cand[-1]) if cand else None
|
||||
|
||||
@app.get("/api/salud")
|
||||
def salud():
|
||||
return jsonify(ok=True, servicio="actas-hbox")
|
||||
|
||||
@app.get("/api/whoami")
|
||||
def whoami():
|
||||
u = current_user()
|
||||
return jsonify(ok=True, user=u, admin=(u.lower() in ADMINS))
|
||||
|
||||
@app.get("/api/acta")
|
||||
def get_acta():
|
||||
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)
|
||||
if not ruta:
|
||||
return jsonify(ok=True, exists=False)
|
||||
with open(ruta, encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
d["_archivo"] = os.path.basename(ruta)
|
||||
return jsonify(ok=True, exists=True, acta=d)
|
||||
|
||||
@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")); 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
|
||||
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
|
||||
|
||||
es_prueba = "PRUEBA" in serie.upper()
|
||||
|
||||
# un acta real solo se puede enviar una vez
|
||||
if not es_prueba and find_latest(tipo, serie):
|
||||
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
|
||||
d["_usuario"] = current_user()
|
||||
d["_ip"] = request.headers.get("X-Forwarded-For", request.remote_addr)
|
||||
d["_prueba"] = es_prueba
|
||||
|
||||
carpeta = os.path.join(BASE, tipo, "_pruebas") if es_prueba else os.path.join(BASE, tipo)
|
||||
os.makedirs(carpeta, exist_ok=True)
|
||||
archivo = f"{serie}_{iso.replace(':', '-')}.json"
|
||||
with open(os.path.join(carpeta, archivo), "w", encoding="utf-8") as f:
|
||||
json.dump(d, f, ensure_ascii=False, indent=2)
|
||||
|
||||
if not es_prueba:
|
||||
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])
|
||||
|
||||
sub = f"{tipo}/_pruebas/{archivo}" if es_prueba else f"{tipo}/{archivo}"
|
||||
return jsonify(ok=True, archivo=sub, prueba=es_prueba)
|
||||
|
||||
@app.delete("/api/acta")
|
||||
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"), 24)
|
||||
if not tipo or not serie:
|
||||
return jsonify(ok=False, error="faltan tipo/serie"), 400
|
||||
carpeta = os.path.join(BASE, tipo)
|
||||
borrados = []
|
||||
if os.path.isdir(carpeta):
|
||||
pref = serie + "_"
|
||||
for fn in list(os.listdir(carpeta)):
|
||||
if fn.startswith(pref) and fn.endswith(".json"):
|
||||
os.remove(os.path.join(carpeta, fn)); borrados.append(fn)
|
||||
idx = os.path.join(carpeta, "_indice.csv")
|
||||
if os.path.exists(idx):
|
||||
rows = list(csv.reader(open(idx, encoding="utf-8")))
|
||||
keep = ([rows[0]] if rows else []) + [r for r in rows[1:] if not (len(r) > 1 and r[1] == serie)]
|
||||
with open(idx, "w", newline="", encoding="utf-8") as f:
|
||||
csv.writer(f).writerows(keep)
|
||||
return jsonify(ok=True, borrados=borrados)
|
||||
|
||||
@app.delete("/api/actas")
|
||||
def borrar_todas():
|
||||
"""Reset admin: borra TODAS las actas reales de un tipo (opcional: también las de prueba)."""
|
||||
if not is_admin():
|
||||
return jsonify(ok=False, error="Solo un administrador puede reiniciar actas."), 403
|
||||
tipo = slug(request.args.get("tipo"))
|
||||
incl_pruebas = request.args.get("pruebas") == "1"
|
||||
if not tipo:
|
||||
return jsonify(ok=False, error="falta tipo"), 400
|
||||
carpeta = os.path.join(BASE, tipo)
|
||||
n = 0
|
||||
if os.path.isdir(carpeta):
|
||||
for fn in list(os.listdir(carpeta)):
|
||||
fp = os.path.join(carpeta, fn)
|
||||
if fn.endswith(".json"):
|
||||
os.remove(fp); n += 1
|
||||
elif fn == "_indice.csv":
|
||||
os.remove(fp)
|
||||
if incl_pruebas:
|
||||
pdir = os.path.join(carpeta, "_pruebas")
|
||||
if os.path.isdir(pdir):
|
||||
for fn in list(os.listdir(pdir)):
|
||||
if fn.endswith(".json"):
|
||||
os.remove(os.path.join(pdir, fn)); n += 1
|
||||
return jsonify(ok=True, borradas=n)
|
||||
|
||||
@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
|
||||
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)
|
||||
|
||||
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
|
||||
56
docs/actas/index.md
Normal file
56
docs/actas/index.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Actas y control de calidad
|
||||
|
||||
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; 30 seleccionables). Procedimiento: [Prueba y flasheo v5](../procedimientos/v5-prueba-y-flasheo.md).
|
||||
|
||||
**[▶ 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).
|
||||
|
||||
**[▶ 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 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 (`…/actas/<tipo>/`). Las actas de
|
||||
integración y ensamble llevan `componentes` con los módulos elegidos. Listado completo en
|
||||
[`/api/actas`](/api/actas){:target="_blank"}.
|
||||
86
docs/admin.html
Normal file
86
docs/admin.html
Normal file
@ -0,0 +1,86 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Administración — HBOX</title>
|
||||
<link rel="stylesheet" href="formularios/acta.css">
|
||||
<style>
|
||||
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:6px}
|
||||
th{text-align:left;color:var(--tinta2);font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;padding:6px 8px 6px 0;border-bottom:1px solid var(--tinta2)}
|
||||
td{padding:7px 8px 7px 0;border-bottom:1px solid var(--linea)}
|
||||
.del{background:#fff;color:var(--falla);border:1px solid var(--falla);border-radius:8px;padding:5px 10px;font-size:13px;cursor:pointer}
|
||||
.bulk{background:var(--falla);color:#fff;border:0;border-radius:9px;padding:9px 14px;font-size:13.5px;cursor:pointer;margin:4px 8px 4px 0}
|
||||
.ver{color:var(--plane);text-decoration:none;font-weight:600}
|
||||
.aviso{background:var(--crit-soft);border-left:3px solid var(--crit);padding:10px 14px;border-radius:0 8px 8px 0;margin:10px 0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="top"><h1>Administración de actas</h1><p id="sub">Solo para el administrador.</p></header>
|
||||
<div style="margin:2px 0 10px"><a class="volver" href="./tablero.html">← Tablero</a></div>
|
||||
<div id="cont"><p>Comprobando permisos…</p></div>
|
||||
</div>
|
||||
<script>
|
||||
const cont = document.getElementById('cont');
|
||||
const TIPOS = [
|
||||
{ tipo:'prueba-v5', label:'Prueba V5' },
|
||||
{ tipo:'prueba-v4', label:'Prueba V4' },
|
||||
{ tipo:'ensamble', label:'Ensamble' },
|
||||
];
|
||||
function esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
|
||||
function fmt(iso){ return iso? iso.replace('T',' ').slice(0,16):''; }
|
||||
|
||||
async function delOne(tipo, serie){
|
||||
if(!confirm('¿Borrar el acta '+serie+'? Podrá rehacerse.')) return;
|
||||
const r = await fetch('/api/acta?tipo='+encodeURIComponent(tipo)+'&serie='+encodeURIComponent(serie),{method:'DELETE'});
|
||||
const j = await r.json(); if(!j.ok){ alert(j.error||'error'); return; } load();
|
||||
}
|
||||
async function delAll(tipo, label){
|
||||
if(!confirm('¿BORRAR TODAS las actas de '+label+'? Esta acción no se puede deshacer.')) return;
|
||||
if(!confirm('Confirma otra vez: se borrarán TODAS las de '+label+'.')) return;
|
||||
const r = await fetch('/api/actas?tipo='+encodeURIComponent(tipo),{method:'DELETE'});
|
||||
const j = await r.json(); if(!j.ok){ alert(j.error||'error'); return; }
|
||||
alert('Borradas: '+j.borradas); load();
|
||||
}
|
||||
async function delPruebas(){
|
||||
if(!confirm('¿Borrar todos los registros de PRUEBA de los tres formularios?')) return;
|
||||
for(const t of TIPOS){ await fetch('/api/actas?tipo='+encodeURIComponent(t.tipo)+'&pruebas=1',{method:'DELETE'}); }
|
||||
alert('Registros de prueba borrados.'); load();
|
||||
}
|
||||
|
||||
async function load(){
|
||||
let who; try{ who = await (await fetch('/api/whoami')).json(); }catch(e){}
|
||||
if(!who || !who.admin){
|
||||
cont.innerHTML = '<div class="aviso"><b>Acceso restringido.</b> Esta página solo está disponible para el administrador (Leo). '
|
||||
+ 'Tú entraste como <b>'+esc(who&&who.user||'—')+'</b>.</div>';
|
||||
return;
|
||||
}
|
||||
document.getElementById('sub').textContent = 'Administrador: '+esc(who.user);
|
||||
let j; try{ j = await (await fetch('/api/actas')).json(); }catch(e){ cont.innerHTML='Error al cargar.'; return; }
|
||||
const actas = j.actas || [];
|
||||
let h = '<div class="paso"><h2>Reiniciar en bloque</h2>'
|
||||
+ '<p class="sub">Borra todas las actas guardadas de un formulario, para volver a empezar el lote.</p>';
|
||||
TIPOS.forEach(t=>{ h += '<button class="bulk" onclick="delAll(\''+t.tipo+'\',\''+t.label+'\')">Borrar TODAS — '+t.label+'</button>'; });
|
||||
h += '<button class="bulk" style="background:var(--tinta2)" onclick="delPruebas()">Limpiar registros de PRUEBA</button></div>';
|
||||
|
||||
h += '<div class="paso"><h2>Actas guardadas ('+actas.length+')</h2>';
|
||||
if(!actas.length){ h += '<p class="sub">No hay actas guardadas.</p>'; }
|
||||
else {
|
||||
h += '<div class="tblwrap"><table><tr><th>Serie</th><th>Tipo</th><th>Técnico</th><th>Resultado</th><th>Fecha</th><th></th><th></th></tr>';
|
||||
actas.forEach(a=>{
|
||||
h += '<tr><td><b>'+esc(a.serie)+'</b></td><td>'+esc(a.tipo)+'</td><td>'+esc(a.tecnico||'')+'</td>'
|
||||
+ '<td>'+esc(a.resultado||'')+'</td><td>'+esc(fmt(a.recibido))+'</td>'
|
||||
+ '<td><a class="ver" href="formularios/ver.html?tipo='+encodeURIComponent(a.tipo)+'&serie='+encodeURIComponent(a.serie)+'">Ver</a></td>'
|
||||
+ '<td><button class="del" onclick="delOne(\''+a.tipo+'\',\''+esc(a.serie)+'\')">Borrar</button></td></tr>';
|
||||
});
|
||||
h += '</table></div>';
|
||||
}
|
||||
h += '</div>';
|
||||
cont.innerHTML = h;
|
||||
}
|
||||
window.delOne=delOne; window.delAll=delAll; window.delPruebas=delPruebas;
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
docs/descargas/SHA256SUMS.txt
Normal file
1
docs/descargas/SHA256SUMS.txt
Normal file
@ -0,0 +1 @@
|
||||
78b34f4603550c05fd4fd739a13fc20556d5e04816324271b9ef0f85b49ce092 hbox-v5-bringup_0.2.1.bin
|
||||
58
docs/descargas/hbox-v5-bringup-README.md
Normal file
58
docs/descargas/hbox-v5-bringup-README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# 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: `P`+`Y` stack ON/OFF, `n` NMEA monitor (fix 4 = RTK FIX). Console (0.2.1): CR+LF lines, `EVT` on every state change, named `STAT` every 5 s, `$PHLG` every 10 s in the fleet v3.x layout (server-parseable).
|
||||
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.
|
||||
|
||||
Procedure for technicians (ES): `06-Testing-Validation/2026-09-15_Drogheda_EinarBoard-v5_Procedimiento-Prueba-y-Flasheo_ES_v1.0.md`
|
||||
|
||||
## Build
|
||||
STM32duino core 3.x (`STMicroelectronics:stm32`). Target is the **generic F411RE**, not the Blackpill.
|
||||
```bash
|
||||
arduino-cli compile \
|
||||
--fqbn "STMicroelectronics:stm32:GenF4:pnum=GENERIC_F411RETX,usb=CDCgen,xserial=generic,rtlib=nanofp" \
|
||||
--build-property "build.extra_flags=-DHSE_VALUE=25000000UL" \
|
||||
--output-dir out hbox_v5_bringup
|
||||
```
|
||||
`rtlib=nanofp` is required — the default newlib-nano build prints nothing for `%f`, so every voltage
|
||||
and temperature line would be blank. `SystemClock_Config()` is overridden in the sketch (HSE 25 MHz → 96 MHz, USB 48 MHz) because the
|
||||
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.2.1.bin
|
||||
```
|
||||
or STM32CubeProgrammer → USB → Connect → Download `release/hbox-v5-bringup_0.2.1.bin` @ `0x08000000`.
|
||||
|
||||
## Release
|
||||
| File | Version | SHA-256 (first 16) | Size |
|
||||
|---|---|---|---|
|
||||
| **`release/hbox-v5-bringup_0.2.1.bin`** | **0.2.1 (2026-09-17) — USE THIS** | `78b34f4603550c05` | 70 084 B |
|
||||
| `release/superseded/hbox-v5-bringup_0.2.1.bin` | 0.2.0 — superseded (LF-only console, no EVT/$PHLG) | `78b34f46494f9ee1` | 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.1** (2026-09-17): console made monitorable — every line ends CR+LF (no more PuTTY staircase); `EVT up=…` line on every
|
||||
state change (stack ON/OFF with reason and the expected voltages, ignition edge, 5 V present/lost, charger status/fault edges,
|
||||
first NMEA frame / NMEA lost >5 s, GGA fix-quality change, stack refused/cancelled); `STAT` every 5 s with named values
|
||||
(`ign=ON`, `chrg=idle`, `flt=none`, `tntc=n/c`, `i2cerr=`, `t_on=`, `nmea=`/`bad=`); `$PHLG` every 10 s + after each stack change,
|
||||
**same 24-field layout + XOR checksum as fleet fw v3.x** (verified against `htracker/retranslator.py::_parse_phlg`; `fw=0.2.1-bu`,
|
||||
`state=BENCH|RUN`, `link=SRCH|DEGR|OK`); NMEA checksum verified (bad frames counted as `rxbad`); `P` arms non-blocking (10 s window),
|
||||
`Y` confirms; new commands `t` ($PHLG now), `l` (field legend), `v` (multimeter checkpoints for the stack gate: SYS5V/PWR_CTRL/GATE_LS/SW_5V);
|
||||
stack auto-OFF if 5 V is lost; `v5=…(BAJO)` + EVT when VEH5V drops under 4.6 V (LTC4040 PFI zone: PFI divider R51 270k/R52 100k
|
||||
→ 4.44 V falling, backup VSYS 4.79 V via R53 499k/R54 100k) — the 24-Jul modem-hiccup mechanism, now visible in the log. Build: no-op ctags + explicit definition order (arduino/ctags unreachable from the build host).
|
||||
- **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`).
|
||||
- **0.1.0** (2026-09-15): first build.
|
||||
|
||||
## ⚠ Do not flash `blackpillrtkcode_v3` (v3.0.3) on v5
|
||||
Its `PIN_BATLED1..4` = PB12–PB15 are the W5500 SPI2 bus on this board. `board_v5.h` is the
|
||||
verified v5 pin map — the production v5 firmware should include it rather than redefine pins.
|
||||
BIN
docs/descargas/hbox-v5-bringup_0.2.1.bin
Normal file
BIN
docs/descargas/hbox-v5-bringup_0.2.1.bin
Normal file
Binary file not shown.
40
docs/descargas/index.md
Normal file
40
docs/descargas/index.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Descargas
|
||||
|
||||
Archivos que el técnico necesita en el banco. Verificar el SHA-256 antes de flashear
|
||||
(`shasum -a 256 archivo` en macOS, `sha256sum archivo` en Linux, `certutil -hashfile archivo SHA256` en Windows).
|
||||
|
||||
## Firmware de bring-up — EinarBoard v5
|
||||
|
||||
| Archivo | Versión | SHA-256 | Uso |
|
||||
|---|---|---|---|
|
||||
| [**hbox-v5-bringup_0.2.1.bin**](hbox-v5-bringup_0.2.1.bin) | **0.2.1** (2026-09-17) | `78b34f4603550c05…` ([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.2.1.bin
|
||||
```
|
||||
|
||||
o con **STM32CubeProgrammer** → USB → Connect → Download del `.bin` en `0x08000000`.
|
||||
|
||||
!!! note "Tarjetas ya flasheadas con 0.1.1 / 0.2.0"
|
||||
Re-flashear con 0.2.1 por el mismo procedimiento DFU. **La primera línea de la consola tras reset dice la versión**
|
||||
(`hbox-v5-bringup 0.2.1 built …`): si la línea `STAT` no termina en `stack=…`, la tarjeta sigue con 0.1.1.
|
||||
|
||||
!!! tip "Consola 0.2.1"
|
||||
Líneas `EVT` en cada cambio (stack, ignición, 5 V, cargador, NMEA, fix), `STAT` con nombres cada 5 s y `$PHLG` cada 10 s
|
||||
(misma trama de salud que lee el servidor). Teclear `l` para la leyenda y `v` para los puntos de medición de la etapa RTK.
|
||||
Para encender la etapa RTK: `P` y luego `Y` (requiere 5 V en P2).
|
||||
|
||||
!!! 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`
|
||||
(v3.0.3) en una v5.**
|
||||
|
||||
Notas de compilación y cambios: [README del firmware de bring-up](hbox-v5-bringup-README.md).
|
||||
|
||||
## Herramientas
|
||||
|
||||
- **STM32CubeProgrammer** (Windows/macOS/Linux): https://www.st.com/en/development-tools/stm32cubeprog.html
|
||||
- **dfu-util** (macOS: `brew install dfu-util`; Linux: `apt install dfu-util`)
|
||||
- Terminal serie: CoolTerm, `screen /dev/tty.usbmodem* 115200`, o el monitor serie de Arduino IDE
|
||||
72
docs/formularios/acta.css
Normal file
72
docs/formularios/acta.css
Normal file
@ -0,0 +1,72 @@
|
||||
/* 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}
|
||||
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)}
|
||||
381
docs/formularios/acta.js
Normal file
381
docs/formularios/acta.js
Normal file
@ -0,0 +1,381 @@
|
||||
/* Motor de actas HBOX — renderiza el formulario, sincroniza con el servidor y firma.
|
||||
- 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
|
||||
|
||||
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 compuesta = Array.isArray(cfg.componentes) && cfg.componentes.length > 0;
|
||||
let admin = false;
|
||||
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);
|
||||
|
||||
const nav = $('div', { style: 'display:flex;justify-content:space-between;margin:2px 0 8px' });
|
||||
nav.append($('a', { class: 'volver', href: '../actas/', text: '← Volver a Actas' }));
|
||||
nav.append($('a', { class: 'volver', href: '../salir.html', text: 'Cerrar sesión' }));
|
||||
wrap.append(nav);
|
||||
|
||||
if (cfg.borrador)
|
||||
wrap.append($('div', { class: 'borrador', html: '⚠ <b>BORRADOR</b> — este formulario aún debe revisarse con ingeniería antes de usarlo como registro oficial.' }));
|
||||
if (cfg.intro) wrap.append($('div', { class: 'nota-item', style: 'margin:0 0 10px', html: cfg.intro }));
|
||||
|
||||
// ---------------- cabecera: serie (simple o compuesta) + técnico ----------------
|
||||
const datos = $('div', { class: 'datos' });
|
||||
const serieSel = $('select', { id: 'f_serie' }); // modo simple
|
||||
const serieCalc = $('input', { type: 'text', id: 'f_serie_calc', readonly: 'readonly', placeholder: 'se calcula al elegir los módulos' }); // modo compuesto
|
||||
const compSel = {}; // clave -> <select>
|
||||
const compBadge = {}; // clave -> <span> estado
|
||||
const preBadge = []; // prerrequisitos -> <span>
|
||||
const tecnico = $('input', { type: 'text', id: 'f_tecnico', placeholder: 'Nombre y apellido' });
|
||||
|
||||
if (!compuesta) {
|
||||
opciones(serieSel, cfg.prefijo, cfg.desde || 1, (cfg.desde || 1) + (cfg.series || 15) - 1, cfg.pad || 2);
|
||||
const dS = $('div'); dS.append($('label', { text: cfg.etiquetaSerie || 'Número de serie' }), serieSel);
|
||||
const dT = $('div'); dT.append($('label', { text: 'Técnico' }), tecnico);
|
||||
datos.append(dS, dT);
|
||||
} else {
|
||||
cfg.componentes.forEach(c => {
|
||||
const sel = $('select', { id: 'f_comp_' + c.clave, 'data-comp': c.clave });
|
||||
opciones(sel, c.prefijo, c.desde || 1, c.hasta || ((c.desde || 1) + (c.series || 15) - 1), c.pad || 2);
|
||||
const badge = $('span', { class: 'badge pend', text: '—' });
|
||||
const d = $('div'); d.append($('label', { text: c.etiqueta }), sel, badge);
|
||||
datos.append(d); compSel[c.clave] = sel; compBadge[c.clave] = badge;
|
||||
});
|
||||
const dSer = $('div', { class: 'serie-calc' }); dSer.append($('label', { text: cfg.etiquetaSerie || 'Número de serie de la unidad (automático)' }), serieCalc);
|
||||
(cfg.prerequisitos || []).forEach(p => {
|
||||
const b = $('span', { class: 'badge pend', text: '—' });
|
||||
const d = $('div'); d.append($('label', { text: p.etiqueta }), b); dSer.append(d); preBadge.push(b);
|
||||
});
|
||||
datos.append(dSer);
|
||||
const dT = $('div'); dT.append($('label', { text: 'Técnico' }), tecnico);
|
||||
datos.append(dT);
|
||||
}
|
||||
const cab = $('section', { class: 'paso' }); cab.append(datos);
|
||||
wrap.append(cab);
|
||||
|
||||
const banner = $('div', { class: 'msg', id: 'f_banner' });
|
||||
wrap.append(banner);
|
||||
|
||||
// selección actual de componentes -> {clave: valor}
|
||||
function seleccion() { const s = {}; for (const k in compSel) s[k] = compSel[k].value; return s; }
|
||||
function serieActual() {
|
||||
if (!compuesta) return serieSel.value;
|
||||
const s = seleccion();
|
||||
if (Object.values(s).some(v => !v)) return '';
|
||||
if (Object.values(s).some(esPrueba)) return (cfg.prefijoPrueba || 'PRUEBA') + '-PRUEBA';
|
||||
return cfg.serie(s);
|
||||
}
|
||||
const key = () => 'acta:' + cfg.tipo + ':' + (serieActual() || '');
|
||||
|
||||
// ---------------- secciones ----------------
|
||||
cfg.secciones.forEach((sec, si) => {
|
||||
const s = $('section', { class: 'paso' });
|
||||
s.append($('h2', { text: sec.titulo }));
|
||||
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: it.modo || 'decimal', 'data-id': id, 'data-lbl': it.txt, placeholder: it.ph || 'medido' }));
|
||||
if (it.u) box.append($('span', { class: 'u', text: it.u }));
|
||||
} else if (it.tipo === 'texto') {
|
||||
box.append($('label', { text: it.txt }));
|
||||
box.append($('textarea', { rows: it.rows || '2', 'data-id': id, 'data-lbl': it.txt, placeholder: it.ph || '' }));
|
||||
} else if (it.tipo === 'radio') {
|
||||
box.append($('label', { text: it.txt }));
|
||||
const g = $('div', { class: 'radios' });
|
||||
(it.opciones || []).forEach(o => {
|
||||
const l = $('label', { class: 'rad' });
|
||||
l.append($('input', { type: 'radio', name: 'r_' + id, value: o, 'data-id': id, 'data-lbl': it.txt }), document.createTextNode(' ' + o));
|
||||
g.append(l);
|
||||
});
|
||||
box.append(g);
|
||||
} else if (it.tipo === 'nota') {
|
||||
box.append($('div', { class: 'nota-item', html: it.txt }));
|
||||
}
|
||||
s.append(box);
|
||||
});
|
||||
wrap.append(s);
|
||||
});
|
||||
|
||||
// ---------------- firma ----------------
|
||||
const firma = $('section', { class: 'paso firma' });
|
||||
firma.append($('h2', { text: 'Cierre y firma' }));
|
||||
firma.append($('p', { class: 'sub', text: 'Al enviar, quedas registrado como responsable de esta prueba.' }));
|
||||
const res = $('div', { class: 'resultado' });
|
||||
const rOK = $('label', { class: 'ok' }); rOK.append($('input', { type: 'radio', name: 'resultado', value: 'OK' }), document.createTextNode(cfg.textoOK || 'APROBADA'));
|
||||
const rNO = $('label', { class: 'falla' }); rNO.append($('input', { type: 'radio', name: 'resultado', value: 'FALLA' }), document.createTextNode('FALLA'));
|
||||
res.append(rOK, rNO);
|
||||
firma.append($('label', { text: 'Resultado final' }), res);
|
||||
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);
|
||||
|
||||
const barra = $('div', { class: 'barra' });
|
||||
const estado = $('span', { class: 'estado', id: 'f_estado', text: 'Sin guardar' });
|
||||
const limpiar = $('button', { class: 'limpiar', id: 'f_limpiar', text: 'Limpiar' });
|
||||
const borrar = $('button', { class: 'limpiar', id: 'f_borrar', text: 'Borrar acta (reabrir)', style: 'display:none;border-color:var(--falla);color:var(--falla)' });
|
||||
const btn = $('button', { class: 'enviar', id: 'f_enviar', text: 'Enviar acta firmada' });
|
||||
const grp = $('div', { style: 'display:flex;gap:10px' }); grp.append(limpiar, borrar, btn);
|
||||
barra.append(estado, grp);
|
||||
document.body.append(barra);
|
||||
|
||||
// ---------------- helpers de estado ----------------
|
||||
const conforme = () => document.getElementById('f_conforme');
|
||||
function fields() { return wrap.querySelectorAll('[data-id]'); }
|
||||
function setEnabled(on) {
|
||||
fields().forEach(el => el.disabled = !on);
|
||||
tecnico.disabled = !on; conforme().disabled = !on;
|
||||
wrap.querySelectorAll('input[name=resultado]').forEach(el => el.disabled = !on);
|
||||
}
|
||||
function blankFields() {
|
||||
fields().forEach(el => { if (el.type === 'checkbox' || el.type === 'radio') el.checked = false; else el.value = ''; });
|
||||
const rr = wrap.querySelector('input[name=resultado]:checked'); if (rr) rr.checked = false;
|
||||
conforme().checked = false; tecnico.value = '';
|
||||
}
|
||||
function fillFrom(a) {
|
||||
tecnico.value = a.tecnico || '';
|
||||
conforme().checked = !!a.conforme;
|
||||
if (a.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${a.resultado}"]`); if (el) el.checked = true; }
|
||||
for (const k in (a.respuestas || {})) {
|
||||
const v = a.respuestas[k].val;
|
||||
const els = wrap.querySelectorAll(`[data-id="${k}"]`); if (!els.length) continue;
|
||||
els.forEach(el => {
|
||||
if (el.type === 'checkbox') el.checked = !!v;
|
||||
else if (el.type === 'radio') el.checked = (el.value === v);
|
||||
else el.value = v || '';
|
||||
});
|
||||
}
|
||||
}
|
||||
function showEditable() {
|
||||
setEnabled(true);
|
||||
limpiar.style.display = ''; btn.style.display = ''; borrar.style.display = 'none';
|
||||
wrap.classList.remove('enviada'); banner.className = 'msg';
|
||||
}
|
||||
function showLocked(a) {
|
||||
setEnabled(false);
|
||||
limpiar.style.display = 'none'; btn.style.display = 'none';
|
||||
borrar.style.display = admin ? '' : 'none';
|
||||
banner.className = 'msg ok';
|
||||
const f = (a._recibido || '').replace('T', ' ').slice(0, 16);
|
||||
banner.innerHTML = '✓ <b>Acta ya enviada</b> por <b>' + (a.tecnico || '—') + '</b>'
|
||||
+ (f ? ' el ' + f : '') + '. Registro final'
|
||||
+ (a.resultado ? ' — resultado <b>' + a.resultado + '</b>' : '') + '.'
|
||||
+ (admin ? '' : ' Para rehacerla, pide a Leo que la borre.');
|
||||
estado.textContent = 'Bloqueada';
|
||||
}
|
||||
|
||||
// ---------------- verificación de componentes / prerrequisitos (modo compuesto) ----------------
|
||||
let verif = {}; // clave -> 'OK' | 'FALLA' | 'SIN_ACTA' | 'PRUEBA' | 'EN_USO'
|
||||
let usados = null; // { clave: { valor: serieUnidad } } — componentes ya asignados en otras actas de este tipo
|
||||
function setBadge(el, st, extra) {
|
||||
const map = { OK: ['ok', '✓ Aprobado'], FALLA: ['falla', '✕ FALLA'], SIN_ACTA: ['falla', '✕ Sin acta'],
|
||||
PRUEBA: ['pend', '⚙ prueba'], EN_USO: ['falla', '⚠ ya en ' + (extra || 'otra unidad')], CONSULTANDO: ['pend', '…'] };
|
||||
const m = map[st] || ['pend', '—'];
|
||||
el.className = 'badge ' + m[0]; el.textContent = m[1];
|
||||
}
|
||||
async function estadoActa(tipo, serie) {
|
||||
if (!serie) return 'SIN_ACTA';
|
||||
if (esPrueba(serie)) return 'PRUEBA';
|
||||
try {
|
||||
const r = await fetch(`/api/acta?tipo=${encodeURIComponent(tipo)}&serie=${encodeURIComponent(serie)}`);
|
||||
const j = await r.json();
|
||||
if (!j.exists) return 'SIN_ACTA';
|
||||
return j.acta && j.acta.resultado === 'FALLA' ? 'FALLA' : 'OK';
|
||||
} catch (e) { return 'SIN_ACTA'; }
|
||||
}
|
||||
async function cargarUsados() {
|
||||
if (!cfg.unicoPorComponente) { usados = {}; return; }
|
||||
usados = {};
|
||||
try {
|
||||
const r = await fetch('/api/actas', { cache: 'no-store' }); const j = await r.json();
|
||||
(j.actas || []).filter(a => a.tipo === cfg.tipo && a.componentes && !esPrueba(a.serie)).forEach(a => {
|
||||
for (const k in a.componentes) { usados[k] = usados[k] || {}; usados[k][a.componentes[k]] = a.serie; }
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
async function verificar() {
|
||||
verif = {};
|
||||
const s = seleccion();
|
||||
const tareas = cfg.componentes.map(async c => {
|
||||
const v = s[c.clave]; const b = compBadge[c.clave];
|
||||
if (!v) { setBadge(b, null); verif[c.clave] = 'SIN_SELECCION'; return; }
|
||||
setBadge(b, 'CONSULTANDO');
|
||||
let st = await estadoActa(c.tipoActa, v);
|
||||
if (st === 'OK' && usados && usados[c.clave] && usados[c.clave][v] && usados[c.clave][v] !== serieActual()) {
|
||||
st = 'EN_USO'; setBadge(b, st, usados[c.clave][v]); verif[c.clave] = st; return;
|
||||
}
|
||||
setBadge(b, st); verif[c.clave] = st;
|
||||
});
|
||||
const pre = (cfg.prerequisitos || []).map(async (p, i) => {
|
||||
const b = preBadge[i];
|
||||
if (Object.values(s).some(v => !v)) { setBadge(b, null); verif['pre_' + i] = 'SIN_SELECCION'; return; }
|
||||
setBadge(b, 'CONSULTANDO');
|
||||
const st = await estadoActa(p.tipo, Object.values(s).some(esPrueba) ? 'PRUEBA' : p.serie(s));
|
||||
setBadge(b, st); verif['pre_' + i] = st;
|
||||
});
|
||||
await Promise.all(tareas.concat(pre));
|
||||
}
|
||||
function todoAprobado() {
|
||||
return Object.values(verif).every(v => v === 'OK' || v === 'PRUEBA');
|
||||
}
|
||||
|
||||
// ---------------- autoguardado local ----------------
|
||||
let t, locked = false;
|
||||
function snapshot() {
|
||||
const r = {};
|
||||
fields().forEach(el => {
|
||||
if (el.type === 'radio') { if (el.checked) r[el.dataset.id] = { lbl: el.dataset.lbl, val: el.value }; else if (!r[el.dataset.id]) r[el.dataset.id] = { lbl: el.dataset.lbl, val: '' }; }
|
||||
else r[el.dataset.id] = { lbl: el.dataset.lbl, val: el.type === 'checkbox' ? el.checked : el.value };
|
||||
});
|
||||
const rr = wrap.querySelector('input[name=resultado]:checked');
|
||||
const d = { tipo: cfg.tipo, serie: serieActual(), tecnico: tecnico.value,
|
||||
conforme: conforme().checked, resultado: rr ? rr.value : '', respuestas: r };
|
||||
if (compuesta) { d.componentes = seleccion(); d.verificacion = Object.assign({}, verif); }
|
||||
return d;
|
||||
}
|
||||
function guardarLocal() {
|
||||
if (locked || !serieActual()) 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 cargarLocal() {
|
||||
let d; try { d = JSON.parse(localStorage.getItem(key()) || 'null'); } catch (e) {}
|
||||
if (d) { fillFrom(d); estado.textContent = 'Borrador recuperado'; }
|
||||
else estado.textContent = 'Nueva acta';
|
||||
}
|
||||
|
||||
// ---------------- cargar una serie ----------------
|
||||
async function loadSerie() {
|
||||
msg.className = 'msg'; locked = false;
|
||||
const s = serieActual();
|
||||
if (compuesta) serieCalc.value = s || '';
|
||||
blankFields(); showEditable();
|
||||
if (compuesta) { if (usados === null) await cargarUsados(); await verificar(); }
|
||||
if (!s) { estado.textContent = compuesta ? 'Elige los tres módulos' : 'Elige el número de serie'; return; }
|
||||
if (esPrueba(s)) { cargarLocal(); estado.textContent = 'Modo prueba (no cuenta)'; return; }
|
||||
estado.textContent = 'Consultando…';
|
||||
let j = null;
|
||||
try { const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(s)}`); j = await r.json(); } catch (e) {}
|
||||
if (j && j.exists) { locked = true; fillFrom(j.acta); showLocked(j.acta); }
|
||||
else {
|
||||
cargarLocal();
|
||||
if (compuesta && cfg.exigirAprobados && !todoAprobado()) {
|
||||
banner.className = 'msg err';
|
||||
banner.innerHTML = '⛔ <b>No se puede ensamblar con esta selección.</b> Todos los módulos deben estar <b>aprobados</b> en su acta y no estar ya asignados a otra unidad. Revisa los indicadores de arriba.';
|
||||
}
|
||||
}
|
||||
}
|
||||
wrap.addEventListener('input', guardarLocal);
|
||||
serieSel.addEventListener('change', loadSerie);
|
||||
Object.values(compSel).forEach(sel => sel.addEventListener('change', loadSerie));
|
||||
|
||||
limpiar.addEventListener('click', () => {
|
||||
if (locked) return;
|
||||
if (!confirm('¿Limpiar este formulario y empezar de nuevo?')) return;
|
||||
try { localStorage.removeItem(key()); } catch (e) {}
|
||||
blankFields(); msg.className = 'msg'; estado.textContent = 'Formulario limpio'; window.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
borrar.addEventListener('click', async () => {
|
||||
if (!admin) return;
|
||||
if (!confirm('¿Borrar el acta de ' + serieActual() + '? Quedará disponible para rehacerse.')) return;
|
||||
borrar.disabled = true;
|
||||
try {
|
||||
const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(serieActual())}`, { method: 'DELETE' });
|
||||
const j = await r.json();
|
||||
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||
locked = false; blankFields(); showEditable(); usados = null;
|
||||
msg.className = 'msg ok'; msg.textContent = 'Acta borrada. Ya puede rehacerse.';
|
||||
estado.textContent = 'Reabierta';
|
||||
} catch (e) { msg.className = 'msg err'; msg.textContent = 'No se pudo borrar: ' + e.message; }
|
||||
borrar.disabled = false;
|
||||
});
|
||||
|
||||
// ---------------- envío ----------------
|
||||
btn.addEventListener('click', async () => {
|
||||
if (locked) return;
|
||||
const d = snapshot();
|
||||
if (!d.serie) return fail(compuesta ? 'Elige los tres módulos.' : 'Elige el número de serie.');
|
||||
if (compuesta && cfg.exigirAprobados && !esPrueba(d.serie) && !todoAprobado())
|
||||
return fail('Hay módulos sin aprobar, sin acta o ya asignados a otra unidad. No se puede enviar.');
|
||||
if (!d.tecnico.trim()) return fail('Escribe tu nombre.');
|
||||
if (!d.resultado) return fail('Marca el resultado final (Aprobada o Falla).');
|
||||
if (!d.conforme) return fail('Debes confirmar que seguiste el procedimiento.');
|
||||
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.status === 409) {
|
||||
try { localStorage.removeItem(key()); } catch (e) {}
|
||||
usados = null; await loadSerie();
|
||||
msg.className = 'msg err'; msg.textContent = j.error || 'Ya existe un acta para esta serie.';
|
||||
btn.disabled = false; btn.textContent = 'Enviar acta firmada';
|
||||
return;
|
||||
}
|
||||
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||
try { localStorage.removeItem(key()); } catch (e) {}
|
||||
msg.className = 'msg ok';
|
||||
msg.innerHTML = '✓ Acta enviada y registrada como <b>' + d.serie + '</b>.'
|
||||
+ (compuesta && !esPrueba(d.serie) ? '<br>Etiqueta de la unidad: <b style="font-size:18px;letter-spacing:.04em">' + d.serie + '</b>' : '');
|
||||
if (!j.prueba) { locked = true; showLocked({ tecnico: d.tecnico, resultado: d.resultado, _recibido: new Date().toISOString() }); }
|
||||
else { btn.disabled = false; btn.textContent = 'Enviar acta firmada'; estado.textContent = 'Prueba enviada'; }
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
} catch (e) {
|
||||
fail('No se pudo enviar: ' + e.message + '. Revisa tu conexión (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); }
|
||||
|
||||
(async () => {
|
||||
try { const r = await fetch('/api/whoami'); const j = await r.json(); admin = !!j.admin; } catch (e) {}
|
||||
loadSerie();
|
||||
})();
|
||||
}
|
||||
window.Acta = { render, num };
|
||||
})();
|
||||
52
docs/formularios/ardusimple.html
Normal file
52
docs/formularios/ardusimple.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Acta de módulo — simpleRTK2B Pro (RTK)</title>
|
||||
<link rel="stylesheet" href="acta.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="acta"></div>
|
||||
<script src="acta.js"></script>
|
||||
<script>
|
||||
Acta.render({
|
||||
tipo: 'modulo-ard',
|
||||
prefijo: 'ARD', desde: 1, series: 30, pad: 3, // ARD-001 … ARD-030
|
||||
etiquetaSerie: 'Número de serie del módulo (etiqueta ARD-NNN)',
|
||||
titulo: 'Acta de módulo — simpleRTK2B Pro (ZED-F9P)',
|
||||
subtitulo: 'Un acta por receptor. Procedimiento: "Módulos e integración" §2. Sin EinarBoard.',
|
||||
secciones: [
|
||||
{ titulo: '2a · Inspección visual (sin energía)',
|
||||
items: [
|
||||
{ tipo:'medida', txt:'Etiqueta de fábrica ArduSimple (código impreso en el módulo)', modo:'text', ph:'p.ej. SRTK2B-…' },
|
||||
{ tipo:'check', txt:'Etiqueta <b>ARD-NNN</b> puesta y legible, sin tapar SMA ni zócalo XBee.' },
|
||||
{ tipo:'check', txt:'Pines del zócalo <b>XBee</b> (2 × 10) rectos; conector <b>SMA</b> firme.' },
|
||||
{ tipo:'check', txt:'Tiras tipo Arduino (las que entran en MOD1) rectas y completas.' },
|
||||
] },
|
||||
{ titulo: '2b · Energizado y fix GNSS',
|
||||
sub: 'Antena GNSS al SMA con vista al cielo. USB-C en POWER+GPS. u-center conectado.',
|
||||
items: [
|
||||
{ tipo:'check', txt:'LED <b>POWER</b> encendido; la laptop ve un puerto serie u-blox.' },
|
||||
{ tipo:'medida', txt:'Versión de firmware del ZED-F9P (UBX-MON-VER)', modo:'text', ph:'p.ej. HPG 1.32' },
|
||||
{ tipo:'medida', txt:'Tiempo hasta fix 3D', esperado:'< 2 min a cielo abierto', u:'min' },
|
||||
{ tipo:'medida', txt:'Satélites en uso con fix 3D', esperado:'≥ 10', u:'sat' },
|
||||
{ tipo:'check', txt:'LED <b>GPS FIX</b> parpadea con el fix.' },
|
||||
] },
|
||||
{ titulo: '2c · Configuración de Helgen',
|
||||
sub: 'u-center → Tools → Receiver Configuration → Load → Transfer file → GNSS. Luego UBX-CFG-CFG → Save.',
|
||||
items: [
|
||||
{ tipo:'medida', txt:'Nombre del archivo de configuración cargado', modo:'text', ph:'p.ej. HBOX_F9P_115200_v1.txt' },
|
||||
{ tipo:'check', txt:'Configuración transferida sin errores y <b>guardada</b> (CFG-CFG Save).' },
|
||||
{ tipo:'check', txt:'Tras desconectar/reconectar USB, conecta a <b>115200</b> y sigue mostrando fix y <code>$GNGGA</code>.' },
|
||||
{ tipo:'texto', txt:'Observaciones', ph:'ninguna / describir' },
|
||||
] },
|
||||
{ titulo: 'Cierre',
|
||||
items: [
|
||||
{ tipo:'check', txt:'Módulo en bolsa antiestática con su etiqueta ARD-NNN.' },
|
||||
] },
|
||||
]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
89
docs/formularios/ensamble.html
Normal file
89
docs/formularios/ensamble.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Acta de ensamble — unidad HBOX v5</title>
|
||||
<link rel="stylesheet" href="acta.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="acta"></div>
|
||||
<script src="acta.js"></script>
|
||||
<script>
|
||||
Acta.render({
|
||||
tipo: 'ensamble',
|
||||
titulo: 'Acta de ensamble — unidad HBOX v5',
|
||||
subtitulo: 'Una unidad a la vez. Procedimiento: "Ensamble de la unidad y energizado final".',
|
||||
intro: '<b>Primero el acta, luego el desarmador.</b> Selecciona los tres módulos: el sitio verifica que cada uno esté <b>aprobado</b>, que la pareja ArduSimple+4G tenga su <b>integración</b> y que ninguno esté ya en otra unidad. El número de serie de la unidad se calcula solo.',
|
||||
componentes: [
|
||||
{ clave:'einar', etiqueta:'EinarBoard v5', prefijo:'V5', desde:1, hasta:30, pad:2, tipoActa:'prueba-v5' },
|
||||
{ clave:'ard', etiqueta:'simpleRTK2B Pro (ArduSimple)', prefijo:'ARD', desde:1, hasta:30, pad:3, tipoActa:'modulo-ard' },
|
||||
{ clave:'modem', etiqueta:'4G NTRIP Master', prefijo:'RTK', desde:1, hasta:30, pad:4, tipoActa:'modulo-4g' },
|
||||
],
|
||||
serie: s => 'HBOX5-E' + Acta.num(s.einar) + '-A' + Acta.num(s.ard) + '-R' + Acta.num(s.modem),
|
||||
prefijoPrueba: 'HBOX5',
|
||||
prerequisitos: [
|
||||
{ etiqueta:'Integración ArduSimple + 4G', tipo:'integracion', serie: s => 'INT-A' + Acta.num(s.ard) + '-R' + Acta.num(s.modem) },
|
||||
],
|
||||
exigirAprobados: true,
|
||||
unicoPorComponente: true,
|
||||
textoOK: 'APROBADA — UNIDAD LISTA',
|
||||
secciones: [
|
||||
{ titulo: '2 · Preparación de la tarjeta (sin energía)',
|
||||
items: [
|
||||
{ tipo:'check', txt:'La EinarBoard física es la seleccionada arriba (etiqueta cerca de MH4).' },
|
||||
{ tipo:'radio', txt:'JP2 (F2)', opciones:['1 = Li-ion','0 = LiFePO4'] },
|
||||
{ tipo:'radio', txt:'JP3 (F1)', opciones:['0','1'] },
|
||||
{ tipo:'radio', txt:'JP4 (F0)', opciones:['1','0'] },
|
||||
{ tipo:'check', txt:'Posición esperada para el pack Li-ion 4.00 V: <b>JP2 = 1 · JP3 = 0 · JP4 = 1</b>. JP1 (0R) presente.' },
|
||||
{ tipo:'check', txt:'P11 sin jumper (el interruptor de panel va ahí). Tarjeta fijada con separadores, sin flexión.' },
|
||||
] },
|
||||
{ titulo: '3 · Módulos enchufables (sin energía)',
|
||||
items: [
|
||||
{ tipo:'check', txt:'simpleRTK2B Pro en <b>MOD1</b>: pin "5V" alineado, cuatro tiras a fondo, ninguna clavija fuera.' },
|
||||
{ tipo:'check', txt:'4G NTRIP Master en <b>J7/J8</b> de la EinarBoard (no en el simpleRTK2B), silueta coincide con la serigrafía.' },
|
||||
{ tipo:'check', txt:'SIM presente. Nada roza la tapa ni componentes altos (K1, C10, L6).' },
|
||||
] },
|
||||
{ titulo: '4 · Panel y arnés interno (sin energía)',
|
||||
items: [
|
||||
{ tipo:'check', txt:'<b>P1</b> ← GX16: 1 = +24 V, 2 = GND, 3 = ignición, 4 = GND. Polaridad verificada con multímetro.' },
|
||||
{ tipo:'medida', txt:'Salida de la SD-15B-05 ajustada en banco', esperado:'5.00 V', u:'V' },
|
||||
{ tipo:'check', txt:'<b>P2</b> ← SD-15B (pin 1 = +5 V). Entrada de la SD-15B al +24 V/GND del GX16.' },
|
||||
{ tipo:'check', txt:'<b>P6</b> ventilador (flujo hacia afuera) · <b>P8</b> LED panel · <b>P10</b> reset · <b>P11</b> interruptor de corte · <b>P12</b> NTC punto caliente.' },
|
||||
{ tipo:'check', txt:'Antena GNSS: bulkhead SMA → SMA del simpleRTK2B, con fijador de rosca.' },
|
||||
{ tipo:'check', txt:'2 antenas LTE: bulkheads SMA → u.FL del módulo 4G (clic), sin dobleces cerrados.' },
|
||||
{ tipo:'check', txt:'<b>P4</b> (batería) todavía <b>NO</b> conectado. Cables sujetos; nada expuesto cerca de 24 V.' },
|
||||
] },
|
||||
{ titulo: '5 · Energizado desde 24 V (sin batería)',
|
||||
sub: 'Fuente 24.0 V, límite 1.0 A al GX16. Interruptor de corte ON. USB en J11, terminal 115200.',
|
||||
items: [
|
||||
{ tipo:'medida', txt:'Corriente con la etapa RTK apagada', esperado:'< 150 mA', u:'mA' },
|
||||
{ tipo:'check', txt:'D18 verde encendido · D17 azul y LED de panel parpadean · ventilador gira.' },
|
||||
{ tipo:'check', txt:'Consola: encabezado <code>hbox-v5-bringup</code> con <b>12 PASS</b>.' },
|
||||
{ tipo:'medida', txt:'STAT v5', esperado:'4.8 – 5.1 V', u:'V' },
|
||||
{ tipo:'medida', txt:'STAT tmp102 / sht40', modo:'text', ph:'p.ej. 31.2 / 30.8 °C' },
|
||||
{ tipo:'check', txt:'Comando <b>P</b> + <b>Y</b> → <code>STACK ON</code>. simpleRTK2B POWER encendido; 4G se registra (< 2 min).' },
|
||||
{ tipo:'medida', txt:'Corriente con la etapa RTK encendida', esperado:'250 – 600 mA', u:'mA' },
|
||||
{ tipo:'medida', txt:'Comando <b>n</b>: tiempo hasta <code>fix=4</code> (RTK FIX)', esperado:'< 10 min con antena al cielo', u:'min' },
|
||||
{ tipo:'medida', txt:'Satélites reportados en <code>n</code>', esperado:'≥ 10', u:'sat' },
|
||||
{ tipo:'check', txt:'Comando <b>P</b> de nuevo → <code>STACK OFF</code>; corriente vuelve a < 150 mA.' },
|
||||
] },
|
||||
{ titulo: '6 · Batería y respaldo',
|
||||
sub: 'Etapa RTK apagada. Pack en P4 (1 = BAT+, 2 = GND, 3 = NTC).',
|
||||
items: [
|
||||
{ tipo:'medida', txt:'Voltaje del pack antes de conectar (BAT+ a GND)', esperado:'3.0 – 4.2 V', u:'V' },
|
||||
{ tipo:'medida', txt:'STAT vbat con el pack conectado', esperado:'≈ voltaje del pack', u:'V' },
|
||||
{ tipo:'check', txt:'<b>Respaldo:</b> al cortar 24 V la consola y los LEDs <b>siguen vivos</b>, sin reinicio (el encabezado no se reimprime). STAT v5 = 0.0.' },
|
||||
{ tipo:'check', txt:'Al regresar 24 V, STAT v5 ≈ 5.0 sin reinicio.' },
|
||||
{ tipo:'texto', txt:'Observaciones (reinicios, calentamiento, corriente fuera de rango)', ph:'ninguna / describir' },
|
||||
] },
|
||||
{ titulo: '7 · Cierre y etiquetado',
|
||||
items: [
|
||||
{ tipo:'check', txt:'Etiqueta <b>exterior</b> con el número de la unidad junto al GX16; etiqueta <b>interior</b> en la tapa.' },
|
||||
{ tipo:'check', txt:'Interruptor de corte en <b>OFF</b> para almacenar. Tapa cerrada con sello, tornillos en cruz, prensaestopas apretados.' },
|
||||
] },
|
||||
]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
52
docs/formularios/integracion.html
Normal file
52
docs/formularios/integracion.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Acta de integración — RTK + 4G</title>
|
||||
<link rel="stylesheet" href="acta.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="acta"></div>
|
||||
<script src="acta.js"></script>
|
||||
<script>
|
||||
Acta.render({
|
||||
tipo: 'integracion',
|
||||
titulo: 'Acta de integración — simpleRTK2B Pro + 4G NTRIP Master',
|
||||
subtitulo: 'La pareja que irá junta en una unidad. Procedimiento: "Módulos e integración" §4.',
|
||||
intro: 'Elige los dos módulos. Ambos deben estar <b>aprobados</b> en su propia acta; el sitio lo verifica al seleccionarlos.',
|
||||
componentes: [
|
||||
{ clave:'ard', etiqueta:'simpleRTK2B Pro (ArduSimple)', prefijo:'ARD', desde:1, hasta:30, pad:3, tipoActa:'modulo-ard' },
|
||||
{ clave:'modem', etiqueta:'4G NTRIP Master', prefijo:'RTK', desde:1, hasta:30, pad:4, tipoActa:'modulo-4g' },
|
||||
],
|
||||
serie: s => 'INT-A' + Acta.num(s.ard) + '-R' + Acta.num(s.modem),
|
||||
prefijoPrueba: 'INT',
|
||||
etiquetaSerie: 'Identificador de la pareja (automático)',
|
||||
exigirAprobados: true,
|
||||
secciones: [
|
||||
{ titulo: '4 · Montaje',
|
||||
sub: '4G sobre el zócalo XBee del simpleRTK2B, 2 antenas LTE, antena GNSS al cielo, USB POWER+GPS. u-center a 115200.',
|
||||
items: [
|
||||
{ tipo:'check', txt:'Montaje completo <b>sin energía</b>; antenas conectadas antes de energizar.' },
|
||||
{ tipo:'check', txt:'Fix <b>3D</b> en u-center.' },
|
||||
{ tipo:'check', txt:'LED de red del 4G registrado; LED <b>NO RTK</b> del simpleRTK2B empieza a <b>parpadear</b> (llegan correcciones).' },
|
||||
] },
|
||||
{ titulo: '4 · RTK FIX',
|
||||
items: [
|
||||
{ tipo:'medida', txt:'Tiempo desde encendido hasta RTK FIX (LED NO RTK apagado / Fix Mode FIXED)', esperado:'< 10 min', u:'min' },
|
||||
{ tipo:'medida', txt:'Satélites en uso', esperado:'≥ 10', u:'sat' },
|
||||
{ tipo:'medida', txt:'Precisión estimada hAcc en FIX', esperado:'< 0.05 m', u:'m' },
|
||||
{ tipo:'check', txt:'Se mantiene <b>FIXED 5 min</b> sin caer a FLOAT (antena quieta).' },
|
||||
{ tipo:'radio', txt:'Confirmación en la plataforma Helgen (estación AS+K reportando)', opciones:['Confirmado por ingeniería','Pendiente de confirmar','No aparece'] },
|
||||
{ tipo:'medida', txt:'Hora de la confirmación / a quién se avisó', modo:'text', ph:'p.ej. 12:40, Leo' },
|
||||
{ tipo:'texto', txt:'Observaciones (FLOAT sin FIX, caídas, señal…)', ph:'ninguna / describir' },
|
||||
] },
|
||||
{ titulo: '4b · Cierre',
|
||||
items: [
|
||||
{ tipo:'check', txt:'Módulos separados con cuidado; cada uno en su bolsa. En la bolsa del ArduSimple anotada la pareja RTK-NNNN.' },
|
||||
] },
|
||||
]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
52
docs/formularios/modem4g.html
Normal file
52
docs/formularios/modem4g.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Acta de módulo — 4G NTRIP Master</title>
|
||||
<link rel="stylesheet" href="acta.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="acta"></div>
|
||||
<script src="acta.js"></script>
|
||||
<script>
|
||||
Acta.render({
|
||||
tipo: 'modulo-4g',
|
||||
prefijo: 'RTK', desde: 1, series: 30, pad: 4, // RTK-0001 … RTK-0030 (etiqueta del módem 4G)
|
||||
etiquetaSerie: 'Número de serie del módem (etiqueta RTK-NNNN)',
|
||||
titulo: 'Acta de módulo — 4G NTRIP Master',
|
||||
subtitulo: 'Un acta por módem. Procedimiento: "Módulos e integración" §3. Antenas LTE conectadas ANTES de energizar.',
|
||||
secciones: [
|
||||
{ titulo: '3a · Inspección visual (sin energía)',
|
||||
items: [
|
||||
{ tipo:'medida', txt:'IMEI (etiqueta del módem)', modo:'numeric', ph:'15 dígitos' },
|
||||
{ tipo:'medida', txt:'ICCID de la SIM Telcel', modo:'numeric', ph:'19–20 dígitos' },
|
||||
{ tipo:'check', txt:'Etiqueta <b>RTK-NNNN</b> puesta; SIM insertada con clic en la dirección correcta.' },
|
||||
{ tipo:'check', txt:'Pines XBee (2 × 10, 2 mm) rectos; conectores <b>u.FL</b> sin daño.' },
|
||||
] },
|
||||
{ titulo: '3b · Montaje sobre el simpleRTK2B y configuración',
|
||||
sub: 'Sin energía: módulo en el zócalo XBee del simpleRTK2B. 2 antenas LTE + antena GNSS. USB POWER+GPS y USB XBEE conectados. Terminal en el puerto XBEE a 115200.',
|
||||
items: [
|
||||
{ tipo:'medida', txt:'simpleRTK2B usado como base para esta prueba', modo:'text', ph:'ARD-NNN' },
|
||||
{ tipo:'check', txt:'Archivo <code>4G_NTRIP_HelgenSRV-NMEA.local.txt</code> enviado línea por línea; <b>ninguna</b> línea con <code>ERROR</code>.' },
|
||||
{ tipo:'medida', txt:'ID de estación programado (<code>$$$AS+K</code>)', modo:'text', ph:'según ingeniería' },
|
||||
{ tipo:'check', txt:'Configuración verificada al releer (el módulo responde con los valores enviados).' },
|
||||
] },
|
||||
{ titulo: '3c · Registro en la red',
|
||||
sub: 'Esperar hasta 2 min con las antenas LTE conectadas.',
|
||||
items: [
|
||||
{ tipo:'check', txt:'LED de red del módulo pasa de parpadeo rápido a <b>lento o fijo</b> (registrado).' },
|
||||
{ tipo:'medida', txt:'Operador reportado', modo:'text', esperado:'Telcel', ph:'Telcel' },
|
||||
{ tipo:'medida', txt:'Señal (RSSI/CSQ) si el módulo la reporta', modo:'text', ph:'p.ej. 18/31 o −75 dBm' },
|
||||
{ tipo:'medida', txt:'Tiempo hasta registrarse', esperado:'< 2 min', u:'min' },
|
||||
{ tipo:'texto', txt:'Observaciones', ph:'ninguna / describir' },
|
||||
] },
|
||||
{ titulo: 'Cierre',
|
||||
items: [
|
||||
{ tipo:'check', txt:'Módulo separado del simpleRTK2B sin doblar pines; en bolsa con su etiqueta RTK-NNNN.' },
|
||||
] },
|
||||
]
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
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: 30, // 15 en el lote actual; 30 seleccionables
|
||||
titulo: 'Acta de prueba y flasheo — EinarBoard V5',
|
||||
subtitulo: 'Lote 15 tarjetas (JLC 22-Ago). Llena cada sección durante la prueba.',
|
||||
secciones: [
|
||||
{ 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.1.bin @ 0x08000000.',
|
||||
items: [
|
||||
{ tipo:'check', txt:'DFU: mantener <b>SW2 (BOOT0)</b>, presionar y soltar <b>SW1 (RESET)</b>, soltar SW2 → aparece "STM32 BOOTLOADER" (0483:DF11).' },
|
||||
{ tipo:'check', txt:'Flasheado con STM32CubeProgrammer (o dfu-util): "download complete" + "verify successful".' },
|
||||
] },
|
||||
{ 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.1</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>
|
||||
95
docs/formularios/ver.html
Normal file
95
docs/formularios/ver.html
Normal file
@ -0,0 +1,95 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Ver acta — HBOX</title>
|
||||
<link rel="stylesheet" href="acta.css">
|
||||
<style>
|
||||
.doc{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:18px}
|
||||
.hdr{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:12px}
|
||||
.hdr .c{background:var(--papel);border:1px solid var(--linea);border-radius:9px;padding:8px 10px}
|
||||
.hdr .l{font-size:11px;color:var(--tinta2);text-transform:uppercase;letter-spacing:.05em}
|
||||
.hdr .v{font-weight:700;font-size:15px}
|
||||
.resu{font-weight:800}
|
||||
.resu.OK{color:var(--ok)} .resu.FALLA{color:var(--falla)}
|
||||
.sec{margin-top:14px} .sec h3{font-size:14px;margin:0 0 4px;color:var(--naranja)}
|
||||
.row{display:flex;gap:10px;padding:5px 0;border-top:1px solid var(--linea);font-size:14px}
|
||||
.row .a{flex:0 0 auto;font-weight:700}
|
||||
.row .t{flex:1}
|
||||
.si{color:var(--ok);font-weight:700} .no{color:var(--tinta2)}
|
||||
@media print{.barra,.top .noprint,button{display:none} body{background:#fff}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="top"><h1 id="titulo">Acta</h1><p id="sub">Cargando…</p></header>
|
||||
<div style="display:flex;justify-content:space-between;margin:2px 0 10px">
|
||||
<a class="volver" href="../tablero.html">← Tablero</a>
|
||||
<a class="volver noprint" href="#" onclick="window.print();return false">Imprimir / PDF</a>
|
||||
</div>
|
||||
<div id="cont"></div>
|
||||
</div>
|
||||
<script>
|
||||
const qs = new URLSearchParams(location.search);
|
||||
const tipo = qs.get('tipo') || '';
|
||||
const serie = qs.get('serie') || '';
|
||||
const cont = document.getElementById('cont');
|
||||
const TITULOS = { 'prueba-v5':'Acta de prueba V5', 'prueba-v4':'Acta de prueba V4', 'modulo-ard':'Acta de módulo ArduSimple', 'modulo-4g':'Acta de módulo 4G', 'integracion':'Acta de integración RTK + 4G', 'ensamble':'Acta de ensamble — unidad HBOX' };
|
||||
const COMP = { einar:'EinarBoard', ard:'simpleRTK2B Pro (ArduSimple)', modem:'4G NTRIP Master' };
|
||||
|
||||
function esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
|
||||
function fmt(iso){ return iso? iso.replace('T',' ').slice(0,16):''; }
|
||||
|
||||
function render(a){
|
||||
document.getElementById('titulo').textContent = (TITULOS[tipo]||'Acta') + ' — ' + esc(a.serie);
|
||||
document.getElementById('sub').textContent = 'Registro firmado · Helgen Technologies';
|
||||
let h = '<div class="doc"><div class="hdr">'
|
||||
+ '<div class="c"><div class="l">Serie</div><div class="v">'+esc(a.serie)+'</div></div>'
|
||||
+ '<div class="c"><div class="l">Técnico</div><div class="v">'+esc(a.tecnico||'—')+'</div></div>'
|
||||
+ '<div class="c"><div class="l">Resultado</div><div class="v resu '+esc(a.resultado)+'">'+esc(a.resultado||'—')+'</div></div>'
|
||||
+ '<div class="c"><div class="l">Fecha</div><div class="v">'+esc(fmt(a._recibido))+'</div></div>'
|
||||
+ '<div class="c"><div class="l">Usuario</div><div class="v">'+esc(a._usuario||'—')+'</div></div>'
|
||||
+ '</div>';
|
||||
if(a.componentes){
|
||||
h += '<div class="hdr">' + Object.keys(a.componentes).map(k=>'<div class="c"><div class="l">'+esc(COMP[k]||k)+'</div><div class="v">'+esc(a.componentes[k])+'</div></div>').join('') + '</div>';
|
||||
}
|
||||
// agrupar respuestas por sección (prefijo sN_)
|
||||
const R = a.respuestas || {};
|
||||
const keys = Object.keys(R).sort((x,y)=>{
|
||||
const px=x.match(/^s(\d+)_(\d+)/), py=y.match(/^s(\d+)_(\d+)/);
|
||||
if(px&&py){ return (+px[1]-+py[1]) || (+px[2]-+py[2]); } return x<y?-1:1;
|
||||
});
|
||||
let curSec=-1;
|
||||
keys.forEach(k=>{
|
||||
const m=k.match(/^s(\d+)_/); const sec=m?+m[1]:0;
|
||||
if(sec!==curSec){ if(curSec!==-1) h+='</div>'; h+='<div class="sec"><h3>Sección '+(sec+1)+'</h3>'; curSec=sec; }
|
||||
const it=R[k]; const val=it.val;
|
||||
let vista;
|
||||
if(val===true) vista='<span class="si">✓ Sí</span>';
|
||||
else if(val===false) vista='<span class="no">— No marcado</span>';
|
||||
else vista = val==='' ? '<span class="no">—</span>' : esc(val);
|
||||
h+='<div class="row"><div class="t">'+ (it.lbl||k) +'</div><div class="a">'+vista+'</div></div>';
|
||||
});
|
||||
if(curSec!==-1) h+='</div>';
|
||||
h += '<div class="sec"><div class="row"><div class="t">Conformidad del técnico</div><div class="a">'
|
||||
+ (a.conforme?'<span class="si">✓ Confirmado</span>':'<span class="no">—</span>') + '</div></div></div>';
|
||||
h += '</div>';
|
||||
cont.innerHTML = h;
|
||||
}
|
||||
|
||||
async function cargar(){
|
||||
if(!tipo || !serie){ cont.innerHTML='<div class="borrador">Falta indicar tipo y serie.</div>'; return; }
|
||||
try{
|
||||
const r = await fetch('/api/acta?tipo='+encodeURIComponent(tipo)+'&serie='+encodeURIComponent(serie));
|
||||
const j = await r.json();
|
||||
if(!j.ok) throw new Error(j.error||'error');
|
||||
if(!j.exists){ cont.innerHTML='<div class="borrador">No hay un acta registrada para <b>'+esc(serie)+'</b> todavía.</div>';
|
||||
document.getElementById('sub').textContent='Sin registro'; return; }
|
||||
render(j.acta);
|
||||
}catch(e){ cont.innerHTML='<div class="borrador">No se pudo cargar: '+esc(e.message)+'</div>'; }
|
||||
}
|
||||
cargar();
|
||||
</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.
|
||||
176
docs/procedimientos/v5-ensamble.md
Normal file
176
docs/procedimientos/v5-ensamble.md
Normal file
@ -0,0 +1,176 @@
|
||||
---
|
||||
title: HBOX v5 — Procedimiento de ensamble de la unidad y energizado final
|
||||
tags: [project-drogheda, hardware, hbox, v5, ensamble, checklist, procedimiento]
|
||||
project: Project Drogheda
|
||||
author: Helgen Technologies
|
||||
date: 2026-09-17
|
||||
version: 1.0
|
||||
status: ACTIVO — una unidad a la vez; se registra en el acta "Ensamble"
|
||||
audience: técnicos de banco
|
||||
---
|
||||
|
||||
# HBOX v5 — Ensamble de la unidad y energizado final
|
||||
|
||||
**Entrada:** tres módulos **ya aprobados** en sus actas — una EinarBoard **V5-NN** (acta de prueba y
|
||||
flasheo), un simpleRTK2B Pro **ARD-NNN** y un módem 4G NTRIP Master **RTK-NNNN** que pasaron juntos la
|
||||
**integración**. **Salida:** una unidad HBOX cerrada, etiquetada **HBOX5-Exx-Axxx-Rxxxx**, encendida y
|
||||
reportando.
|
||||
|
||||
**Duración estimada:** 60–90 min por unidad.
|
||||
|
||||
> ⛔ **REGLA 1 — Primero el acta, luego el desarmador.** Abrir el acta de ensamble, seleccionar los
|
||||
> tres módulos y esperar a que el sitio muestre los tres en **verde (Aprobado)** y la pareja ArduSimple+4G
|
||||
> con **integración OK**. Si alguno sale en rojo o "sin acta", **no se ensambla** con ese módulo.
|
||||
>
|
||||
> ⛔ **REGLA 2 — Batería al final.** El pack 1S4P se conecta en P4 solo en la sección 6, con la unidad
|
||||
> ya energizada desde 24 V y con el NTC del pack en P4-3.
|
||||
>
|
||||
> ⛔ **REGLA 3 — Todo módulo se inserta sin energía**, mirando la orientación dos veces. Un XBee al
|
||||
> revés o un simpleRTK2B corrido un pin se destruye al encender.
|
||||
|
||||
---
|
||||
|
||||
## 0. El número de serie de la unidad
|
||||
|
||||
El acta lo compone automáticamente al seleccionar los módulos:
|
||||
|
||||
```
|
||||
HBOX5 - E07 - A010 - R0006
|
||||
│ │ │ └── módem 4G NTRIP Master RTK-0006
|
||||
│ │ └────────── simpleRTK2B Pro (ArduSimple) ARD-010
|
||||
│ └──────────────── EinarBoard V5-07
|
||||
└──────────────────────── producto HBOX, tarjeta v5
|
||||
```
|
||||
|
||||
Ese texto va en la **etiqueta exterior** del HBOX (panel, junto al GX16) y en la **etiqueta interior**
|
||||
(cara interna de la tapa). Un módulo solo puede pertenecer a **una** unidad: el sitio rechaza el acta
|
||||
si el módulo ya está asignado.
|
||||
|
||||
---
|
||||
|
||||
## 1. Material y herramientas
|
||||
|
||||
| Ítem | Notas |
|
||||
|---|---|
|
||||
| Gabinete HBOX con panel perforado (GX16-5, 3 × SMA, RJ45, prensaestopas) | según el diseño mecánico vigente |
|
||||
| EinarBoard **V5-NN** aprobada, con firmware de bring-up **0.2.1 o superior** | ver acta de tarjeta |
|
||||
| simpleRTK2B Pro **ARD-NNN** y módem 4G NTRIP Master **RTK-NNNN** aprobados e integrados | con sus bolsas |
|
||||
| Fuente **Mean Well SD-15B-05** (24 V → 5 V) | va dentro del HBOX |
|
||||
| Pack de batería **1S4P** con NTC (3 hilos → P4) | conectar al final |
|
||||
| Ventilador 5 V 2 hilos (P6), LED de panel (P8), botón de reset (P10), interruptor de corte (P11), NTC de punto caliente (P12) | arnés interno |
|
||||
| Pigtails de antena: GNSS SMA-hembra bulkhead → SMA del simpleRTK2B; 2 × LTE SMA bulkhead → u.FL del módulo 4G | |
|
||||
| Separadores M2.5/M3, tornillería, bridas, fijador de rosca para SMA | |
|
||||
| Fuente de banco 24 V / 1 A con límite de corriente, multímetro, laptop con terminal serie | |
|
||||
| Etiquetas: exterior e interior con el número de unidad | impresas desde el acta |
|
||||
|
||||
---
|
||||
|
||||
## 2. Preparación de la tarjeta (sin energía)
|
||||
|
||||
- [ ] EinarBoard **V5-NN** corresponde a la seleccionada en el acta (leer la etiqueta cerca de MH4).
|
||||
- [ ] Jumpers del cargador en la posición del pack Li-ion 4.00 V: **JP2 (F2) = 1 · JP3 (F1) = 0 · JP4 (F0) = 1**.
|
||||
Anotar en el acta la posición de cada uno. *(Si el pack cambia a LiFePO4, ingeniería indicará JP2 = 0.)*
|
||||
- [ ] Jumper **JP1** (0R, alimentación del ventilador) presente.
|
||||
- [ ] **P11** (corte) **abierto** — sin jumper; el interruptor de panel se conecta ahí en la sección 4.
|
||||
- [ ] Tarjeta fijada en la platina/gabinete con separadores en **MH1–MH6** (y MH7–MH12 según el diseño) sin flexionar la tarjeta; tornillos con apriete suave.
|
||||
|
||||
## 3. Módulos enchufables (sin energía)
|
||||
|
||||
- [ ] **simpleRTK2B Pro ARD-NNN** en **MOD1**: alinear el pin **"5V"** del zócalo UNO con el pin 5V del
|
||||
módulo; las cuatro tiras entran a fondo y parejas; **ninguna** clavija queda fuera del zócalo.
|
||||
- [ ] **Módem 4G NTRIP Master RTK-NNNN** en **J7/J8** (zócalo XBee de 2 mm de la EinarBoard, **no** en el
|
||||
simpleRTK2B): la silueta del módulo coincide con la serigrafía; pin 1 (VCC) del lado marcado.
|
||||
- [ ] SIM presente en el módulo 4G (ya verificada en su acta).
|
||||
- [ ] Nada toca la tapa ni los separadores; los módulos no rozan componentes altos (K1, C10, L6).
|
||||
|
||||
## 4. Panel y arnés interno (sin energía)
|
||||
|
||||
Conectar contra el pinout; cada JST hace **clic**.
|
||||
|
||||
| Conector | Qué se conecta | Verificar |
|
||||
|---|---|---|
|
||||
| **P1** (GX16-5 del panel) | pin 1 = +24 V · pin 2 = GND · pin 3 = ignición · pin 4 = GND | polaridad con multímetro **antes** de conectar al panel |
|
||||
| **SD-15B-05** | entrada: +24 V/GND del GX16 (empalme en Y) · salida +5 V/GND → **P2** (pin 1 = +5 V) | ajustar salida a **5.0 V** en banco antes de montar |
|
||||
| **P6** | ventilador (pin 1 = +5V_FAN, pin 2 = retorno) | dirección de flujo: aire **hacia afuera** por la rejilla |
|
||||
| **P8** | LED de panel (pin 1 = ánodo, pin 2 = cátodo) | LED asoma por el panel |
|
||||
| **P10** | botón de reset externo | normalmente abierto |
|
||||
| **P11** | interruptor de corte del panel (2 hilos) | en posición **ON** cierra pin 1–2 |
|
||||
| **P12** | NTC de punto caliente pegado al módulo 4G o al disipador | con cinta térmica |
|
||||
| **P4** | **NO conectar todavía** (batería, sección 6) | — |
|
||||
| Antena **GNSS** | bulkhead SMA del panel → SMA del simpleRTK2B | fijador de rosca, apriete a mano + ¼ vuelta |
|
||||
| Antenas **LTE (2)** | bulkheads SMA del panel → u.FL del módulo 4G | los u.FL hacen clic; ruta del cable sin dobleces < 10 mm |
|
||||
| **J10** RJ45 / **P15** RS-485 / **P16** RS-232 | solo si el panel de esta unidad los lleva | tapas ciegas en los que no |
|
||||
|
||||
- [ ] Ningún cable pasa sobre el módulo 4G ni sobre la antena; cables sujetos con bridas al chasis.
|
||||
- [ ] Sin conductores expuestos cerca de 24 V (P1, SD-15B).
|
||||
|
||||
## 5. Energizado desde 24 V (unidad abierta, sin batería)
|
||||
|
||||
1. Fuente de banco **24.0 V, límite 1.0 A**, salida apagada, conectada al **GX16** del panel (pin 1 = +, pin 2 = −).
|
||||
Interruptor de corte (P11) en **ON**. USB-C en **J11** conectado a la laptop, terminal serie a 115200.
|
||||
2. Encender la salida. En los primeros 10 s:
|
||||
|
||||
| Punto | Esperado |
|
||||
|---|---|
|
||||
| Corriente de la fuente | **< 150 mA** (etapa RTK aún apagada) |
|
||||
| **D18** verde | Encendido |
|
||||
| **D17** azul + LED de panel | Parpadean (firmware de bring-up corriendo) |
|
||||
| Consola | Encabezado `hbox-v5-bringup`, reporte con **12 PASS**, luego `STAT … v5=4.9–5.1` |
|
||||
| Ventilador | **Gira** (con 24 V presente el buck U2 se enciende solo) |
|
||||
|
||||
3. Anotar en el acta: `v5` (debe ser 4.8–5.1 V), `tmp102`, `sht40`.
|
||||
4. **Encender la etapa RTK desde la consola:** teclear **`P`** y confirmar con **`Y`**. El firmware sube
|
||||
`PWR_CTRL` y reporta `STACK ON`. En los siguientes 60 s:
|
||||
|
||||
| Punto | Esperado |
|
||||
|---|---|
|
||||
| Corriente de la fuente | sube a **250–600 mA** (pico corto al registrar el 4G) |
|
||||
| simpleRTK2B: LED POWER | Encendido |
|
||||
| 4G: LED de red | Buscando → registrado (< 2 min) |
|
||||
| Consola, comando **`n`** | Muestra las tramas `$GNGGA` de PA3: **`fix=1`** (GPS) al minuto, **`fix=4`** (RTK FIX) en < 10 min con antena al cielo; `sats ≥ 10` |
|
||||
|
||||
5. Anotar en el acta el **tiempo hasta `fix=4`** y el número de satélites.
|
||||
6. Teclear **`P`** de nuevo para **apagar** la etapa RTK. La corriente debe volver a < 150 mA.
|
||||
|
||||
> Si `n` no muestra tramas: el simpleRTK2B no está a 115200 en UART1 (volver al acta del módulo RTK:
|
||||
> configuración Helgen) o MOD1 está corrido un pin. **Apagar antes de tocar nada.**
|
||||
|
||||
## 6. Batería y respaldo
|
||||
|
||||
1. Con la unidad encendida desde 24 V y la etapa RTK **apagada**: conectar el pack en **P4**
|
||||
(pin 1 = BAT+, pin 2 = GND, pin 3 = NTC). Medir antes con multímetro: **3.0–4.2 V** entre BAT+ y GND.
|
||||
2. En la consola, `STAT` debe mostrar **`vbat`** con el voltaje del pack y **`chrg`**: `0` = cargando
|
||||
(el firmware de bring-up mantiene la carga **deshabilitada**, así que `chrg=1` es lo esperado aquí).
|
||||
3. **Prueba de respaldo:** apagar la salida de la fuente de 24 V.
|
||||
|
||||
| Punto | Esperado |
|
||||
|---|---|
|
||||
| D18, D17 y consola | **Siguen vivos** (el LTC4040 conmuta a batería sin reinicio: el encabezado **no** se vuelve a imprimir) |
|
||||
| `STAT` | `v5=0.0`, `vbat` con el voltaje del pack |
|
||||
| Ventilador | Se detiene (no hay 24 V) — correcto |
|
||||
|
||||
4. Volver a encender 24 V: `STAT` vuelve a `v5≈5.0`. Sin reinicio.
|
||||
5. Anotar: voltaje del pack, "respaldo OK sin reinicio".
|
||||
|
||||
## 7. Cierre, etiquetado y registro
|
||||
|
||||
- [ ] Etiqueta **exterior** `HBOX5-Exx-Axxx-Rxxxx` en el panel, junto al GX16. Etiqueta **interior** en la tapa.
|
||||
- [ ] Interruptor de corte en **OFF** para almacenar (evita descargar la batería). Anotarlo en la etiqueta interior.
|
||||
- [ ] Tapa cerrada con su sello; tornillos en cruz; prensaestopas apretados.
|
||||
- [ ] Acta de ensamble enviada con resultado **APROBADA**.
|
||||
|
||||
---
|
||||
|
||||
## Anexo A — Notas de ingeniería
|
||||
|
||||
- **Firmware:** el bring-up **0.2.0** añade el modo de banco (`P` = stack ON/OFF con confirmación,
|
||||
`n` = monitor NMEA de PA3, 20 s). Mantiene la carga deshabilitada (`CHRG_EN` alto) y K1 liberado.
|
||||
La unidad ensamblada queda con este firmware hasta que exista el firmware de producción v5
|
||||
(rediseño de gestión de batería); se re-flashea por J11 sin abrir el HBOX si J11 queda accesible —
|
||||
**decidir si J11 asoma por el panel o se deja interno.**
|
||||
- **Corriente en 24 V:** valores orientativos; los reales se fijan con las primeras tres unidades y
|
||||
se actualizan aquí (bring-up 0.2.0 imprime `v5` pero no mide corriente).
|
||||
- **Respaldo sin reinicio** es la prueba del LTC4040 + L6/SW_BAT en la tarjeta real (el EMC audit
|
||||
marcó `/SW_BAT` 0.3 mm como crítico): si alguna unidad reinicia al cortar 24 V, anotar `vbat` y
|
||||
temperatura — es evidencia para el respin.
|
||||
- **Modo B / relevador K1:** no se prueba aquí; K1 queda liberado (camino directo F9P ↔ módem).
|
||||
211
docs/procedimientos/v5-modulos-integracion.md
Normal file
211
docs/procedimientos/v5-modulos-integracion.md
Normal file
@ -0,0 +1,211 @@
|
||||
---
|
||||
title: HBOX v5 — Prueba de módulos (simpleRTK2B Pro y 4G NTRIP Master) e integración RTK antes del ensamble
|
||||
tags: [project-drogheda, hardware, hbox, v5, ardusimple, 4g, ntrip, testing, procedimiento]
|
||||
project: Project Drogheda
|
||||
author: Helgen Technologies
|
||||
date: 2026-09-17
|
||||
version: 1.0
|
||||
status: ACTIVO — se aplica a cada módulo antes de asignarlo a una unidad
|
||||
audience: técnicos de banco
|
||||
---
|
||||
|
||||
# HBOX v5 — Prueba de módulos e integración RTK (antes del ensamble)
|
||||
|
||||
**Objetivo:** que ningún módulo llegue al ensamble sin haber demostrado que funciona **solo** y **en
|
||||
pareja**: cada simpleRTK2B Pro obtiene fix GNSS, cada 4G NTRIP Master se registra en la red, y la
|
||||
pareja RTK + 4G alcanza **RTK FIX** con correcciones NTRIP. Todo queda registrado en las actas
|
||||
del sitio (`https://drogheda.helgen.online` → Actas).
|
||||
|
||||
**Duración estimada:** módulo RTK 10 min · módulo 4G 10 min · integración 15–30 min (depende del cielo).
|
||||
|
||||
> ⛔ **REGLA 1 — Un acta por módulo, con su número de serie.** No se prueba "un lote"; se prueba
|
||||
> RTK-010, 4G-006, uno por uno.
|
||||
>
|
||||
> ⛔ **REGLA 2 — Sin EinarBoard en estas pruebas.** El simpleRTK2B se alimenta por su propio USB y el
|
||||
> módulo 4G va en el zócalo XBee del simpleRTK2B. La tarjeta EinarBoard entra hasta el ensamble.
|
||||
>
|
||||
> ⛔ **REGLA 3 — Antenas siempre conectadas antes de energizar** el módulo 4G y el receptor GNSS.
|
||||
> Un transmisor LTE sin antena se puede dañar.
|
||||
|
||||
---
|
||||
|
||||
## 0. Identidad de cada pieza (números de serie)
|
||||
|
||||
| Pieza | Etiqueta | Rango | Dónde va la etiqueta |
|
||||
|---|---|---|---|
|
||||
| EinarBoard v5 | **V5-01 … V5-15** (30 seleccionables) | lote JLC 22-Ago | esquina cerca de MH4 (ya puesta en la prueba de tarjeta) |
|
||||
| simpleRTK2B Pro (ArduSimple) | **ARD-001 … ARD-030** | consecutivo de 3 dígitos | sobre el blindaje del ZED-F9P, sin tapar el conector SMA ni el zócalo XBee |
|
||||
| 4G NTRIP Master (módem) | **RTK-0001 … RTK-0030** | consecutivo de 4 dígitos (serie hasta RTK-1000) | sobre el blindaje del módem, sin tapar los conectores u.FL de antena |
|
||||
| Unidad HBOX (ensamblada) | **HBOX5-E07-A010-R0006** | se calcula sola en el acta de ensamble | panel exterior del HBOX |
|
||||
|
||||
El número de la unidad **se arma con los tres módulos que lleva dentro**: `E07` = EinarBoard V5-07,
|
||||
`A010` = ArduSimple ARD-010, `R0006` = módem 4G RTK-0006. **Ojo:** la etiqueta "RTK-" es la del **módem 4G**, no la del receptor GNSS. No hace falta que los números coincidan entre sí: el técnico
|
||||
**selecciona** en el acta de ensamble qué módulo (ya aprobado) va en qué unidad, y el sitio compone el
|
||||
número. Así, con solo leer la etiqueta del HBOX se sabe qué hay dentro.
|
||||
|
||||
Anota también en cada acta la **etiqueta de fábrica** del módulo (el código que trae impreso
|
||||
ArduSimple) y, en el 4G, el **IMEI** y el **ICCID de la SIM**. Es lo que después permite rastrear un
|
||||
módulo en la plataforma.
|
||||
|
||||
---
|
||||
|
||||
## 1. Material
|
||||
|
||||
| Cant. | Ítem |
|
||||
|---|---|
|
||||
| 1 | Laptop con **u-center** (u-blox, Windows) o **u-center 2** |
|
||||
| 2 | Cables USB-C **con datos** (el simpleRTK2B Pro tiene dos puertos USB: **POWER+GPS** y **XBEE**) |
|
||||
| 1 | Antena GNSS multibanda (SMA) con vista al cielo (ventana con cielo abierto o exterior) |
|
||||
| 2 | Antenas LTE (u.FL → SMA pigtail según el módulo) |
|
||||
| 1 | SIM Telcel activa (nano-SIM) por módulo 4G |
|
||||
| 1 | Archivo de configuración del **ZED-F9P** de Helgen (u-center `.txt`) — lo entrega ingeniería |
|
||||
| 1 | Archivo de configuración del **4G NTRIP Master** con credenciales reales (`4G_NTRIP_HelgenSRV-NMEA.local.txt`) — lo entrega ingeniería, **no** se copia a otras máquinas |
|
||||
| — | Etiquetas ARD-NNN (ArduSimple) y RTK-NNNN (módem 4G), marcador, bolsas antiestáticas |
|
||||
|
||||
---
|
||||
|
||||
## 2. Módulo simpleRTK2B Pro (ArduSimple) — acta **"Módulo ArduSimple"**
|
||||
|
||||
### 2a. Inspección visual (sin energía)
|
||||
|
||||
- [ ] Etiqueta **ARD-NNN** puesta y legible; anotar también la etiqueta de fábrica.
|
||||
- [ ] Pines del zócalo **XBee** (2 hileras de 10) rectos; conector **SMA** firme; sin componentes golpeados.
|
||||
- [ ] Pines de la tira tipo Arduino (los que entran en MOD1 de la EinarBoard) rectos y completos.
|
||||
|
||||
### 2b. Energizado y fix GNSS
|
||||
|
||||
1. Conectar la **antena GNSS** al SMA. Colocarla con vista al cielo.
|
||||
2. Conectar el USB-C **POWER+GPS** a la laptop.
|
||||
3. Verificar:
|
||||
|
||||
| Punto | Esperado |
|
||||
|---|---|
|
||||
| LED **POWER** | Encendido |
|
||||
| Laptop | Aparece un puerto serie (u-blox GNSS receiver) |
|
||||
| u-center → Receiver → Connect | Conecta; **Fix Mode** pasa de *No Fix* a **3D** en < 2 min a cielo abierto |
|
||||
| Satélites en uso | **≥ 10** (cielo abierto) |
|
||||
| LED **GPS FIX** | Parpadea cuando hay fix |
|
||||
|
||||
4. Anotar en el acta: **versión de firmware** del ZED-F9P (u-center → View → Messages → UBX-MON-VER),
|
||||
satélites en uso, y el tiempo hasta el primer fix.
|
||||
|
||||
### 2c. Cargar la configuración de Helgen
|
||||
|
||||
1. u-center → **Tools → Receiver Configuration** → *Load configuration* → archivo F9P de Helgen →
|
||||
**Transfer file → GNSS**.
|
||||
2. Guardar en el receptor: **UBX-CFG-CFG → Save current configuration** (todas las memorias) → Send.
|
||||
3. Desconectar y reconectar el USB. Volver a conectar en u-center a la **velocidad configurada por Helgen
|
||||
(115200)**. Debe seguir mostrando fix y mensajes `$GNGGA`.
|
||||
|
||||
- [ ] Configuración cargada y guardada; sobrevive al reinicio.
|
||||
|
||||
> Si el módulo no da fix en 5 min a cielo abierto, probar con otra antena antes de marcarlo FALLA.
|
||||
|
||||
---
|
||||
|
||||
## 3. Módem 4G NTRIP Master — acta **"Módem 4G"**
|
||||
|
||||
### 3a. Inspección visual (sin energía)
|
||||
|
||||
- [ ] Etiqueta **RTK-NNNN** puesta; anotar **IMEI** (etiqueta del módem) e **ICCID** (tarjeta SIM).
|
||||
- [ ] **SIM insertada** en la dirección correcta, con clic.
|
||||
- [ ] Pines XBee (2 × 10, paso 2 mm) rectos; conectores **u.FL** de antena sin daño.
|
||||
|
||||
### 3b. Montaje sobre el simpleRTK2B y configuración
|
||||
|
||||
1. **Sin energía**, insertar el módulo 4G en el zócalo **XBee del simpleRTK2B Pro** (el módulo queda
|
||||
sobre el receptor; el corte de la silueta indica la orientación — nunca forzar).
|
||||
2. Conectar las **2 antenas LTE** a los u.FL del módulo.
|
||||
3. Conectar la antena GNSS. Conectar el USB **POWER+GPS** (alimenta todo) **y** el USB **XBEE** (puerto de
|
||||
configuración del módem).
|
||||
4. En la laptop abrir un terminal serie en el puerto **XBEE** a **115200 baud** y enviar, línea por línea,
|
||||
el archivo `4G_NTRIP_HelgenSRV-NMEA.local.txt` (solo las líneas `$$$AS+…`). Cada línea debe contestar
|
||||
`OK` (o el eco de ArduSimple).
|
||||
5. Al terminar: `$$$AS+I=1111` para entrar a config y **`$$$AS+K=…`** debe llevar el **ID de estación**
|
||||
que indique ingeniería para este módulo (se anota en el acta).
|
||||
|
||||
- [ ] Configuración enviada completa, sin líneas con `ERROR`.
|
||||
- [ ] ID de estación (`AS+K`) anotado en el acta.
|
||||
|
||||
### 3c. Registro en la red
|
||||
|
||||
Esperar hasta 2 min con las antenas LTE conectadas:
|
||||
|
||||
| Punto | Esperado |
|
||||
|---|---|
|
||||
| LED de **red / 4G** del módulo | Pasa de parpadeo rápido (buscando) a **parpadeo lento o fijo** (registrado) |
|
||||
| Consulta de estado por el puerto XBEE (comando de estado de ArduSimple) | Reporta operador **Telcel** y señal (RSSI) |
|
||||
|
||||
- [ ] Registrado en la red Telcel. Anotar RSSI si el módulo lo reporta.
|
||||
|
||||
> Sin registro en 3 min: revisar que la SIM esté activa (probar la SIM en otro módulo aprobado). Si
|
||||
> la SIM funciona en otro módulo y este no registra → **FALLA (4G)**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Integración ArduSimple + 4G — acta **"Integración"**
|
||||
|
||||
Se hace con la **pareja** que después irá junta en una unidad (ARD-NNN + RTK-NNNN). El acta registra
|
||||
los dos números; el mismo par se seleccionará en el ensamble.
|
||||
|
||||
Montaje: el mismo del punto 3b (4G sobre el simpleRTK2B, dos antenas LTE, antena GNSS al cielo, USB
|
||||
POWER+GPS conectado; el USB XBEE puede quedar conectado para ver mensajes).
|
||||
|
||||
1. u-center conectado al puerto **POWER+GPS** (115200). Esperar fix **3D**.
|
||||
2. Observar el LED **NO RTK** del simpleRTK2B: encendido = sin correcciones; **parpadeando = RTK FLOAT**;
|
||||
**apagado = RTK FIX**.
|
||||
3. En u-center: **Fix Mode** debe llegar a **3D/DGNSS/FIXED** (RTK fixed). Tiempo típico a cielo abierto:
|
||||
1–5 min tras el registro 4G.
|
||||
|
||||
| Punto | Esperado | Anotar |
|
||||
|---|---|---|
|
||||
| Tiempo desde encendido hasta **RTK FIX** | < 10 min | minutos |
|
||||
| Fix Mode en u-center | 3D/DGNSS/**FIXED** | — |
|
||||
| Satélites en uso | ≥ 10 | número |
|
||||
| Precisión estimada (u-center, hAcc) | < 0.05 m en FIX | metros |
|
||||
| Estabilidad | Se mantiene FIXED **5 min** sin caer a FLOAT (con la antena quieta) | sí/no |
|
||||
| Plataforma Helgen | Ingeniería confirma que la estación (ID `AS+K`) aparece reportando en el servidor | sí/no |
|
||||
|
||||
- [ ] RTK FIX alcanzado y estable 5 min.
|
||||
- [ ] Confirmado en plataforma (o anotado "pendiente de confirmar" con hora).
|
||||
|
||||
> **Si llega a FLOAT pero nunca a FIX:** cielo insuficiente (probar exterior) o punto de montaje
|
||||
> incorrecto; no es falla del módulo si el módulo 4G ya registró y hay correcciones (LED NO RTK
|
||||
> parpadea). Si **nunca sale de "encendido"** (sin correcciones): revisar credenciales NTRIP con
|
||||
> ingeniería antes de marcar FALLA.
|
||||
|
||||
### 4b. Cierre
|
||||
|
||||
- [ ] Apagar (desconectar USB). Separar el módulo 4G del simpleRTK2B **con cuidado, sin doblar pines**.
|
||||
- [ ] Guardar cada módulo en su bolsa con su etiqueta. Anotar en la bolsa del ArduSimple: "pareja: RTK-NNNN".
|
||||
- [ ] Enviar el acta de integración con los dos números.
|
||||
|
||||
---
|
||||
|
||||
## 5. Hoja rápida de estados (para el técnico)
|
||||
|
||||
| Módulo | Aprobado cuando… |
|
||||
|---|---|
|
||||
| ARD-NNN | fix 3D, ≥10 sat, configuración Helgen cargada y guardada |
|
||||
| RTK-NNNN (módem) | SIM registrada en Telcel, configuración enviada sin errores, `AS+K` anotado |
|
||||
| Pareja ArduSimple + 4G | RTK FIX estable 5 min con correcciones por 4G |
|
||||
|
||||
Un módulo con **FALLA** no se asigna a ninguna unidad: el acta de ensamble no lo dejará seleccionar.
|
||||
|
||||
---
|
||||
|
||||
## Anexo A — Notas de ingeniería
|
||||
|
||||
- En la **unidad v5** el módulo 4G **no** va sobre el simpleRTK2B: va en el zócalo **J7/J8** de la
|
||||
EinarBoard (mismo footprint XBee). El enlace F9P ↔ módem pasa por el relevador **K1**; con K1
|
||||
liberado (estado por defecto del firmware, `BYP_CTRL` bajo) el camino es directo. Por eso la
|
||||
prueba de integración se hace con el módulo montado en el simpleRTK2B (misma electrónica, sin
|
||||
depender del firmware de la EinarBoard).
|
||||
- Configuración F9P: UART1 a **115200** (es lo que lee la EinarBoard en PA3/PA2) y UART2 a la
|
||||
velocidad del módem (`AS+B=115200`). El archivo `.txt` del F9P **no está en el repo** — hay que
|
||||
añadirlo a `04-Source-Code/rtk-config/` (sin secretos) para que este procedimiento sea reproducible.
|
||||
- El ID de estación `AS+K` es la identidad de la unidad en el servidor secundario (`AS+r/AS+h`).
|
||||
Decisión pendiente: `AS+K` = número del módem 4G (R0006) o número de unidad. Mientras se decide,
|
||||
el acta lo registra como texto libre.
|
||||
- Las credenciales (APN, NTRIP) viven solo en `…local.txt` en la laptop de banco. Nunca en git ni en
|
||||
el sitio.
|
||||
@ -37,7 +37,7 @@ y dejarla flasheada con el firmware de arranque entregado por ingeniería.
|
||||
| 1 | Fuente de banco 0–30 V con límite de corriente ajustable |
|
||||
| 1 | Cable USB-C ↔ USB-C (o USB-A ↔ USB-C) **con datos** (no solo carga) |
|
||||
| 1 | Laptop con **STM32CubeProgrammer** instalado (o `dfu-util` en terminal) |
|
||||
| 1 | Archivo de firmware: **`hbox-v5-bringup_0.1.0.bin`** (repo: `04-Source-Code/firmware/hbox_v5_bringup/release/`, SHA-256 inicia `1a9a77ae`) |
|
||||
| 1 | Archivo de firmware: **`hbox-v5-bringup_0.2.1.bin`** — descarga: **https://drogheda.helgen.online/descargas/** (repo: `04-Source-Code/firmware/hbox_v5_bringup/release/`, SHA-256 inicia `78b34f46`) |
|
||||
| 1 | Jumper 2.54 mm para P11 (interruptor de corte) |
|
||||
| 1 | Lupa o lámpara con aumento |
|
||||
| — | Hoja de registro (sección 7) |
|
||||
@ -183,7 +183,7 @@ Solo para tarjetas que pasaron las secciones 1 a 4.
|
||||
2. Arriba a la derecha, en el selector de conexión elegir **USB**. Pulsar el botón de refrescar;
|
||||
debe aparecer `USB1`. Pulsar **Connect**.
|
||||
3. Ir a la pestaña **Erasing & Programming** (icono de flecha hacia abajo, lado izquierdo).
|
||||
4. **File path** → seleccionar `hbox-v5-bringup_0.1.0.bin`.
|
||||
4. **File path** → seleccionar `hbox-v5-bringup_0.2.1.bin`.
|
||||
5. **Start address** = `0x08000000`. Marcar **Verify programming** y **Run after programming**.
|
||||
6. Pulsar **Start Programming**. Esperar "File download complete" y "Verify… successful".
|
||||
7. Pulsar **Disconnect**.
|
||||
@ -192,7 +192,7 @@ Solo para tarjetas que pasaron las secciones 1 a 4.
|
||||
|
||||
```bash
|
||||
dfu-util -l # debe listar [0483:df11]
|
||||
dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.1.0.bin
|
||||
dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.2.1.bin
|
||||
```
|
||||
|
||||
### 5d. Verificación después del flasheo
|
||||
@ -206,7 +206,7 @@ dfu-util -a 0 -s 0x08000000:leave -D hbox-v5-bringup_0.1.0.bin
|
||||
| **D17** (LED azul) + LED de panel en P8 (si hay) | **Parpadean** a 1 Hz (MCU ejecutando el firmware) |
|
||||
| **DB1 → DB4** (LEDs del indicador de batería) | Se encienden **en secuencia** una vez, luego se apagan; después R, G, B en P14 si hay LED RGB conectado |
|
||||
| Laptop | Aparece un **puerto serie** nuevo ("STMicroelectronics Virtual COM Port" / `usbmodem…`) |
|
||||
| Terminal serie a **115200 baud** | Encabezado `hbox-v5-bringup 0.1.0`, luego líneas `[PASS]` / `[FAIL]`, y al final **`>>> BOARD OK`** |
|
||||
| Terminal serie a **115200 baud** | Encabezado `hbox-v5-bringup 0.2.1` (si dice 0.1.1 o 0.2.0, re-flashear), luego líneas `[PASS]` / `[FAIL]`, y al final **`>>> BOARD OK`** |
|
||||
|
||||
3. Si el terminal se abrió tarde y no se vio el reporte: teclear **`r`** en el terminal → repite
|
||||
todas las pruebas (y vuelve a encender los LEDs en secuencia).
|
||||
|
||||
40
docs/salir.html
Normal file
40
docs/salir.html
Normal file
@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Cerrar sesión — HBOX</title>
|
||||
<link rel="stylesheet" href="formularios/acta.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap" style="max-width:560px">
|
||||
<header class="top"><h1>Cerrar sesión</h1><p>Documentación técnica HBOX · Helgen</p></header>
|
||||
<section class="paso">
|
||||
<p id="estado">Cerrando sesión…</p>
|
||||
<div id="listo" style="display:none">
|
||||
<p><b>✓ Sesión cerrada.</b></p>
|
||||
<p>Para entrar con otro usuario, abre de nuevo el sitio y aparecerá la ventana de
|
||||
inicio de sesión.</p>
|
||||
<p style="color:var(--tinta2);font-size:14px">Si por seguridad del navegador la ventana
|
||||
no aparece, <b>cierra por completo esta pestaña</b> (o el navegador) para terminar la sesión.</p>
|
||||
<p style="margin-top:18px"><a class="volver" href="/">← Volver a la documentación</a></p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<script>
|
||||
// Envía credenciales inválidas a un endpoint que siempre responde 401, para que el
|
||||
// navegador reemplace las credenciales guardadas de Basic Auth.
|
||||
(function () {
|
||||
function done(){ document.getElementById('estado').style.display='none';
|
||||
document.getElementById('listo').style.display='block'; }
|
||||
try {
|
||||
var x = new XMLHttpRequest();
|
||||
x.open('GET', '/logout?ts=' + Date.now(), true, 'salir', 'salir' + Date.now());
|
||||
x.onloadend = done;
|
||||
x.send();
|
||||
} catch (e) { done(); }
|
||||
setTimeout(done, 1500);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
224
docs/tablero.html
Normal file
224
docs/tablero.html
Normal file
@ -0,0 +1,224 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Tablero de estado — HBOX</title>
|
||||
<style>
|
||||
:root{
|
||||
--naranja:#e8590c; --tinta:#1d1c19; --tinta2:#5a564d; --linea:#e6e1d6;
|
||||
--papel:#faf8f4; --tarjeta:#fff; --superf:#fcfcfb;
|
||||
--apro:#2f9e44; --apro-bg:#ebfbee; --falla:#e03131; --falla-bg:#fff5f5;
|
||||
--pend:#adb5bd; --pend-bg:#f4f2ee; --ens:#1971c2; --ens-bg:#e7f1fb;
|
||||
}
|
||||
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){
|
||||
--papel:#191b1e;--tinta:#e6e1d6;--tinta2:#a09a8d;--linea:#33363b;--tarjeta:#1f2226;--superf:#1b1d20;
|
||||
--apro:#69db7c;--apro-bg:#1b2a1f;--falla:#ff8787;--falla-bg:#2c1d1d;--pend:#5c636a;--pend-bg:#26282c;--ens:#74c0fc;--ens-bg:#182734;}}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--papel);color:var(--tinta);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
||||
.wrap{max-width:1000px;margin:0 auto;padding:18px 16px 70px}
|
||||
header.top{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;
|
||||
background:var(--tinta);color:#fff;padding:16px;border-radius:12px;margin-bottom:16px}
|
||||
header.top h1{margin:0;font-size:20px}
|
||||
header.top .meta{color:#d7d2c8;font-size:13px}
|
||||
button{background:var(--naranja);color:#fff;border:0;border-radius:9px;padding:9px 16px;font-size:14px;font-weight:600;cursor:pointer}
|
||||
a.volver{color:#fff;font-size:13px;text-decoration:none;opacity:.85}
|
||||
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:18px}
|
||||
.kpi{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:14px 16px}
|
||||
.kpi .n{font-size:30px;font-weight:700;line-height:1}
|
||||
.kpi .l{color:var(--tinta2);font-size:13px;margin-top:4px}
|
||||
section.bloque{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:16px;margin:14px 0}
|
||||
section.bloque h2{margin:0 0 2px;font-size:17px}
|
||||
.resumen{color:var(--tinta2);font-size:13.5px;margin:0 0 10px}
|
||||
.barra{display:flex;height:14px;border-radius:7px;overflow:hidden;background:var(--pend-bg);margin:6px 0 14px;gap:2px}
|
||||
.barra span{display:block;height:100%}
|
||||
.barra .a{background:var(--apro)} .barra .f{background:var(--falla)} .barra .e{background:var(--ens)}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:8px}
|
||||
.cel{display:flex;flex-direction:column;gap:2px;text-decoration:none;color:inherit;
|
||||
border:1px solid var(--linea);border-radius:10px;padding:9px 10px;background:var(--superf)}
|
||||
.cel .s{font-weight:700;font-size:14px}
|
||||
.cel .st{font-size:11.5px;font-weight:600;display:flex;align-items:center;gap:4px}
|
||||
.cel .who{font-size:11px;color:var(--tinta2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.cel.apro{background:var(--apro-bg);border-color:var(--apro)} .cel.apro .st{color:var(--apro)}
|
||||
.cel.falla{background:var(--falla-bg);border-color:var(--falla)} .cel.falla .st{color:var(--falla)}
|
||||
.cel.ens{background:var(--ens-bg);border-color:var(--ens)} .cel.ens .st{color:var(--ens)}
|
||||
.cel.pend .st{color:var(--tinta2)}
|
||||
.leyenda{display:flex;flex-wrap:wrap;gap:14px;font-size:12.5px;color:var(--tinta2);margin-top:10px}
|
||||
.leyenda b{font-weight:600}
|
||||
table{border-collapse:collapse;width:100%;font-size:13.5px;margin-top:6px}
|
||||
th{text-align:left;color:var(--tinta2);font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;padding:6px 8px 6px 0;border-bottom:1px solid var(--tinta2)}
|
||||
td{padding:7px 8px 7px 0;border-bottom:1px solid var(--linea)}
|
||||
.pill{font-size:11.5px;font-weight:700;padding:2px 8px;border-radius:99px}
|
||||
.pill.apro{background:var(--apro-bg);color:var(--apro)} .pill.falla{background:var(--falla-bg);color:var(--falla)}
|
||||
.dos{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||||
@media(max-width:680px){.dos{grid-template-columns:1fr}}
|
||||
.err{background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla);padding:10px 14px;border-radius:10px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="top">
|
||||
<div><h1>Tablero de estado — HBOX</h1><div class="meta" id="meta">Cargando…</div></div>
|
||||
<div style="display:flex;gap:12px;align-items:center">
|
||||
<a class="volver" href="./">← Documentación</a>
|
||||
<button id="refrescar">Actualizar</button>
|
||||
</div>
|
||||
</header>
|
||||
<div id="cont"></div>
|
||||
</div>
|
||||
<script>
|
||||
const STREAMS = [
|
||||
{ tipo:'prueba-v5', prefijo:'V5', desde:1, n:30, pad:2, titulo:'Tarjetas EinarBoard — V5', form:'formularios/v5.html' },
|
||||
{ tipo:'modulo-ard', prefijo:'ARD', desde:1, n:30, pad:3, titulo:'Módulos simpleRTK2B Pro — ARD', form:'formularios/ardusimple.html', usoClave:'ard' },
|
||||
{ tipo:'modulo-4g', prefijo:'RTK', desde:1, n:30, pad:4, titulo:'Módems 4G NTRIP Master — RTK', form:'formularios/modem4g.html', usoClave:'modem' },
|
||||
{ tipo:'prueba-v4', prefijo:'V4', desde:1, n:15, pad:2, titulo:'Tarjetas EinarBoard — V4 (borrador)', form:'formularios/v4.html' },
|
||||
];
|
||||
const TABLAS = [
|
||||
{ tipo:'integracion', titulo:'Integración ArduSimple + 4G (parejas)', form:'formularios/integracion.html', cols:[['ard','ArduSimple'],['modem','4G (RTK-)']] },
|
||||
{ tipo:'ensamble', titulo:'Unidades ensambladas — HBOX v5', form:'formularios/ensamble.html', cols:[['einar','EinarBoard'],['ard','ArduSimple'],['modem','4G (RTK-)']], unidad:true },
|
||||
];
|
||||
const ICON = { apro:'✓', falla:'✕', pend:'·', ens:'▣' };
|
||||
const cont = document.getElementById('cont');
|
||||
const meta = document.getElementById('meta');
|
||||
|
||||
function fmt(iso){ return iso ? iso.replace('T',' ').slice(0,16) : ''; }
|
||||
|
||||
function celda(st, sc){
|
||||
const cls = st.estado;
|
||||
const a = document.createElement('a');
|
||||
a.className = 'cel ' + cls;
|
||||
a.href = (cls==='pend') ? sc.form : ('formularios/ver.html?tipo='+encodeURIComponent(sc.tipo)+'&serie='+encodeURIComponent(st.serie));
|
||||
a.title = st.serie + (st.tecnico? ' — '+st.tecnico : '');
|
||||
const label = cls==='apro' ? 'Aprobada' : cls==='falla' ? 'Falla' : cls==='ens' ? 'Ensamblada' : 'Pendiente';
|
||||
a.innerHTML = '<span class="s">'+st.serie+'</span>'
|
||||
+ '<span class="st">'+ (ICON[cls]||'·') +' '+label+'</span>'
|
||||
+ '<span class="who">'+ (st.tecnico? st.tecnico : '—') +'</span>';
|
||||
return a;
|
||||
}
|
||||
|
||||
function bloque(sc, actas){
|
||||
const map = {};
|
||||
actas.filter(a=>a.tipo===sc.tipo).forEach(a=>{ map[a.serie]=a; });
|
||||
let apro=0, falla=0, pend=0, ens=0;
|
||||
const cells=[];
|
||||
const desde = sc.desde||1, pad = sc.pad||2;
|
||||
for(let i=desde;i<desde+sc.n;i++){
|
||||
const serie = sc.prefijo+'-'+String(i).padStart(pad,'0');
|
||||
const a = map[serie];
|
||||
let estado='pend', tecnico='';
|
||||
if(a){ tecnico=a.tecnico||'';
|
||||
estado = a.resultado==='FALLA' ? 'falla' : 'apro';
|
||||
// módulo aprobado y ya montado en una unidad → 'ens' (ensamblado)
|
||||
if(estado==='apro' && sc.usoClave && enUnidad[sc.usoClave] && enUnidad[sc.usoClave][serie]){ estado='ens'; tecnico='en '+enUnidad[sc.usoClave][serie]; }
|
||||
}
|
||||
if(estado==='apro')apro++; else if(estado==='falla')falla++; else if(estado==='ens')ens++; else pend++;
|
||||
cells.push(celda({serie,estado,tecnico,recibido:a?a.recibido:''}, sc));
|
||||
}
|
||||
const hechas = sc.n - pend;
|
||||
const sec = document.createElement('section'); sec.className='bloque';
|
||||
sec.innerHTML = '<h2>'+sc.titulo+'</h2>'
|
||||
+ '<p class="resumen">'+hechas+' de '+sc.n+' registradas · '+apro+' aprobadas'
|
||||
+ (ens ? ' · '+ens+' ya en unidad' : '') + ' · '+falla+' con falla · '+pend+' pendientes</p>';
|
||||
const barra = document.createElement('div'); barra.className='barra';
|
||||
const seg=(c,n)=>{ if(!n)return; const s=document.createElement('span'); s.className=c; s.style.flex=n; barra.append(s); };
|
||||
seg('a',apro); seg('e',ens); seg('f',falla);
|
||||
const rest=document.createElement('span'); rest.style.flex=pend; barra.append(rest);
|
||||
sec.append(barra);
|
||||
const grid=document.createElement('div'); grid.className='grid'; cells.forEach(c=>grid.append(c));
|
||||
sec.append(grid);
|
||||
return sec;
|
||||
}
|
||||
|
||||
let enUnidad = {}; // clave -> { serieModulo: serieUnidad }
|
||||
function tabla(tc, actas){
|
||||
const rows = actas.filter(a=>a.tipo===tc.tipo);
|
||||
const sec=document.createElement('section'); sec.className='bloque';
|
||||
const ok = rows.filter(a=>a.resultado!=='FALLA').length, bad = rows.length-ok;
|
||||
sec.innerHTML='<h2>'+tc.titulo+'</h2><p class="resumen">'+rows.length+' registradas · '+ok+' aprobadas · '+bad+' con falla · <a href="'+tc.form+'">+ nueva acta</a></p>';
|
||||
const t=document.createElement('table');
|
||||
t.innerHTML='<tr><th>'+(tc.unidad?'Unidad':'Pareja')+'</th>'+tc.cols.map(c=>'<th>'+c[1]+'</th>').join('')+'<th>Técnico</th><th>Resultado</th><th>Fecha</th></tr>';
|
||||
rows.forEach(a=>{
|
||||
const tr=document.createElement('tr');
|
||||
const p = a.resultado==='FALLA'?'<span class="pill falla">FALLA</span>':'<span class="pill apro">OK</span>';
|
||||
const link='formularios/ver.html?tipo='+encodeURIComponent(tc.tipo)+'&serie='+encodeURIComponent(a.serie||'');
|
||||
tr.innerHTML='<td><a href="'+link+'"><b>'+(a.serie||'')+'</b></a></td>'
|
||||
+ tc.cols.map(c=>'<td>'+((a.componentes||{})[c[0]]||'—')+'</td>').join('')
|
||||
+ '<td>'+(a.tecnico||'')+'</td><td>'+p+'</td><td>'+fmt(a.recibido)+'</td>';
|
||||
t.append(tr);
|
||||
});
|
||||
sec.append(t); if(!rows.length) sec.innerHTML+='<p class="resumen">Aún no hay registros.</p>';
|
||||
return sec;
|
||||
}
|
||||
function render(actas){
|
||||
cont.innerHTML='';
|
||||
// módulos ya montados en unidades (actas de ensamble reales)
|
||||
enUnidad = {};
|
||||
actas.filter(a=>a.tipo==='ensamble' && a.componentes && a.resultado!=='FALLA').forEach(a=>{
|
||||
for(const k in a.componentes){ enUnidad[k]=enUnidad[k]||{}; enUnidad[k][a.componentes[k]]=a.serie; }
|
||||
});
|
||||
// KPIs
|
||||
const v5 = actas.filter(a=>a.tipo==='prueba-v5');
|
||||
const rtk = actas.filter(a=>a.tipo==='modulo-ard'), g4 = actas.filter(a=>a.tipo==='modulo-4g');
|
||||
const integ = actas.filter(a=>a.tipo==='integracion');
|
||||
const ens = actas.filter(a=>a.tipo==='ensamble');
|
||||
const okN = l=>l.filter(a=>a.resultado!=='FALLA').length;
|
||||
const kpis=document.createElement('div'); kpis.className='kpis';
|
||||
const kpi=(n,l)=>{ const d=document.createElement('div'); d.className='kpi'; d.innerHTML='<div class="n">'+n+'</div><div class="l">'+l+'</div>'; return d; };
|
||||
kpis.append(kpi(okN(v5),'EinarBoard V5 aprobadas'));
|
||||
kpis.append(kpi(okN(rtk),'ArduSimple aprobados'));
|
||||
kpis.append(kpi(okN(g4),'Módems 4G aprobados'));
|
||||
kpis.append(kpi(okN(integ),'Parejas integradas'));
|
||||
kpis.append(kpi(okN(ens),'Unidades HBOX listas'));
|
||||
cont.append(kpis);
|
||||
// unidades e integración primero, luego inventario de módulos
|
||||
TABLAS.slice().reverse().forEach(tc=>cont.append(tabla(tc,actas)));
|
||||
STREAMS.forEach(sc=>cont.append(bloque(sc,actas)));
|
||||
// leyenda
|
||||
const ley=document.createElement('div'); ley.className='leyenda';
|
||||
ley.innerHTML='<span><b style="color:var(--apro)">✓</b> Aprobada</span>'
|
||||
+'<span><b style="color:var(--falla)">✕</b> Falla</span>'
|
||||
+'<span><b style="color:var(--ens)">▣</b> Ya montado en una unidad</span>'
|
||||
+'<span><b>·</b> Pendiente</span>'
|
||||
+'<span>· Toca una tarjeta para ver o llenar su acta.</span>';
|
||||
cont.append(ley);
|
||||
// actividad reciente + por técnico
|
||||
const dos=document.createElement('div'); dos.className='dos';
|
||||
const rec=document.createElement('section'); rec.className='bloque';
|
||||
rec.innerHTML='<h2>Actividad reciente</h2>';
|
||||
const tbl=document.createElement('table');
|
||||
tbl.innerHTML='<tr><th>Serie</th><th>Técnico</th><th>Resultado</th><th>Fecha</th></tr>';
|
||||
actas.slice(0,12).forEach(a=>{
|
||||
const tr=document.createElement('tr');
|
||||
const p = a.resultado==='FALLA'?'<span class="pill falla">FALLA</span>':'<span class="pill apro">OK</span>';
|
||||
tr.innerHTML='<td><b>'+(a.serie||'')+'</b></td><td>'+(a.tecnico||'')+'</td><td>'+p+'</td><td>'+fmt(a.recibido)+'</td>';
|
||||
tbl.append(tr);
|
||||
});
|
||||
rec.append(tbl); if(!actas.length) rec.innerHTML+='<p class="resumen">Aún no hay actas registradas.</p>';
|
||||
const pt=document.createElement('section'); pt.className='bloque';
|
||||
pt.innerHTML='<h2>Por técnico</h2>';
|
||||
const cnt={}; actas.forEach(a=>{ const t=(a.tecnico||'—').trim()||'—'; cnt[t]=(cnt[t]||0)+1; });
|
||||
const t2=document.createElement('table'); t2.innerHTML='<tr><th>Técnico</th><th>Actas</th></tr>';
|
||||
Object.entries(cnt).sort((x,y)=>y[1]-x[1]).forEach(([t,c])=>{ const tr=document.createElement('tr'); tr.innerHTML='<td>'+t+'</td><td>'+c+'</td>'; t2.append(tr); });
|
||||
pt.append(t2); if(!actas.length) pt.innerHTML+='<p class="resumen">—</p>';
|
||||
dos.append(rec,pt); cont.append(dos);
|
||||
}
|
||||
|
||||
async function cargar(){
|
||||
meta.textContent='Actualizando…';
|
||||
try{
|
||||
const r=await fetch('/api/actas',{cache:'no-store'});
|
||||
const j=await r.json();
|
||||
if(!j.ok) throw new Error(j.error||'error');
|
||||
render(j.actas||[]);
|
||||
meta.textContent='Actualizado '+new Date().toLocaleTimeString('es-MX',{hour:'2-digit',minute:'2-digit'})+' · '+(j.total||0)+' actas';
|
||||
}catch(e){
|
||||
cont.innerHTML='<div class="err">No se pudo cargar el tablero: '+e.message+'</div>';
|
||||
meta.textContent='Error';
|
||||
}
|
||||
}
|
||||
document.getElementById('refrescar').addEventListener('click',cargar);
|
||||
cargar();
|
||||
setInterval(cargar, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
18
mkdocs.yml
18
mkdocs.yml
@ -30,6 +30,7 @@ markdown_extensions:
|
||||
- admonition
|
||||
- tables
|
||||
- attr_list
|
||||
- md_in_html
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tasklist:
|
||||
@ -39,8 +40,19 @@ markdown_extensions:
|
||||
|
||||
nav:
|
||||
- Inicio: index.md
|
||||
- "Tablero de estado": tablero.html
|
||||
- Actas y control de calidad:
|
||||
- "Índice de actas": actas/index.md
|
||||
- "Acta de prueba V5 (tarjeta)": formularios/v5.html
|
||||
- "Acta de módulo ArduSimple (simpleRTK2B, ARD-NNN)": formularios/ardusimple.html
|
||||
- "Acta de módem 4G (NTRIP Master, RTK-NNNN)": formularios/modem4g.html
|
||||
- "Acta de integración ArduSimple + 4G": formularios/integracion.html
|
||||
- "Acta de ensamble (unidad HBOX v5)": formularios/ensamble.html
|
||||
- "Acta de prueba V4 (borrador)": formularios/v4.html
|
||||
- Procedimientos de banco:
|
||||
- "EinarBoard v5 — Prueba y flasheo (lote 15 pzas)": procedimientos/v5-prueba-y-flasheo.md
|
||||
- "HBOX v5 — Módulos RTK/4G e integración (antes del ensamble)": procedimientos/v5-modulos-integracion.md
|
||||
- "HBOX v5 — Ensamble de la unidad y energizado final": procedimientos/v5-ensamble.md
|
||||
- "EinarBoard 1.0 — Bring-up runbook": procedimientos/einarboard-1.0-bringup.md
|
||||
- "FW v3.0.3 — Prueba de banco (política de ventilador)": procedimientos/fan-policy-bench-test.md
|
||||
- "Histórico — Proto bench v3": procedimientos/historico-proto-bench-v3.md
|
||||
@ -48,9 +60,15 @@ 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
|
||||
- Git:
|
||||
- "Guía Git (español)": git/guia-git-es.md
|
||||
- "Git guide (English)": git/git-guide-en.md
|
||||
- Descargas:
|
||||
- "Firmware y herramientas": descargas/index.md
|
||||
- "README firmware bring-up v5": descargas/hbox-v5-bringup-README.md
|
||||
- "Administración (Leo)": admin.html
|
||||
- "Salir": salir.html
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Construye el sitio y lo sube al servidor de Helgen por rsync/ssh.
|
||||
# Uso: ./scripts/deploy.sh [usuario@servidor] [ruta-remota]
|
||||
# Requiere: pip install mkdocs-material y llave SSH hacia el servidor.
|
||||
# Requiere: pip install mkdocs-material y acceso SSH al servidor (puerto 22).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
REMOTE="${1:-git@git.helgen.online}"
|
||||
REMOTE_PATH="${2:-/var/www/hbox-docs}"
|
||||
PORT="${SSH_PORT:-222}"
|
||||
REMOTE="${1:-leo@git.helgen.online}"
|
||||
REMOTE_PATH="${2:-/srv/projects/drogheda/hbox-docs/site}"
|
||||
PORT="${SSH_PORT:-22}"
|
||||
|
||||
mkdocs build --strict
|
||||
rsync -az --delete -e "ssh -p $PORT" site/ "$REMOTE:$REMOTE_PATH/"
|
||||
|
||||
6
scripts/sync-docs.sh
Executable file → Normal file
6
scripts/sync-docs.sh
Executable file → Normal file
@ -8,6 +8,8 @@ SRC="${DROGHEDA_ROOT:-..}" # hbox-docs vive como submódulo directo de Droghed
|
||||
|
||||
declare -a MAP=(
|
||||
"06-Testing-Validation/2026-09-15_Drogheda_EinarBoard-v5_Procedimiento-Prueba-y-Flasheo_ES_v1.0.md|docs/procedimientos/v5-prueba-y-flasheo.md"
|
||||
"06-Testing-Validation/2026-09-17_Drogheda_HBOX-v5_Modulos-e-Integracion_ES_v1.0.md|docs/procedimientos/v5-modulos-integracion.md"
|
||||
"06-Testing-Validation/2026-09-17_Drogheda_HBOX-v5_Procedimiento-Ensamble_ES_v1.0.md|docs/procedimientos/v5-ensamble.md"
|
||||
"06-Testing-Validation/2026-07-08_Drogheda_EinarBoard-1.0_BringUp-Runbook_v1.0.md|docs/procedimientos/einarboard-1.0-bringup.md"
|
||||
"06-Testing-Validation/2026-08-18_Drogheda_FW-v3.0.3_Fan-Policy_Bench-Test_v1.0.md|docs/procedimientos/fan-policy-bench-test.md"
|
||||
"03-Architecture-Hardware/watchdog-proto-bench-bringup-v3.md|docs/procedimientos/historico-proto-bench-v3.md"
|
||||
@ -18,6 +20,10 @@ declare -a MAP=(
|
||||
"03-Architecture-Hardware/0007-1000_Assembly_Guide.pdf|docs/montaje/0007-1000_Assembly_Guide.pdf"
|
||||
"03-Architecture-Hardware/kicad/docs/GIT-GUIDE_Colaboradores-Externos_ES.md|docs/git/guia-git-es.md"
|
||||
"03-Architecture-Hardware/kicad/docs/GIT-GUIDE_External-Collaborators_EN.md|docs/git/git-guide-en.md"
|
||||
# Descargas para técnicos (firmware de bring-up v5)
|
||||
"04-Source-Code/firmware/hbox_v5_bringup/release/hbox-v5-bringup_0.2.1.bin|docs/descargas/hbox-v5-bringup_0.2.1.bin"
|
||||
"04-Source-Code/firmware/hbox_v5_bringup/release/SHA256SUMS|docs/descargas/SHA256SUMS.txt"
|
||||
"04-Source-Code/firmware/hbox_v5_bringup/README.md|docs/descargas/hbox-v5-bringup-README.md"
|
||||
)
|
||||
|
||||
n=0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user