diagnostico en campo y banco: actas con fotos y logs, reflasheo, portada e indice
This commit is contained in:
parent
455dbc4dc1
commit
86af91fe4b
@ -53,6 +53,7 @@ El backend la deduce sola de `ACTAS_DIR`; si prefieres fijarla, agrega a
|
|||||||
```ini
|
```ini
|
||||||
Environment=ADJUNTOS_DIR=/srv/projects/drogheda/adjuntos
|
Environment=ADJUNTOS_DIR=/srv/projects/drogheda/adjuntos
|
||||||
Environment=ADJUNTO_MAX_MB=8
|
Environment=ADJUNTO_MAX_MB=8
|
||||||
|
Environment=ARCHIVO_MAX_MB=20
|
||||||
Environment=ACTAS_REPETIBLES=diagnostico-campo,instalacion-campo
|
Environment=ACTAS_REPETIBLES=diagnostico-campo,instalacion-campo
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -66,14 +67,14 @@ En el bloque `server { listen 443 … }` de `drogheda.helgen.online`, el `locati
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Remote-User $remote_user; # ← quién es el técnico
|
proxy_set_header X-Remote-User $remote_user; # ← quién es el técnico
|
||||||
client_max_body_size 12m; # ← sin esto, las fotos dan 413
|
client_max_body_size 25m; # ← sin esto, las fotos dan 413
|
||||||
proxy_read_timeout 120s; # ← subidas con señal mala
|
proxy_read_timeout 120s; # ← subidas con señal mala
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Las **dos líneas marcadas son nuevas y obligatorias**:
|
Las **dos líneas marcadas son nuevas y obligatorias**:
|
||||||
|
|
||||||
- Sin `client_max_body_size`, nginx corta la foto en 1 MB y el técnico ve *"la foto pesa
|
- Sin `client_max_body_size`, nginx corta la subida en 1 MB y el técnico ve *"la foto pesa
|
||||||
demasiado"* aunque no lo pese. (El formulario ya reduce cada foto a ~1600 px / 300-400 kB
|
demasiado"* aunque no lo pese. (El formulario ya reduce cada foto a ~1600 px / 300-400 kB
|
||||||
antes de subirla; el margen es para ráfagas de varias fotos.)
|
antes de subirla; el margen es para ráfagas de varias fotos.)
|
||||||
- Sin `X-Remote-User`, el backend no sabe quién envió cada acta: el nombre no se precarga y —más
|
- Sin `X-Remote-User`, el backend no sabe quién envió cada acta: el nombre no se precarga y —más
|
||||||
|
|||||||
@ -7,6 +7,11 @@ User=leo
|
|||||||
Group=leo
|
Group=leo
|
||||||
WorkingDirectory=/srv/projects/drogheda/hbox-docs/backend
|
WorkingDirectory=/srv/projects/drogheda/hbox-docs/backend
|
||||||
Environment=ACTAS_DIR=/srv/projects/drogheda/actas
|
Environment=ACTAS_DIR=/srv/projects/drogheda/actas
|
||||||
|
Environment=ADJUNTOS_DIR=/srv/projects/drogheda/adjuntos
|
||||||
|
Environment=ADJUNTO_MAX_MB=8
|
||||||
|
Environment=ARCHIVO_MAX_MB=20
|
||||||
|
Environment=ACTAS_REPETIBLES=diagnostico-campo,instalacion-campo
|
||||||
|
Environment=ACTAS_ADMINS=leo
|
||||||
Environment=PORT=8081
|
Environment=PORT=8081
|
||||||
ExecStart=/srv/projects/drogheda/hbox-docs/backend/venv/bin/gunicorn -w 2 -b 127.0.0.1:8081 app:app
|
ExecStart=/srv/projects/drogheda/hbox-docs/backend/venv/bin/gunicorn -w 2 -b 127.0.0.1:8081 app:app
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|||||||
@ -40,8 +40,12 @@ TIPOS_REPETIBLES = {t.strip() for t in os.environ.get(
|
|||||||
|
|
||||||
# Subida de fotos
|
# Subida de fotos
|
||||||
MAX_MB = int(os.environ.get("ADJUNTO_MAX_MB", "8"))
|
MAX_MB = int(os.environ.get("ADJUNTO_MAX_MB", "8"))
|
||||||
app.config["MAX_CONTENT_LENGTH"] = (MAX_MB + 2) * 1024 * 1024
|
# Archivos de log (capturas por USB del Blackpill, del 4G y del ArduSimple).
|
||||||
|
LOG_MAX_MB = int(os.environ.get("ARCHIVO_MAX_MB", "20"))
|
||||||
|
app.config["MAX_CONTENT_LENGTH"] = (max(MAX_MB, LOG_MAX_MB) + 2) * 1024 * 1024
|
||||||
EXT_OK = {"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp"}
|
EXT_OK = {"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp"}
|
||||||
|
# Lista blanca de logs: texto plano y capturas binarias de u-blox. Nada ejecutable.
|
||||||
|
EXT_LOG = {".txt", ".log", ".csv", ".nmea", ".ubx", ".json", ".cfg", ".md"}
|
||||||
|
|
||||||
SAFE = re.compile(r"[^A-Za-z0-9_\-]")
|
SAFE = re.compile(r"[^A-Za-z0-9_\-]")
|
||||||
SAFE_FILE = re.compile(r"[^A-Za-z0-9_\-.]")
|
SAFE_FILE = re.compile(r"[^A-Za-z0-9_\-.]")
|
||||||
@ -172,6 +176,12 @@ def crear():
|
|||||||
if isinstance(f, dict) and isinstance(f.get("ruta"), str)
|
if isinstance(f, dict) and isinstance(f.get("ruta"), str)
|
||||||
and os.path.isfile(os.path.join(ADJ, f["ruta"]))]
|
and os.path.isfile(os.path.join(ADJ, f["ruta"]))]
|
||||||
|
|
||||||
|
archivos = d.get("archivos")
|
||||||
|
if isinstance(archivos, list):
|
||||||
|
d["archivos"] = [a for a in archivos[:40]
|
||||||
|
if isinstance(a, dict) and isinstance(a.get("ruta"), str)
|
||||||
|
and os.path.isfile(os.path.join(ADJ, a["ruta"]))]
|
||||||
|
|
||||||
ts = datetime.datetime.now().replace(microsecond=0)
|
ts = datetime.datetime.now().replace(microsecond=0)
|
||||||
iso = ts.isoformat()
|
iso = ts.isoformat()
|
||||||
d["_recibido"] = iso
|
d["_recibido"] = iso
|
||||||
@ -195,11 +205,12 @@ def crear():
|
|||||||
with open(idx, "a", newline="", encoding="utf-8") as f:
|
with open(idx, "a", newline="", encoding="utf-8") as f:
|
||||||
w = csv.writer(f)
|
w = csv.writer(f)
|
||||||
if nuevo:
|
if nuevo:
|
||||||
w.writerow(["recibido", "serie", "tecnico", "resultado", "clasificacion",
|
w.writerow(["recibido", "fecha", "serie", "maquina", "tecnico", "resultado",
|
||||||
"accion", "fotos", "archivo"])
|
"clasificacion", "accion", "fotos", "logs", "archivo"])
|
||||||
w.writerow([iso, serie, tecnico, d.get("resultado", ""),
|
w.writerow([iso, d.get("fecha", ""), serie, d.get("maquina_id", d.get("maquina", "")),
|
||||||
|
tecnico, d.get("resultado", ""),
|
||||||
d.get("clasificacion", ""), d.get("accion", ""),
|
d.get("clasificacion", ""), d.get("accion", ""),
|
||||||
len(d.get("fotos") or []), archivo])
|
len(d.get("fotos") or []), len(d.get("archivos") or []), archivo])
|
||||||
|
|
||||||
sub = f"{tipo}/_pruebas/{archivo}" if es_prueba else f"{tipo}/{archivo}"
|
sub = f"{tipo}/_pruebas/{archivo}" if es_prueba else f"{tipo}/{archivo}"
|
||||||
return jsonify(ok=True, archivo=sub, prueba=es_prueba)
|
return jsonify(ok=True, archivo=sub, prueba=es_prueba)
|
||||||
@ -282,6 +293,9 @@ def listar():
|
|||||||
for k in ("clasificacion", "accion", "motivo", "maquina", "terminal", "unidad"):
|
for k in ("clasificacion", "accion", "motivo", "maquina", "terminal", "unidad"):
|
||||||
if d.get(k):
|
if d.get(k):
|
||||||
fila[k] = d[k]
|
fila[k] = d[k]
|
||||||
|
if d.get("archivos"):
|
||||||
|
fila["archivos"] = [{"ruta": a.get("ruta"), "nombre": a.get("nombre")}
|
||||||
|
for a in d["archivos"] if a.get("ruta")]
|
||||||
if d.get("fotos"):
|
if d.get("fotos"):
|
||||||
fila["fotos"] = [f.get("ruta") for f in d["fotos"] if f.get("ruta")]
|
fila["fotos"] = [f.get("ruta") for f in d["fotos"] if f.get("ruta")]
|
||||||
fila["archivo_n"] = fn
|
fila["archivo_n"] = fn
|
||||||
@ -310,7 +324,9 @@ def historial():
|
|||||||
out.append({"archivo": fn, "recibido": d.get("_recibido"), "tecnico": d.get("tecnico"),
|
out.append({"archivo": fn, "recibido": d.get("_recibido"), "tecnico": d.get("tecnico"),
|
||||||
"resultado": d.get("resultado"), "clasificacion": d.get("clasificacion"),
|
"resultado": d.get("resultado"), "clasificacion": d.get("clasificacion"),
|
||||||
"accion": d.get("accion"), "motivo": d.get("motivo"),
|
"accion": d.get("accion"), "motivo": d.get("motivo"),
|
||||||
"fotos": len(d.get("fotos") or [])})
|
"fotos": len(d.get("fotos") or []),
|
||||||
|
"archivos": len(d.get("archivos") or []),
|
||||||
|
"fecha": d.get("fecha")})
|
||||||
out.reverse()
|
out.reverse()
|
||||||
return jsonify(ok=True, total=len(out), visitas=out)
|
return jsonify(ok=True, total=len(out), visitas=out)
|
||||||
|
|
||||||
@ -362,6 +378,46 @@ def subir_adjuntos():
|
|||||||
return jsonify(ok=True, fotos=guardadas, rechazadas=rechazadas)
|
return jsonify(ok=True, fotos=guardadas, rechazadas=rechazadas)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/archivos")
|
||||||
|
def subir_archivos():
|
||||||
|
"""Sube archivos de log de campo/banco. multipart: tipo, serie, archivo (1..10), etiqueta."""
|
||||||
|
tipo = slug(request.form.get("tipo")); serie = slug(request.form.get("serie"), 24)
|
||||||
|
etiqueta = str(request.form.get("etiqueta", ""))[:80]
|
||||||
|
if not tipo or not serie:
|
||||||
|
return jsonify(ok=False, error="faltan tipo/serie"), 400
|
||||||
|
subidos = request.files.getlist("archivo")
|
||||||
|
if not subidos:
|
||||||
|
return jsonify(ok=False, error="no llegó ningún archivo"), 400
|
||||||
|
if len(subidos) > 10:
|
||||||
|
return jsonify(ok=False, error="máximo 10 archivos por envío"), 400
|
||||||
|
|
||||||
|
carpeta = os.path.join(ADJ, tipo, serie, "logs")
|
||||||
|
os.makedirs(carpeta, exist_ok=True)
|
||||||
|
guardados, rechazados = [], []
|
||||||
|
for fh in subidos:
|
||||||
|
nombre = SAFE_FILE.sub("_", os.path.basename(fh.filename or "captura"))[:80]
|
||||||
|
ext = os.path.splitext(nombre)[1].lower()
|
||||||
|
if ext not in EXT_LOG:
|
||||||
|
rechazados.append({"nombre": nombre,
|
||||||
|
"error": "extensión no permitida (usa .txt .log .csv .nmea .ubx .json)"})
|
||||||
|
continue
|
||||||
|
datos = fh.read(LOG_MAX_MB * 1024 * 1024 + 1)
|
||||||
|
if len(datos) > LOG_MAX_MB * 1024 * 1024:
|
||||||
|
rechazados.append({"nombre": nombre, "error": f"pesa más de {LOG_MAX_MB} MB"}); continue
|
||||||
|
if not datos:
|
||||||
|
rechazados.append({"nombre": nombre, "error": "el archivo está vacío"}); continue
|
||||||
|
final = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + uuid.uuid4().hex[:4] + "_" + nombre
|
||||||
|
with open(os.path.join(carpeta, final), "wb") as f:
|
||||||
|
f.write(datos)
|
||||||
|
guardados.append({"ruta": f"{tipo}/{serie}/logs/{final}", "nombre": nombre,
|
||||||
|
"bytes": len(datos), "etiqueta": etiqueta, "usuario": current_user(),
|
||||||
|
"subida": datetime.datetime.now().replace(microsecond=0).isoformat()})
|
||||||
|
if not guardados:
|
||||||
|
return jsonify(ok=False, error=(rechazados[0]["error"] if rechazados else "no se guardó nada"),
|
||||||
|
rechazados=rechazados), 400
|
||||||
|
return jsonify(ok=True, archivos=guardados, rechazados=rechazados)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/adjunto")
|
@app.get("/api/adjunto")
|
||||||
def ver_adjunto():
|
def ver_adjunto():
|
||||||
ruta = (request.args.get("ruta") or "").strip().lstrip("/")
|
ruta = (request.args.get("ruta") or "").strip().lstrip("/")
|
||||||
@ -375,8 +431,8 @@ def ver_adjunto():
|
|||||||
|
|
||||||
@app.errorhandler(413)
|
@app.errorhandler(413)
|
||||||
def _muy_grande(e):
|
def _muy_grande(e):
|
||||||
return jsonify(ok=False, error=f"La foto pesa demasiado (máximo {MAX_MB} MB). "
|
return jsonify(ok=False, error=f"El archivo pesa demasiado (fotos: máximo {MAX_MB} MB; "
|
||||||
"Vuelve a tomarla o espera a tener mejor señal."), 413
|
f"logs: máximo {LOG_MAX_MB} MB)."), 413
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -1,229 +0,0 @@
|
|||||||
#!/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")))
|
|
||||||
91
backend/parche-nginx-api.py
Normal file
91
backend/parche-nginx-api.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Agrega al bloque `location /api/` de nginx las dos líneas que necesita el acta de campo:
|
||||||
|
|
||||||
|
proxy_set_header X-Remote-User $remote_user; -> identifica al técnico (y evita que
|
||||||
|
un usuario se haga pasar por otro)
|
||||||
|
client_max_body_size 25m; -> sin esto las fotos y los logs se cortan en 1 MB
|
||||||
|
|
||||||
|
Es idempotente: si ya están, no toca nada. Hace respaldo antes de escribir.
|
||||||
|
|
||||||
|
Uso (en el servidor):
|
||||||
|
sudo python3 parche-nginx-api.py # busca el archivo solo
|
||||||
|
sudo python3 parche-nginx-api.py /ruta/al/archivo.conf
|
||||||
|
Después:
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
"""
|
||||||
|
import sys, os, glob, datetime, re
|
||||||
|
|
||||||
|
LINEAS = [
|
||||||
|
('X-Remote-User', ' proxy_set_header X-Remote-User $remote_user;'),
|
||||||
|
('client_max_body_size', ' client_max_body_size 25m;'),
|
||||||
|
('proxy_read_timeout', ' proxy_read_timeout 120s;'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def candidatos():
|
||||||
|
vistos = []
|
||||||
|
for pat in ('/etc/nginx/sites-enabled/*', '/etc/nginx/sites-available/*', '/etc/nginx/conf.d/*'):
|
||||||
|
for f in glob.glob(pat):
|
||||||
|
if os.path.isfile(f) or os.path.islink(f):
|
||||||
|
real = os.path.realpath(f)
|
||||||
|
try:
|
||||||
|
if 'drogheda' in open(real, encoding='utf-8', errors='ignore').read() and real not in vistos:
|
||||||
|
vistos.append(real)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return vistos
|
||||||
|
|
||||||
|
def bloque_api(txt):
|
||||||
|
"""Devuelve (inicio_cuerpo, fin_bloque) del location /api/ o None."""
|
||||||
|
m = re.search(r'location\s+/api/\s*\{', txt)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
i = m.end() # justo después de la llave que abre
|
||||||
|
nivel, j = 1, i
|
||||||
|
while j < len(txt) and nivel:
|
||||||
|
if txt[j] == '{': nivel += 1
|
||||||
|
elif txt[j] == '}': nivel -= 1
|
||||||
|
j += 1
|
||||||
|
return (i, j)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
archivos = [sys.argv[1]] if len(sys.argv) > 1 else candidatos()
|
||||||
|
if not archivos:
|
||||||
|
sys.exit("No encontré ningún archivo de nginx que mencione 'drogheda'.\n"
|
||||||
|
"Pásalo a mano: sudo python3 parche-nginx-api.py /etc/nginx/sites-available/EL-ARCHIVO")
|
||||||
|
if len(archivos) > 1:
|
||||||
|
print("Hay varios candidatos; elige uno y vuelve a correr con la ruta:")
|
||||||
|
for f in archivos:
|
||||||
|
print(" ", f)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
ruta = archivos[0]
|
||||||
|
txt = open(ruta, encoding='utf-8').read()
|
||||||
|
pos = bloque_api(txt)
|
||||||
|
if not pos:
|
||||||
|
sys.exit(f"{ruta} no tiene un bloque `location /api/ {{ … }}`.\n"
|
||||||
|
"Agrégalo a mano ANTES del `location / {` (ver DESPLIEGUE.md §4).")
|
||||||
|
|
||||||
|
ini, fin = pos
|
||||||
|
cuerpo = txt[ini:fin]
|
||||||
|
faltan = [l for clave, l in LINEAS if clave not in cuerpo]
|
||||||
|
if not faltan:
|
||||||
|
print(f"✓ {ruta}: el bloque /api/ ya tiene todo. No cambié nada.")
|
||||||
|
print(txt[ini - len('location /api/ {'):fin])
|
||||||
|
return
|
||||||
|
|
||||||
|
respaldo = ruta + '.bak-' + datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
|
||||||
|
open(respaldo, 'w', encoding='utf-8').write(txt)
|
||||||
|
nuevo = txt[:ini] + '\n' + '\n'.join(faltan) + txt[ini:]
|
||||||
|
open(ruta, 'w', encoding='utf-8').write(nuevo)
|
||||||
|
|
||||||
|
print(f"Respaldo: {respaldo}")
|
||||||
|
print(f"Agregadas {len(faltan)} línea(s) a {ruta}:")
|
||||||
|
for l in faltan:
|
||||||
|
print(" " + l.strip())
|
||||||
|
pos2 = bloque_api(nuevo)
|
||||||
|
print("\nEl bloque quedó así:\n")
|
||||||
|
print(nuevo[pos2[0] - len('location /api/ {'):pos2[1]])
|
||||||
|
print("\nAhora: sudo nginx -t && sudo systemctl reload nginx")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
59
docs/descargas/4G_NTRIP_HelgenSRV-NMEA.example.txt
Normal file
59
docs/descargas/4G_NTRIP_HelgenSRV-NMEA.example.txt
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# ArduSimple simpleRTK2B + 4G NTRIP Master — configuration TEMPLATE (REDACTED)
|
||||||
|
# Project Drogheda / HBOX
|
||||||
|
#
|
||||||
|
# HOW TO USE
|
||||||
|
# 1. Copy this file to a LOCAL, git-ignored name:
|
||||||
|
# cp 4G_NTRIP_HelgenSRV-NMEA.example.txt 4G_NTRIP_HelgenSRV-NMEA.local.txt
|
||||||
|
# 2. Fill the <PLACEHOLDERS> with the real values for the deployment.
|
||||||
|
# 3. Send the $$$AS+ lines to the module per ArduSimple's config procedure.
|
||||||
|
# NEVER commit the filled-in file. Real APN/NTRIP credentials must not enter git
|
||||||
|
# (project invariant: no secrets in the repo).
|
||||||
|
#
|
||||||
|
# Lines starting with '#' are documentation — only the $$$AS+ lines are config.
|
||||||
|
# Key reference:
|
||||||
|
# AS+A APN host · AS+Q APN user · AS+q APN pass
|
||||||
|
# AS+S caster host · AS+P caster port · AS+U "user:pass" · AS+M mountpoint
|
||||||
|
# AS+B baud · AS+r/AS+h secondary server:port · AS+K station/mount id
|
||||||
|
# ============================================================================
|
||||||
|
$$$AS+I=1111
|
||||||
|
$$$AS+A=<APN_HOST>
|
||||||
|
$$$AS+Q=<APN_USER>
|
||||||
|
$$$AS+q=<APN_PASS>
|
||||||
|
$$$AS+O=0
|
||||||
|
$$$AS+L=1
|
||||||
|
$$$AS+D=1
|
||||||
|
$$$AS+f=0
|
||||||
|
$$$AS+B=115200
|
||||||
|
$$$AS+N=1
|
||||||
|
$$$AS+S=<NTRIP_CASTER_HOST>
|
||||||
|
$$$AS+P=<NTRIP_CASTER_PORT>
|
||||||
|
$$$AS+U=<NTRIP_USER>:<NTRIP_PASS>
|
||||||
|
$$$AS+M=<NTRIP_MOUNTPOINT>
|
||||||
|
$$$AS+n=0
|
||||||
|
$$$AS+s=
|
||||||
|
$$$AS+p=
|
||||||
|
$$$AS+u=
|
||||||
|
$$$AS+m=0
|
||||||
|
$$$AS+i=
|
||||||
|
$$$AS+a=0
|
||||||
|
$$$AS+Z=0
|
||||||
|
$$$AS+z=pp.services.u-blox.com
|
||||||
|
$$$AS+Y=
|
||||||
|
$$$AS+y=0
|
||||||
|
$$$AS+X=0
|
||||||
|
$$$AS+x=0
|
||||||
|
$$$AS+W=0
|
||||||
|
$$$AS+w=
|
||||||
|
$$$AS+V=
|
||||||
|
$$$AS+v=
|
||||||
|
$$$AS+t=
|
||||||
|
$$$AS+T=1
|
||||||
|
$$$AS+r=<SECONDARY_SERVER_IP>
|
||||||
|
$$$AS+h=<SECONDARY_SERVER_PORT>
|
||||||
|
$$$AS+k=2
|
||||||
|
$$$AS+K=<STATION_ID>
|
||||||
|
$$$AS+j=300
|
||||||
|
$$$AS+C=1
|
||||||
|
$$$AS+c=0
|
||||||
|
$$$AS+F=1
|
||||||
@ -41,6 +41,67 @@ o con **STM32CubeProgrammer** → USB → Connect → Download del `.bin` en `0x
|
|||||||
|
|
||||||
Notas de compilación y cambios: [README del firmware de bring-up](hbox-v5-bringup-README.md).
|
Notas de compilación y cambios: [README del firmware de bring-up](hbox-v5-bringup-README.md).
|
||||||
|
|
||||||
|
## Firmware de campo — tarjetas V3 y V4
|
||||||
|
|
||||||
|
Es el firmware que gobierna la unidad en la máquina (`blackpillrtkcode_v3` / `v4`), distinto del de
|
||||||
|
bring-up del v5. Se usa en el [reflasheo en taller](../procedimientos/reflasheo-unidades.md).
|
||||||
|
|
||||||
|
| Generación | Archivo | Estado |
|
||||||
|
|---|---|---|
|
||||||
|
| **V3** — flota desplegada (RTK-003, RTK-006, RTK-014) | `hbox-fw-v3_X.Y.Z.bin` | ⬜ **pendiente de publicar** |
|
||||||
|
| **V4** | `hbox-fw-v4_X.Y.Z.bin` | ⬜ **pendiente de publicar** |
|
||||||
|
|
||||||
|
!!! danger "Mientras diga «pendiente», no hay binario aprobado"
|
||||||
|
No flashees una unidad V3 o V4 con un archivo que no esté publicado aquí con su SHA-256. Los
|
||||||
|
binarios de estas generaciones **no viven en el repositorio** (están en `.gitignore`): tienen que
|
||||||
|
compilarse, probarse en banco y publicarse. El procedimiento de generación está en el
|
||||||
|
[reflasheo §6](../procedimientos/reflasheo-unidades.md).
|
||||||
|
|
||||||
|
!!! warning "Un binario no sirve para otra generación"
|
||||||
|
La polaridad del control de carga está **invertida** entre la tarjeta de producción y la de
|
||||||
|
protoboard. Flashear el binario equivocado puede dejar la batería cargando justo cuando el
|
||||||
|
firmware cree estarla protegiendo por temperatura — la falla #1 del RCA de junio.
|
||||||
|
|
||||||
|
## Parámetros del módem 4G NTRIP Master
|
||||||
|
|
||||||
|
La configuración se envía por el puerto USB **XBEE** del simpleRTK2B Pro a 115200 baudios, línea por
|
||||||
|
línea (solo las que empiezan con `$$$AS+`). Cada una debe contestar `OK`.
|
||||||
|
|
||||||
|
| Archivo | Qué es |
|
||||||
|
|---|---|
|
||||||
|
| [**4G_NTRIP_HelgenSRV-NMEA.example.txt**](4G_NTRIP_HelgenSRV-NMEA.example.txt) | **Plantilla** con todos los parámetros y sus nombres, con los valores sensibles en blanco |
|
||||||
|
|
||||||
|
!!! danger "El archivo con los valores reales no está aquí, y es a propósito"
|
||||||
|
El archivo lleno contiene **usuario y contraseña del caster NTRIP y de la APN**. Publicarlo en el
|
||||||
|
sitio sería ponerlo al alcance de cualquiera que tenga una cuenta de técnico, y meterlo al
|
||||||
|
repositorio rompe un invariante del proyecto (*sin secretos en el repo, nunca*).
|
||||||
|
|
||||||
|
**Cómo se obtiene:** pídelo a ingeniería para la unidad que vas a configurar. Llega por un canal
|
||||||
|
directo, se usa y no se guarda en el equipo ni se reenvía.
|
||||||
|
|
||||||
|
Referencia rápida de los parámetros que más se consultan:
|
||||||
|
|
||||||
|
| Comando | Qué configura |
|
||||||
|
|---|---|
|
||||||
|
| `AS+I=1111` | Entra a modo configuración |
|
||||||
|
| `AS+A` · `AS+Q` · `AS+q` | APN: host, usuario, contraseña |
|
||||||
|
| `AS+S` · `AS+P` | Caster NTRIP: host y puerto |
|
||||||
|
| `AS+U` · `AS+M` | Caster: `usuario:contraseña` y mountpoint |
|
||||||
|
| `AS+B` | Velocidad del puerto (115200) |
|
||||||
|
| `AS+K` | **ID de estación** del módulo — se anota en el acta |
|
||||||
|
| `AS+r` · `AS+h` | Servidor secundario: IP y puerto |
|
||||||
|
|
||||||
|
## Firmware y configuración del simpleRTK2B Pro (ZED-F9P)
|
||||||
|
|
||||||
|
| Archivo | Estado | Notas |
|
||||||
|
|---|---|---|
|
||||||
|
| Configuración del ZED-F9P (u-center `.txt`) | ⬜ **pendiente de publicar** | UART1 a 115200 (es lo que lee la tarjeta) y UART2 a la velocidad del módem |
|
||||||
|
| Firmware del ZED-F9P (u-blox) | Se descarga de u-blox | Anotar la versión instalada con **UBX-MON-VER** |
|
||||||
|
|
||||||
|
Para leer o cargar la configuración: u-center → *Tools → Receiver Configuration*. Después de cargar,
|
||||||
|
**guardar en FLASH** y reconectar para confirmar que sobrevive al reinicio — una configuración que
|
||||||
|
solo quedó en RAM se pierde al primer corte de energía, y así perdimos tiempo en junio.
|
||||||
|
|
||||||
## Herramientas
|
## Herramientas
|
||||||
|
|
||||||
- **STM32CubeProgrammer** (Windows/macOS/Linux): https://www.st.com/en/development-tools/stm32cubeprog.html
|
- **STM32CubeProgrammer** (Windows/macOS/Linux): https://www.st.com/en/development-tools/stm32cubeprog.html
|
||||||
|
|||||||
@ -25,7 +25,72 @@ estado en que estaba la unidad**. Esa es toda tu misión, y es más valiosa de l
|
|||||||
|
|
||||||
Por eso el procedimiento dice: **no cortes la energía hasta terminar el paso 5.**
|
Por eso el procedimiento dice: **no cortes la energía hasta terminar el paso 5.**
|
||||||
|
|
||||||
## 2. Qué llevar
|
## 2. Antes de ir — permiso de acceso y de qué máquina hablamos
|
||||||
|
|
||||||
|
Dos cosas tienen que estar resueltas **antes** de salir. Sin ellas la visita no se hace.
|
||||||
|
|
||||||
|
### 2.1 De qué máquina estamos hablando
|
||||||
|
|
||||||
|
En ICAVE nadie conoce el equipo por el número del HBOX: lo conocen por la **máquina**
|
||||||
|
(`FL-JUN-060`, `RT-ZPMC-20`, `HS-WES-62`). El permiso se pide por máquina, el operador te
|
||||||
|
lleva a una máquina y el acta se cruza por máquina.
|
||||||
|
|
||||||
|
Las tres unidades v3 desplegadas y en qué máquina va cada una:
|
||||||
|
|
||||||
|
| Máquina (como la nombra ICAVE) | Equipo | Unidad HBOX | Estado hoy |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `FL-JUN-060` | Montacargas | **RTK-014** | **Instalada.** Dejó de reportar el 17-sep. Nadie la ha tocado — evidencia intacta |
|
||||||
|
| `RT-ZPMC-20` | RTG | **RTK-003** | Desmontada, en el escritorio del taller |
|
||||||
|
| `HS-WES-62` | Tractoplana | **RTK-006** | Desmontada, en el escritorio del taller |
|
||||||
|
|
||||||
|
Solo `FL-JUN-060` necesita permiso de acceso: las otras dos ya están en el taller y se revisan en banco.
|
||||||
|
|
||||||
|
!!! warning "No confundir los números"
|
||||||
|
Estas unidades de campo son **`RTK-006`** (tres dígitos). Los módems 4G del HBOX v5 son
|
||||||
|
**`RTK-0006`** (cuatro dígitos) y viven en otra acta. Son cosas distintas.
|
||||||
|
|
||||||
|
Por eso, antes de pedir el acceso hay que **saber en qué máquina está montada la unidad** que
|
||||||
|
queremos revisar. Si el registro no lo dice o hay duda, se confirma con ICAVE en el mismo correo.
|
||||||
|
Y si al llegar la unidad resulta estar en **otra** máquina de la que dice la lista, eso se anota
|
||||||
|
en el acta: el registro estaba mal y hay que corregirlo.
|
||||||
|
|
||||||
|
### 2.2 El correo de solicitud de acceso
|
||||||
|
|
||||||
|
Lo manda **Operaciones (Adriana)** al contacto de ICAVE, con copia a Agustín Alfonseca,
|
||||||
|
Leon David Piña, Leo y los técnicos que van a subir. Modelo:
|
||||||
|
|
||||||
|
> **Asunto:** Solicitud de acceso a la unidad `FL-JUN-060` — revisión de equipo HBOX
|
||||||
|
>
|
||||||
|
> Estimado **[nombre]**:
|
||||||
|
>
|
||||||
|
> Buen día.
|
||||||
|
>
|
||||||
|
> Por medio del presente, solicito de tu apoyo para que se nos proporcione acceso a la unidad
|
||||||
|
> **`FL-JUN-060`**, con la finalidad de realizar una revisión del equipo HBOX instalado en dicha unidad.
|
||||||
|
>
|
||||||
|
> La actividad será llevada a cabo por los técnicos **Alán Basurto y Daniel Martínez**, quienes
|
||||||
|
> realizarán las verificaciones correspondientes para asegurar el correcto funcionamiento del sistema.
|
||||||
|
>
|
||||||
|
> Quedamos atentos a tus comentarios y al apoyo para coordinar el acceso en el horario que mejor
|
||||||
|
> se adapte a la operación.
|
||||||
|
>
|
||||||
|
> Muchas gracias. Saludos cordiales.
|
||||||
|
|
||||||
|
Lo único que cambia entre visitas es **el número de la unidad** y, si aplica, **los nombres de
|
||||||
|
los técnicos**. Una máquina por correo: mezclar dos en el mismo correo enreda la autorización.
|
||||||
|
|
||||||
|
### 2.3 La regla
|
||||||
|
|
||||||
|
| Situación | ¿Se sube a la máquina? |
|
||||||
|
|---|---|
|
||||||
|
| Correo contestado y horario acordado | **Sí**, con la máquina detenida y bloqueada (LOTO) |
|
||||||
|
| Autorización de palabra del supervisor en sitio | Sí, pero se anota quién autorizó y a qué hora |
|
||||||
|
| Sin respuesta al correo | **No.** Se revisa desde el piso lo que se alcance y se reagenda |
|
||||||
|
| La unidad ya está desmontada, en el taller | No aplica: no hay máquina de por medio |
|
||||||
|
|
||||||
|
Una revisión incompleta se repite la semana que entra. Un accidente en un patio de maniobras, no.
|
||||||
|
|
||||||
|
## 3. Qué llevar
|
||||||
|
|
||||||
| Herramienta | Para qué | ¿Obligatoria? |
|
| Herramienta | Para qué | ¿Obligatoria? |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@ -37,7 +102,7 @@ estado en que estaba la unidad**. Esa es toda tu misión, y es más valiosa de l
|
|||||||
| Bolsa antiestática | Transportar la unidad desmontada | Sí |
|
| Bolsa antiestática | Transportar la unidad desmontada | Sí |
|
||||||
| Termómetro infrarrojo | Medir la temperatura interior del gabinete | Deseable — cuesta poco y mide la hipótesis térmica |
|
| Termómetro infrarrojo | Medir la temperatura interior del gabinete | Deseable — cuesta poco y mide la hipótesis térmica |
|
||||||
|
|
||||||
## 3. Seguridad
|
## 4. Seguridad
|
||||||
|
|
||||||
1. **Nunca** trabajes sobre un RTG o equipo energizado sin el permiso y el bloqueo (LOTO) del
|
1. **Nunca** trabajes sobre un RTG o equipo energizado sin el permiso y el bloqueo (LOTO) del
|
||||||
terminal. Si el equipo está operando, **no subas**: agenda con el supervisor de ICAVE.
|
terminal. Si el equipo está operando, **no subas**: agenda con el supervisor de ICAVE.
|
||||||
@ -47,10 +112,11 @@ estado en que estaba la unidad**. Esa es toda tu misión, y es más valiosa de l
|
|||||||
4. El gabinete puede tener **24 V**: no es peligroso para la vida, pero un corto sí puede
|
4. El gabinete puede tener **24 V**: no es peligroso para la vida, pero un corto sí puede
|
||||||
quemar la tarjeta. Mide con la máquina apagada siempre que puedas.
|
quemar la tarjeta. Mide con la máquina apagada siempre que puedas.
|
||||||
|
|
||||||
## 4. Los pasos, en orden
|
## 5. Los pasos, en orden
|
||||||
|
|
||||||
| Paso | Qué haces | Por qué importa |
|
| Paso | Qué haces | Por qué importa |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| **0** | **Permiso de acceso a la máquina** y confirmar en cuál está montada | Sin autorización no hay visita; sin máquina no hay permiso |
|
||||||
| 1 | Anotas **por qué vienes** y qué dijo el operador | La hora en que dejó de reportar acota la causa |
|
| 1 | Anotas **por qué vienes** y qué dijo el operador | La hora en que dejó de reportar acota la causa |
|
||||||
| 2 | Fotos con el gabinete **cerrado**: máquina, gabinete, antenas | Sol, altura, antena floja o robada se ven aquí |
|
| 2 | Fotos con el gabinete **cerrado**: máquina, gabinete, antenas | Sol, altura, antena floja o robada se ven aquí |
|
||||||
| 3 | **Cuentas los destellos del LED** en 10 segundos | Es el diagnóstico gratis que la unidad te está dando |
|
| 3 | **Cuentas los destellos del LED** en 10 segundos | Es el diagnóstico gratis que la unidad te está dando |
|
||||||
@ -59,7 +125,7 @@ estado en que estaba la unidad**. Esa es toda tu misión, y es más valiosa de l
|
|||||||
| 6 | *Ahora sí* puedes cortar la energía y probar | Ya está toda la evidencia guardada |
|
| 6 | *Ahora sí* puedes cortar la energía y probar | Ya está toda la evidencia guardada |
|
||||||
| 7 | Clasificas, decides cómo queda y firmas | Convierte tu visita en un dato comparable |
|
| 7 | Clasificas, decides cómo queda y firmas | Convierte tu visita en un dato comparable |
|
||||||
|
|
||||||
## 5. Tabla del LED — qué te está diciendo la unidad
|
## 6. Tabla del LED — qué te está diciendo la unidad
|
||||||
|
|
||||||
Mira el LED **10 segundos con el reloj** y **cuenta cuántas veces prende**. No lo estimes:
|
Mira el LED **10 segundos con el reloj** y **cuenta cuántas veces prende**. No lo estimes:
|
||||||
5 destellos y 10 destellos en 10 segundos significan cosas **opuestas**.
|
5 destellos y 10 destellos en 10 segundos significan cosas **opuestas**.
|
||||||
@ -74,7 +140,7 @@ Mira el LED **10 segundos con el reloj** y **cuenta cuántas veces prende**. No
|
|||||||
| **Dos destellos** juntos y pausa | ~7 | `RUN` + enlace en las últimas | Prácticamente sin correcciones. |
|
| **Dos destellos** juntos y pausa | ~7 | `RUN` + enlace en las últimas | Prácticamente sin correcciones. |
|
||||||
| Parpadeo **rápido continuo** | no se cuentan | arrancando o reiniciándose | Puede estar reiniciándose en bucle. **Solo la captura USB lo confirma.** |
|
| Parpadeo **rápido continuo** | no se cuentan | arrancando o reiniciándose | Puede estar reiniciándose en bucle. **Solo la captura USB lo confirma.** |
|
||||||
|
|
||||||
## 6. Medidas esperadas
|
## 7. Medidas esperadas
|
||||||
|
|
||||||
Con la punta negra en el chasis/negativo del gabinete:
|
Con la punta negra en el chasis/negativo del gabinete:
|
||||||
|
|
||||||
@ -90,7 +156,7 @@ Con la punta negra en el chasis/negativo del gabinete:
|
|||||||
**No hay 24 V en el GX16** → la falla es **del gabinete o de la máquina** (breaker, fusible,
|
**No hay 24 V en el GX16** → la falla es **del gabinete o de la máquina** (breaker, fusible,
|
||||||
fuente, cable) → el HBOX probablemente está sano; no lo desmontes.
|
fuente, cable) → el HBOX probablemente está sano; no lo desmontes.
|
||||||
|
|
||||||
## 7. La captura por USB
|
## 8. La captura por USB
|
||||||
|
|
||||||
Es el paso que más nos sirve y el que más se salta. Vale la pena cargar la laptop.
|
Es el paso que más nos sirve y el que más se salta. Vale la pena cargar la laptop.
|
||||||
|
|
||||||
@ -122,7 +188,7 @@ Los cuatro campos que debes anotar aunque no puedas copiar la línea completa:
|
|||||||
Y de la línea `BOOT`, el campo `reset=` dice **por qué arrancó la última vez**
|
Y de la línea `BOOT`, el campo `reset=` dice **por qué arrancó la última vez**
|
||||||
(corte de energía, watchdog, brownout). Es la causa de muerte escrita por la propia unidad.
|
(corte de energía, watchdog, brownout). Es la causa de muerte escrita por la propia unidad.
|
||||||
|
|
||||||
## 8. Catálogo de clasificaciones
|
## 9. Catálogo de clasificaciones
|
||||||
|
|
||||||
Elige lo que **observaste**, no lo que supones. *No está claro* es una respuesta válida y útil;
|
Elige lo que **observaste**, no lo que supones. *No está claro* es una respuesta válida y útil;
|
||||||
inventar una causa nos hace perder semanas.
|
inventar una causa nos hace perder semanas.
|
||||||
@ -140,7 +206,7 @@ inventar una causa nos hace perder semanas.
|
|||||||
| La unidad se ve sana — parece el servidor | LED fijo y captura sana | **No desmontar.** Ingeniería revisa el backend |
|
| La unidad se ve sana — parece el servidor | LED fijo y captura sana | **No desmontar.** Ingeniería revisa el backend |
|
||||||
| No está claro — se necesita banco | Todo lo demás | Desmontar y enviar |
|
| No está claro — se necesita banco | Todo lo demás | Desmontar y enviar |
|
||||||
|
|
||||||
## 9. Qué pasa cuando envías el acta
|
## 10. Qué pasa cuando envías el acta
|
||||||
|
|
||||||
1. Queda guardada en el servidor con tu nombre, la hora y las fotos.
|
1. Queda guardada en el servidor con tu nombre, la hora y las fotos.
|
||||||
2. Aparece en el **[tablero de campo](tablero-campo.html)**, que ingeniería revisa en Querétaro.
|
2. Aparece en el **[tablero de campo](tablero-campo.html)**, que ingeniería revisa en Querétaro.
|
||||||
|
|||||||
@ -43,6 +43,9 @@ section.bloque h2{margin:0 0 2px;font-size:17px}
|
|||||||
.visita .f{color:var(--tinta2);font-size:12.5px}
|
.visita .f{color:var(--tinta2);font-size:12.5px}
|
||||||
.visita .cl{font-weight:700}
|
.visita .cl{font-weight:700}
|
||||||
.fotos{display:flex;flex-wrap:wrap;gap:7px;margin-top:8px}
|
.fotos{display:flex;flex-wrap:wrap;gap:7px;margin-top:8px}
|
||||||
|
.logs{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
|
||||||
|
.logs a{font-size:12.5px;font-weight:600;color:var(--info);text-decoration:none;
|
||||||
|
border:1px solid var(--linea);border-radius:7px;padding:4px 9px;background:var(--superf)}
|
||||||
.fotos a{display:block;width:74px;height:74px;border-radius:8px;overflow:hidden;border:1px solid var(--linea)}
|
.fotos a{display:block;width:74px;height:74px;border-radius:8px;overflow:hidden;border:1px solid var(--linea)}
|
||||||
.fotos img{width:100%;height:100%;object-fit:cover;display:block;background:var(--pend-bg)}
|
.fotos img{width:100%;height:100%;object-fit:cover;display:block;background:var(--pend-bg)}
|
||||||
details{margin-top:8px}
|
details{margin-top:8px}
|
||||||
@ -68,6 +71,7 @@ table.det td.k{color:var(--tinta2);width:45%}
|
|||||||
</div>
|
</div>
|
||||||
<script src="../formularios/flota.js"></script>
|
<script src="../formularios/flota.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
const TIPOS = ['diagnostico-campo','diagnostico-banco'];
|
||||||
const TIPO = 'diagnostico-campo';
|
const TIPO = 'diagnostico-campo';
|
||||||
const cont = document.getElementById('cont');
|
const cont = document.getElementById('cont');
|
||||||
const el = (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 el = (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;};
|
||||||
@ -103,12 +107,12 @@ async function detalle(tipo, serie, archivo, caja){
|
|||||||
function render(actas){
|
function render(actas){
|
||||||
cont.innerHTML='';
|
cont.innerHTML='';
|
||||||
const porUnidad = {};
|
const porUnidad = {};
|
||||||
actas.filter(a=>a.tipo===TIPO).forEach(a=>{ (porUnidad[a.serie]=porUnidad[a.serie]||[]).push(a); });
|
actas.filter(a=>TIPOS.includes(a.tipo)).forEach(a=>{ (porUnidad[a.serie]=porUnidad[a.serie]||[]).push(a); });
|
||||||
|
|
||||||
const flota = (window.FLOTA||[]).slice();
|
const flota = (window.FLOTA||[]).slice();
|
||||||
Object.keys(porUnidad).forEach(s=>{ if(!flota.some(u=>u.serie===s)) flota.push({serie:s, etiqueta:s, maquina:'(fuera del registro de flota)', terminal:'Sin clasificar'}); });
|
Object.keys(porUnidad).forEach(s=>{ if(!flota.some(u=>u.serie===s)) flota.push({serie:s, etiqueta:s, maquina:'(fuera del registro de flota)', terminal:'Sin clasificar'}); });
|
||||||
|
|
||||||
const visitas = actas.filter(a=>a.tipo===TIPO);
|
const visitas = actas.filter(a=>TIPOS.includes(a.tipo));
|
||||||
const conVisita = Object.keys(porUnidad).length;
|
const conVisita = Object.keys(porUnidad).length;
|
||||||
const operando = flota.filter(u=>{const v=(porUnidad[u.serie]||[])[0]; return v && pill(v)[0]==='ok';}).length;
|
const operando = flota.filter(u=>{const v=(porUnidad[u.serie]||[])[0]; return v && pill(v)[0]==='ok';}).length;
|
||||||
const kp = el('div',{class:'kpis'});
|
const kp = el('div',{class:'kpis'});
|
||||||
@ -136,7 +140,7 @@ function render(actas){
|
|||||||
if(!vs.length) box.append(el('div',{class:'visita vacio',text:'Todavía no hay ningún diagnóstico registrado para esta unidad.'}));
|
if(!vs.length) box.append(el('div',{class:'visita vacio',text:'Todavía no hay ningún diagnóstico registrado para esta unidad.'}));
|
||||||
vs.forEach(v=>{
|
vs.forEach(v=>{
|
||||||
const d = el('div',{class:'visita'});
|
const d = el('div',{class:'visita'});
|
||||||
d.append(el('div',{class:'f',text:fecha(v.recibido)+' · '+(v.tecnico||'—')+(v.motivo?' · '+v.motivo:'')}));
|
d.append(el('div',{class:'f',text:(v.tipo==='diagnostico-banco'?'🔬 banco · ':'🚜 campo · ')+fecha(v.recibido)+' · '+(v.tecnico||'—')+(v.motivo?' · '+v.motivo:'')}));
|
||||||
if(v.clasificacion) d.append(el('div',{class:'cl',text:v.clasificacion}));
|
if(v.clasificacion) d.append(el('div',{class:'cl',text:v.clasificacion}));
|
||||||
if(v.accion) d.append(el('div',{text:'→ '+v.accion}));
|
if(v.accion) d.append(el('div',{text:'→ '+v.accion}));
|
||||||
if(v.fotos && v.fotos.length){
|
if(v.fotos && v.fotos.length){
|
||||||
@ -148,10 +152,19 @@ function render(actas){
|
|||||||
});
|
});
|
||||||
d.append(f);
|
d.append(f);
|
||||||
}
|
}
|
||||||
|
if(v.archivos && v.archivos.length){
|
||||||
|
const g = el('div',{class:'logs'});
|
||||||
|
v.archivos.forEach(a=>{
|
||||||
|
const x = el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(a.ruta),target:'_blank',
|
||||||
|
text:'📄 '+(a.nombre||'captura')});
|
||||||
|
g.append(x);
|
||||||
|
});
|
||||||
|
d.append(g);
|
||||||
|
}
|
||||||
const det = el('details');
|
const det = el('details');
|
||||||
det.append(el('summary',{text:'Ver todas las respuestas'}));
|
det.append(el('summary',{text:'Ver todas las respuestas'}));
|
||||||
const caja = el('div'); det.append(caja);
|
const caja = el('div'); det.append(caja);
|
||||||
det.addEventListener('toggle',()=>{ if(det.open && !caja.dataset.listo){ caja.dataset.listo='1'; detalle(TIPO, v.serie, v.archivo_n, caja); } });
|
det.addEventListener('toggle',()=>{ if(det.open && !caja.dataset.listo){ caja.dataset.listo='1'; detalle(v.tipo, v.serie, v.archivo_n, caja); } });
|
||||||
d.append(det);
|
d.append(det);
|
||||||
box.append(d);
|
box.append(d);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -120,3 +120,12 @@ button.mini-btn{background:#fff;border:1px solid var(--linea);border-radius:6px;
|
|||||||
.barra{padding:10px 12px}
|
.barra{padding:10px 12px}
|
||||||
button.enviar{padding:15px 18px}
|
button.enviar{padding:15px 18px}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* archivos de log adjuntos (capturas por USB) */
|
||||||
|
.mini.doc{width:auto;min-width:160px;max-width:260px;flex-direction:row;flex-wrap:wrap;
|
||||||
|
align-items:center;gap:6px;padding:8px 10px}
|
||||||
|
.mini.doc .ico{font-size:18px}
|
||||||
|
.mini.doc .nom{font-size:12.5px;font-weight:600;word-break:break-all;flex:1 1 90px}
|
||||||
|
.mini.doc .est{font-size:11.5px;flex:0 0 auto}
|
||||||
|
input[type=date]{width:100%;padding:11px 12px;border:1px solid var(--linea);border-radius:9px;
|
||||||
|
font-size:16px;background:#fff;color:var(--tinta)}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
- Un acta real se envía UNA vez: al elegir una serie ya enviada, se carga llena y bloqueada.
|
- Un acta real se envía UNA vez: al elegir una serie ya enviada, se carga llena y bloqueada.
|
||||||
- Solo el administrador (leo) ve el botón "Borrar acta (reabrir)".
|
- Solo el administrador (leo) ve el botón "Borrar acta (reabrir)".
|
||||||
- La serie PRUEBA no se bloquea (para practicar).
|
- La serie PRUEBA no se bloquea (para practicar).
|
||||||
Tipos de ítem: check | medida | texto | radio | nota | foto · Envío: POST /api/actas
|
Tipos de ítem: check | medida | texto | radio | nota | foto | archivo | fecha · Envío: POST /api/actas
|
||||||
|
|
||||||
Extensiones para actas de CAMPO (diagnóstico / instalación):
|
Extensiones para actas de CAMPO (diagnóstico / instalación):
|
||||||
(c) cfg.repetible = true → la serie NO se bloquea: cada visita es un registro nuevo,
|
(c) cfg.repetible = true → la serie NO se bloquea: cada visita es un registro nuevo,
|
||||||
@ -15,6 +15,8 @@
|
|||||||
item.req = true → obligatorio para poder enviar
|
item.req = true → obligatorio para poder enviar
|
||||||
item.guardaEn = 'clasificacion' → además copia el valor a la raíz del acta (índice/tablero)
|
item.guardaEn = 'clasificacion' → además copia el valor a la raíz del acta (índice/tablero)
|
||||||
item.tipo = 'foto' → cámara del teléfono; sube a /api/adjuntos y guarda la ruta
|
item.tipo = 'foto' → cámara del teléfono; sube a /api/adjuntos y guarda la ruta
|
||||||
|
item.tipo = 'archivo' → captura de log (.txt .log .csv .nmea .ubx); sube a /api/archivos
|
||||||
|
item.tipo = 'fecha' → selector de fecha; arranca con la de hoy
|
||||||
|
|
||||||
Modos de serie:
|
Modos de serie:
|
||||||
(a) simple: cfg.prefijo + cfg.series (cantidad) [+ cfg.desde, cfg.pad] → "RTK-005"
|
(a) simple: cfg.prefijo + cfg.series (cantidad) [+ cfg.desde, cfg.pad] → "RTK-005"
|
||||||
@ -154,7 +156,8 @@
|
|||||||
const condicionales = []; // [{el, fn}] — secciones o ítems que solo se muestran si se cumple algo
|
const condicionales = []; // [{el, fn}] — secciones o ítems que solo se muestran si se cumple algo
|
||||||
const obligatorios = []; // [{id, txt, esFoto}]
|
const obligatorios = []; // [{id, txt, esFoto}]
|
||||||
const guardaEn = {}; // data-id -> nombre del campo en la raíz del acta
|
const guardaEn = {}; // data-id -> nombre del campo en la raíz del acta
|
||||||
let fotos = []; // [{ruta, etiqueta, pendiente?}]
|
let fotos = []; // [{ruta, etiqueta, campo}]
|
||||||
|
let archivos = []; // [{ruta, nombre, bytes, campo}] — capturas de log
|
||||||
const pendientes = []; // fotos que no lograron subir: {blob, etiqueta, li}
|
const pendientes = []; // fotos que no lograron subir: {blob, etiqueta, li}
|
||||||
|
|
||||||
cfg.secciones.forEach((sec, si) => {
|
cfg.secciones.forEach((sec, si) => {
|
||||||
@ -166,7 +169,8 @@
|
|||||||
const id = it.clave || `s${si}_${ii}`;
|
const id = it.clave || `s${si}_${ii}`;
|
||||||
const box = $('div', { class: 'item' });
|
const box = $('div', { class: 'item' });
|
||||||
if (typeof it.si === 'function') condicionales.push({ el: box, fn: it.si });
|
if (typeof it.si === 'function') condicionales.push({ el: box, fn: it.si });
|
||||||
if (it.req) obligatorios.push({ id, txt: String(it.txt || '').replace(/<[^>]+>/g, ''), esFoto: it.tipo === 'foto' });
|
if (it.req) obligatorios.push({ id, txt: String(it.txt || '').replace(/<[^>]+>/g, ''),
|
||||||
|
esFoto: it.tipo === 'foto', esArchivo: it.tipo === 'archivo' });
|
||||||
if (it.guardaEn) guardaEn[id] = it.guardaEn;
|
if (it.guardaEn) guardaEn[id] = it.guardaEn;
|
||||||
if (it.aviso) box.append($('div', { class: 'aviso', html: it.aviso }));
|
if (it.aviso) box.append($('div', { class: 'aviso', html: it.aviso }));
|
||||||
if (it.tipo === 'check') {
|
if (it.tipo === 'check') {
|
||||||
@ -194,6 +198,23 @@
|
|||||||
box.append(g);
|
box.append(g);
|
||||||
} else if (it.tipo === 'nota') {
|
} else if (it.tipo === 'nota') {
|
||||||
box.append($('div', { class: 'nota-item', html: it.txt }));
|
box.append($('div', { class: 'nota-item', html: it.txt }));
|
||||||
|
} else if (it.tipo === 'fecha') {
|
||||||
|
box.append($('label', { html: it.txt }));
|
||||||
|
const hoy = new Date(); hoy.setMinutes(hoy.getMinutes() - hoy.getTimezoneOffset());
|
||||||
|
const hoyISO = hoy.toISOString().slice(0, 10);
|
||||||
|
box.append($('input', { type: 'date', 'data-id': id, 'data-lbl': String(it.txt || '').replace(/<[^>]+>/g, ''),
|
||||||
|
value: hoyISO, 'data-def': hoyISO, style: 'max-width:220px' }));
|
||||||
|
} else if (it.tipo === 'archivo') {
|
||||||
|
box.classList.add('foto');
|
||||||
|
box.append($('span', { class: 'txt', html: (it.req ? '<b>Archivo obligatorio</b> — ' : 'Archivo — ') + it.txt }));
|
||||||
|
if (it.ayuda) box.append($('div', { class: 'nota-item', html: it.ayuda }));
|
||||||
|
const inp = $('input', { type: 'file', accept: '.txt,.log,.csv,.nmea,.ubx,.json,.cfg,text/plain', multiple: 'multiple', style: 'display:none' });
|
||||||
|
const btn = $('button', { type: 'button', class: 'btn-foto', text: '📄 Subir archivo de captura' });
|
||||||
|
const tiras = $('div', { class: 'miniaturas', 'data-fotos': id });
|
||||||
|
const marca = $('input', { type: 'hidden', 'data-id': id, 'data-lbl': 'Archivos: ' + String(it.txt || '').replace(/<[^>]+>/g, ''), value: '' });
|
||||||
|
btn.addEventListener('click', () => inp.click());
|
||||||
|
inp.addEventListener('change', () => { subirArchivos(inp.files, id, it.etiqueta || String(it.txt || '').replace(/<[^>]+>/g, '').slice(0, 60), tiras, marca); inp.value = ''; });
|
||||||
|
box.append(btn, inp, tiras, marca);
|
||||||
} else if (it.tipo === 'foto') {
|
} else if (it.tipo === 'foto') {
|
||||||
box.classList.add('foto');
|
box.classList.add('foto');
|
||||||
box.append($('span', { class: 'txt', html: (it.req ? '<b>Foto obligatoria</b> — ' : 'Foto — ') + it.txt }));
|
box.append($('span', { class: 'txt', html: (it.req ? '<b>Foto obligatoria</b> — ' : 'Foto — ') + it.txt }));
|
||||||
@ -260,15 +281,30 @@
|
|||||||
t.append(li);
|
t.append(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function pintarArchivos() {
|
||||||
|
archivos.forEach(a => {
|
||||||
|
const t = wrap.querySelector(`.miniaturas[data-fotos="${a.campo}"]`);
|
||||||
|
if (!t) return;
|
||||||
|
const li = $('div', { class: 'mini doc ok' });
|
||||||
|
li.append($('span', { class: 'ico', text: '📄' }),
|
||||||
|
$('span', { class: 'nom', text: String(a.nombre || '').slice(0, 28) }),
|
||||||
|
$('span', { class: 'est', text: '✓ guardado' }));
|
||||||
|
t.append(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
function blankFields() {
|
function blankFields() {
|
||||||
fotos = [];
|
fotos = []; archivos = [];
|
||||||
wrap.querySelectorAll('.miniaturas').forEach(t => t.innerHTML = '');
|
wrap.querySelectorAll('.miniaturas').forEach(t => t.innerHTML = '');
|
||||||
fields().forEach(el => { if (el.type === 'checkbox' || el.type === 'radio') el.checked = false; else el.value = ''; });
|
fields().forEach(el => {
|
||||||
|
if (el.type === 'checkbox' || el.type === 'radio') el.checked = false;
|
||||||
|
else el.value = (el.dataset.def !== undefined ? el.dataset.def : '');
|
||||||
|
});
|
||||||
const rr = wrap.querySelector('input[name=resultado]:checked'); if (rr) rr.checked = false;
|
const rr = wrap.querySelector('input[name=resultado]:checked'); if (rr) rr.checked = false;
|
||||||
conforme().checked = false; tecnico.value = nombreUsuario;
|
conforme().checked = false; tecnico.value = nombreUsuario;
|
||||||
}
|
}
|
||||||
function fillFrom(a) {
|
function fillFrom(a) {
|
||||||
if (Array.isArray(a.fotos)) { fotos = a.fotos.slice(); pintarFotos(); }
|
if (Array.isArray(a.fotos)) { fotos = a.fotos.slice(); pintarFotos(); }
|
||||||
|
if (Array.isArray(a.archivos)) { archivos = a.archivos.slice(); pintarArchivos(); }
|
||||||
tecnico.value = a.tecnico || '';
|
tecnico.value = a.tecnico || '';
|
||||||
conforme().checked = !!a.conforme;
|
conforme().checked = !!a.conforme;
|
||||||
if (a.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${a.resultado}"]`); if (el) el.checked = true; }
|
if (a.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${a.resultado}"]`); if (el) el.checked = true; }
|
||||||
@ -310,6 +346,9 @@
|
|||||||
else if (el.type !== 'hidden') v[k] = el.value;
|
else if (el.type !== 'hidden') v[k] = el.value;
|
||||||
});
|
});
|
||||||
v.__fotos = fotos;
|
v.__fotos = fotos;
|
||||||
|
v.__archivos = archivos;
|
||||||
|
// atajo para condiciones: ¿ya se subió un archivo de este campo?
|
||||||
|
v.subido = c => archivos.some(x => x.campo === c);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
function aplicarCondiciones() {
|
function aplicarCondiciones() {
|
||||||
@ -331,6 +370,7 @@
|
|||||||
obligatorios.forEach(o => {
|
obligatorios.forEach(o => {
|
||||||
const el = wrap.querySelector(`[data-id="${o.id}"]`);
|
const el = wrap.querySelector(`[data-id="${o.id}"]`);
|
||||||
if (!el || !visible(el)) return; // oculto por una condición: no aplica
|
if (!el || !visible(el)) return; // oculto por una condición: no aplica
|
||||||
|
if (o.esArchivo) { if (!archivos.some(a => a.campo === o.id)) falta.push(o.txt); return; }
|
||||||
if (o.esFoto) { if (!fotos.some(f => f.campo === o.id)) falta.push(o.txt); return; }
|
if (o.esFoto) { if (!fotos.some(f => f.campo === o.id)) falta.push(o.txt); return; }
|
||||||
const val = v[o.id];
|
const val = v[o.id];
|
||||||
if (val === '' || val === false || val === undefined || val === null) falta.push(o.txt);
|
if (val === '' || val === false || val === undefined || val === null) falta.push(o.txt);
|
||||||
@ -370,6 +410,37 @@
|
|||||||
li.append(rt);
|
li.append(rt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function kb(n) { return n > 1048576 ? (n / 1048576).toFixed(1) + ' MB' : Math.max(1, Math.round(n / 1024)) + ' kB'; }
|
||||||
|
async function subirArchivos(lista, campo, etiqueta, tiras, marca) {
|
||||||
|
if (!serieActual()) { fail('Primero elige la unidad: los archivos se guardan con su número de serie.'); return; }
|
||||||
|
for (const file of Array.from(lista).slice(0, 10)) {
|
||||||
|
const li = $('div', { class: 'mini doc subiendo' });
|
||||||
|
li.append($('span', { class: 'ico', text: '📄' }),
|
||||||
|
$('span', { class: 'nom', text: file.name.slice(0, 28) }));
|
||||||
|
const est = $('span', { class: 'est', text: 'subiendo…' }); li.append(est); tiras.append(li);
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('tipo', cfg.tipo); fd.append('serie', serieActual());
|
||||||
|
fd.append('etiqueta', etiqueta); fd.append('archivo', file, file.name);
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/archivos', { method: 'POST', body: fd });
|
||||||
|
const j = await r.json();
|
||||||
|
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||||
|
const a = j.archivos[0]; a.campo = campo; archivos.push(a);
|
||||||
|
li.className = 'mini doc ok'; est.textContent = '✓ ' + kb(a.bytes);
|
||||||
|
const q = $('button', { type: 'button', class: 'mini-btn', text: '✕ quitar' });
|
||||||
|
q.addEventListener('click', () => {
|
||||||
|
archivos = archivos.filter(x => x !== a); li.remove();
|
||||||
|
marca.value = archivos.filter(x => x.campo === campo).length || '';
|
||||||
|
guardarLocal(); aplicarCondiciones();
|
||||||
|
});
|
||||||
|
li.append(q);
|
||||||
|
marca.value = archivos.filter(x => x.campo === campo).length;
|
||||||
|
guardarLocal(); aplicarCondiciones();
|
||||||
|
} catch (e) {
|
||||||
|
li.className = 'mini doc err'; est.textContent = 'no subió — ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
async function subirFotos(lista, campo, etiqueta, tiras, marca) {
|
async function subirFotos(lista, campo, etiqueta, tiras, marca) {
|
||||||
if (!serieActual()) { fail('Primero elige la unidad: las fotos se guardan con su número de serie.'); return; }
|
if (!serieActual()) { fail('Primero elige la unidad: las fotos se guardan con su número de serie.'); return; }
|
||||||
for (const file of Array.from(lista).slice(0, 10)) {
|
for (const file of Array.from(lista).slice(0, 10)) {
|
||||||
@ -477,6 +548,7 @@
|
|||||||
conforme: conforme().checked, resultado: rr ? rr.value : '', respuestas: r };
|
conforme: conforme().checked, resultado: rr ? rr.value : '', respuestas: r };
|
||||||
if (compuesta) { d.componentes = seleccion(); d.verificacion = Object.assign({}, verif); }
|
if (compuesta) { d.componentes = seleccion(); d.verificacion = Object.assign({}, verif); }
|
||||||
if (fotos.length) d.fotos = fotos;
|
if (fotos.length) d.fotos = fotos;
|
||||||
|
if (archivos.length) d.archivos = archivos;
|
||||||
const v = valores();
|
const v = valores();
|
||||||
for (const k in guardaEn) if (v[k] !== undefined && v[k] !== '') d[guardaEn[k]] = v[k];
|
for (const k in guardaEn) if (v[k] !== undefined && v[k] !== '') d[guardaEn[k]] = v[k];
|
||||||
if (flota) {
|
if (flota) {
|
||||||
|
|||||||
@ -1,381 +0,0 @@
|
|||||||
/* 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 };
|
|
||||||
})();
|
|
||||||
202
docs/formularios/banco.html
Normal file
202
docs/formularios/banco.html
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Acta de diagnóstico en banco — HBOX</title>
|
||||||
|
<link rel="stylesheet" href="acta.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="acta"></div>
|
||||||
|
<script src="flota.js"></script>
|
||||||
|
<script src="acta.js"></script>
|
||||||
|
<script>
|
||||||
|
const corto = v => String(v.continuidad || '').startsWith('Sí');
|
||||||
|
const enciende = v => String(v.rampa || '').startsWith('C') || String(v.rampa || '').startsWith('D');
|
||||||
|
const listoParaFlashear = v => v.subido && v.subido('log_blackpill');
|
||||||
|
|
||||||
|
Acta.render({
|
||||||
|
tipo: 'diagnostico-banco',
|
||||||
|
repetible: true,
|
||||||
|
tecnicoDesdeUsuario: true,
|
||||||
|
nombres: { alan: 'Alán Basurto', daniel: 'Daniel Martínez', leo: 'Leo Rodríguez' },
|
||||||
|
unidades: window.FLOTA,
|
||||||
|
etiquetaSerie: 'Unidad que estás revisando',
|
||||||
|
titulo: 'Acta de diagnóstico en banco — HBOX',
|
||||||
|
subtitulo: 'Helgen Technologies · unidad sobre la mesa, con fuente de banco y multímetro.',
|
||||||
|
textoOK: 'QUEDA FUNCIONAL',
|
||||||
|
intro: '<b>Antes de empezar.</b> En una unidad ya desmontada se perdieron la temperatura máxima, '
|
||||||
|
+ 'la cuenta de recuperaciones y la causa del último arranque: el firmware v3 las guarda en '
|
||||||
|
+ 'memoria y se borraron al desconectarla. Aquí no vamos a poder probar la historia térmica con '
|
||||||
|
+ 'datos del equipo — solo con lo que se vea. Lo que sí sale de este banco: si hay corto, cuánto '
|
||||||
|
+ 'consume, si arranca, si el módem registra, si el GNSS ve cielo y en qué estado está la batería. '
|
||||||
|
+ 'Procedimiento completo: <a href="../procedimientos/diagnostico-banco/">Diagnóstico en banco</a>.',
|
||||||
|
secciones: [
|
||||||
|
|
||||||
|
{ titulo: '0 · Identificación',
|
||||||
|
items: [
|
||||||
|
{ tipo:'fecha', clave:'fecha', req:true, guardaEn:'fecha', txt:'Fecha de la revisión' },
|
||||||
|
{ tipo:'texto', clave:'maquina_id', rows:'1', guardaEn:'maquina_id',
|
||||||
|
txt:'¿De qué máquina venía esta unidad?', ph:'RT-ZPMC-20 · HS-WES-62 · FL-JUN-060' },
|
||||||
|
{ tipo:'radio', clave:'llego', txt:'¿Cómo llegó a la mesa?',
|
||||||
|
opciones:['Desmontada por nosotros','Desmontada por personal de ICAVE','No sé quién la desmontó'] },
|
||||||
|
{ tipo:'texto', clave:'antecedente', rows:'2',
|
||||||
|
txt:'¿Qué se sabe de su falla?', ph:'Dejó de reportar a fines de agosto; se desmontó el 15-sep.' },
|
||||||
|
{ tipo:'nota', txt:'<div class="peligro"><b>Orden obligatorio:</b> inspección → continuidad → '
|
||||||
|
+ 'energizado. <b>Nunca energices antes de medir continuidad.</b> Si hay un corto, la fuente '
|
||||||
|
+ 'lo encuentra quemando algo.</div>' },
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ titulo: '1 · Inspección sin energía',
|
||||||
|
sub:'Con la unidad desconectada de todo y antes de mover cables.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'foto', clave:'f_ext', req:true, etiqueta:'exterior', txt:'Exterior de la caja (varios lados).' },
|
||||||
|
{ tipo:'foto', clave:'f_int', req:true, etiqueta:'interior', txt:'Interior completo, antes de tocar nada.' },
|
||||||
|
{ tipo:'radio', clave:'olor', req:true, txt:'¿Huele a quemado o hay partes oscurecidas o derretidas?',
|
||||||
|
opciones:['No','Sí, leve','Sí, evidente'] },
|
||||||
|
{ tipo:'foto', clave:'f_dano', etiqueta:'dano', si:v=>String(v.olor||'').startsWith('Sí'), req:true,
|
||||||
|
txt:'Foto del daño por calor.' },
|
||||||
|
{ tipo:'radio', clave:'agua', req:true, txt:'¿Agua, marcas de agua, sarro o corrosión verde?',
|
||||||
|
opciones:['No','Marcas o humedad','Corrosión evidente'] },
|
||||||
|
{ tipo:'radio', clave:'bateria_fisica', req:true, txt:'Estado físico de la batería',
|
||||||
|
opciones:['Normal','Hinchada o abombada','Con manchas o fuga','Caliente','La unidad no trae batería'],
|
||||||
|
aviso:'Hinchada, con fuga o caliente: <b>desconéctala antes de energizar</b> y trabaja sin ella. No la cargues, no la perfores, no viaja en avión.' },
|
||||||
|
{ tipo:'foto', clave:'f_bat', etiqueta:'bateria',
|
||||||
|
si:v=>['Hinchada','Con manchas','Caliente'].some(t=>String(v.bateria_fisica||'').startsWith(t)), req:true,
|
||||||
|
txt:'Foto de la batería, dos ángulos.' },
|
||||||
|
{ tipo:'check', clave:'wiggle', txt:'Prueba de meneo en cada conector, Dupont y SMA. Anoto abajo lo que esté flojo.' },
|
||||||
|
{ tipo:'texto', clave:'nota_visual', rows:'2', txt:'¿Qué encontraste?' },
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ titulo: '2 · Continuidad a tierra — antes de energizar',
|
||||||
|
sub:'Multímetro en ohms, punta negra en el negativo/chasis. La lectura sube despacio: es normal, espera a que se estabilice.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', clave:'r_24v', txt:'Entrada 24 V (GX16 pines 1-2)', esperado:'> 10 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', clave:'r_5v', txt:'Riel de 5 V (salida Mean Well)', esperado:'> 1 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', clave:'r_3v3', txt:'Riel de 3.3 V', esperado:'> 1 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', clave:'r_rtk', txt:'Alimentación del simpleRTK2B (riel conmutado)', esperado:'> 1 kΩ', u:'Ω' },
|
||||||
|
{ tipo:'medida', clave:'v_bat_vacio', txt:'Voltaje de la batería en vacío (desconectada)', esperado:'3.0 – 4.2 V', u:'V' },
|
||||||
|
{ tipo:'radio', clave:'continuidad', req:true, txt:'¿Alguna lectura se quedó fija entre 0 y 20 Ω?',
|
||||||
|
opciones:['No, todas suben o están altas','Sí — hay un corto'] },
|
||||||
|
{ tipo:'nota', si:corto, txt:'<div class="peligro"><b>Hay un corto: no energices.</b> Fotografía la zona, '
|
||||||
|
+ 'marca la unidad, complétala como FALLA y déjala aparte para enviar a Querétaro. '
|
||||||
|
+ 'Energizar un corto convierte una reparación en una tarjeta perdida.</div>' },
|
||||||
|
{ tipo:'foto', clave:'f_corto', etiqueta:'corto', si:corto, req:true, txt:'Foto de la zona donde mediste el corto.' },
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ titulo: '3 · Energizado con rampa de corriente', si:v=>!corto(v),
|
||||||
|
sub:'Fuente a 24.0 V, límite de corriente 500 mA, sin antenas todavía. Mira el amperímetro los primeros 3 segundos.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', clave:'i_pico', txt:'Corriente de arranque (pico)', u:'mA' },
|
||||||
|
{ tipo:'medida', clave:'i_estable', txt:'Corriente estable a los 60 segundos', esperado:'150 – 400 mA con la etapa RTK encendida', u:'mA' },
|
||||||
|
{ tipo:'radio', clave:'rampa', req:true, txt:'¿Qué hizo al energizar?',
|
||||||
|
opciones:[
|
||||||
|
'A · 0 mA, la fuente no entrega nada',
|
||||||
|
'B · Se pega al límite y el voltaje se cae (corto)',
|
||||||
|
'C · 50–150 mA estables, sin LED',
|
||||||
|
'D · 150–400 mA y el LED parpadea',
|
||||||
|
'E · Sube despacio y algo se calienta'] },
|
||||||
|
{ tipo:'nota', si:v=>/^[BE]/.test(String(v.rampa||'')),
|
||||||
|
txt:'<div class="peligro"><b>Apaga la fuente ahora.</b> Busca con el dorso de la mano qué componente '
|
||||||
|
+ 'se calienta, tómale foto y no vuelvas a energizar.</div>' },
|
||||||
|
{ tipo:'foto', clave:'f_calor', etiqueta:'sobrecalentamiento', si:v=>String(v.rampa||'').startsWith('E'), req:true,
|
||||||
|
txt:'Foto del componente que se calienta.' },
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ titulo: '4 · Rieles', si:enciende,
|
||||||
|
sub:'Con la unidad energizada y estable.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'medida', clave:'v_24', txt:'Entrada 24 V', esperado:'23 – 26 V', u:'V' },
|
||||||
|
{ tipo:'medida', clave:'v_5', txt:'Riel 5 V (salida Mean Well)', esperado:'4.8 – 5.2 V', u:'V' },
|
||||||
|
{ tipo:'medida', clave:'v_3v3',txt:'Riel 3.3 V', esperado:'3.20 – 3.40 V', u:'V' },
|
||||||
|
{ tipo:'medida', clave:'v_rtk_arranque', txt:'Alimentación del simpleRTK2B al arrancar', esperado:'0 V (obligatorio)', u:'V',
|
||||||
|
aviso:'Si hay 5 V aquí en el instante de arrancar es un <b>defecto</b>: apagado es el estado seguro, la etapa RTK solo debe encender cuando el MCU lo pide.' },
|
||||||
|
{ tipo:'medida', clave:'v_bat_on', txt:'Batería con la unidad encendida', u:'V' },
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ titulo: '5 · Captura del Blackpill (la principal)', si:enciende,
|
||||||
|
sub:'USB del Blackpill · 115200 baudios · guardar a archivo · 3 minutos, luego RESET.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'archivo', clave:'log_blackpill', etiqueta:'blackpill', req:true,
|
||||||
|
txt:'Sube el archivo de la captura del Blackpill',
|
||||||
|
ayuda:'Nómbralo <code>RTK-0NN_blackpill_AAAA-MM-DD.log</code>. Súbelo aunque no entiendas lo que dice.' },
|
||||||
|
{ tipo:'texto', clave:'linea_boot', rows:'2', txt:'Pega la línea BOOT (dice qué firmware trae)', ph:'BOOT,1240,fw=3.0.3,reset=PIN' },
|
||||||
|
{ tipo:'texto', clave:'linea_tlm', rows:'3', txt:'Pega una línea TLM ya estabilizada', ph:'TLM,182000,RUN,DEGR,1,11,…' },
|
||||||
|
{ tipo:'radio', clave:'estado_fw', txt:'¿A qué estado llegó después de 10 minutos con antenas?',
|
||||||
|
opciones:['RUN con link OK','RUN pero link DEGR o LIMP','Se queda en BOOT','Se reinicia solo una y otra vez','SLEEP'] },
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ titulo: '6 · Captura del módem 4G NTRIP Master', si:enciende,
|
||||||
|
sub:'Puerto USB "XBEE" del simpleRTK2B Pro · 115200 baudios · 3 minutos desde el arranque.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'nota', txt:'<b>Ojo con esta consola:</b> una unidad sana imprime casi lo mismo que una '
|
||||||
|
+ 'averiada. <b>No prueba</b> que las correcciones lleguen ni que los datos se envíen — eso solo lo '
|
||||||
|
+ 'dicen el <code>link</code> del Blackpill, un fix ≥4 en el ArduSimple o el tráfico en el servidor. '
|
||||||
|
+ 'Sirve para saber si el módem enciende, si registra en Telcel y con qué configuración quedó.' },
|
||||||
|
{ tipo:'archivo', clave:'log_4g', etiqueta:'modem4g', txt:'Sube la captura del módem 4G',
|
||||||
|
ayuda:'Nómbralo <code>RTK-0NN_modem4g_AAAA-MM-DD.log</code>.' },
|
||||||
|
{ tipo:'radio', clave:'led_4g', txt:'LED de red del módulo 4G',
|
||||||
|
opciones:['Parpadeo lento o fijo (registrado)','Parpadeo rápido (buscando, no registra)','Apagado'] },
|
||||||
|
{ tipo:'texto', clave:'id_estacion', rows:'1', txt:'ID de estación (AS+K) y mountpoint, si pudiste leerlos' },
|
||||||
|
{ tipo:'texto', clave:'imei_iccid', rows:'1', txt:'IMEI del módem e ICCID de la SIM, si están a la vista' },
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ titulo: '7 · Captura del simpleRTK2B Pro (ArduSimple)', si:enciende,
|
||||||
|
sub:'USB POWER+GPS · 115200 baudios · antena GNSS CON VISTA AL CIELO (dentro del taller no habrá fix).',
|
||||||
|
items: [
|
||||||
|
{ tipo:'archivo', clave:'log_ard', etiqueta:'ardusimple', txt:'Sube la captura de NMEA o el log de u-center',
|
||||||
|
ayuda:'Nómbralo <code>RTK-0NN_ardusimple_AAAA-MM-DD.nmea</code>.' },
|
||||||
|
{ tipo:'medida', clave:'sats', txt:'Satélites vistos', esperado:'≥ 8 a cielo abierto' },
|
||||||
|
{ tipo:'radio', clave:'calidad', txt:'Calidad del fix (sexto campo de $GNGGA, o Fix Mode en u-center)',
|
||||||
|
opciones:['0 — sin posición','1 — GPS solo, sin correcciones','2 — corregido básico','4 — RTK fijo','5 — RTK flotante'],
|
||||||
|
aviso:'Un receptor en <b>1</b> con buenos satélites <b>no está roto</b>: dice que las correcciones no le llegan. Eso apunta al módem o al servidor, no al GNSS.' },
|
||||||
|
{ tipo:'texto', clave:'fw_f9p', rows:'1', txt:'Versión de firmware del ZED-F9P (u-center → UBX-MON-VER)' },
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ titulo: '8 · Reflasheo — solo si ya subiste la captura', si:enciende,
|
||||||
|
sub:'El flasheo borra la memoria del equipo y cambia el firmware que estaba fallando. Por eso va después de la evidencia, no antes.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'nota', si:v=>!listoParaFlashear(v),
|
||||||
|
txt:'<div class="peligro">⛔ <b>Todavía no puedes reflashear.</b> Sube primero la captura del '
|
||||||
|
+ 'Blackpill en el paso 5. Sin ella, reflashear destruye la única evidencia del estado en que '
|
||||||
|
+ 'estaba la unidad.</div>' },
|
||||||
|
{ tipo:'radio', clave:'reflash', si:listoParaFlashear, txt:'¿Reflasheaste la unidad?',
|
||||||
|
opciones:['No','Sí, con el binario publicado en Descargas'] },
|
||||||
|
{ tipo:'texto', clave:'fw_nuevo', si:v=>String(v.reflash||'').startsWith('Sí'), rows:'1',
|
||||||
|
txt:'Versión que quedó (línea BOOT después del flasheo)', ph:'fw=3.0.3' },
|
||||||
|
{ tipo:'archivo', clave:'log_post', etiqueta:'post-flasheo', si:v=>String(v.reflash||'').startsWith('Sí'),
|
||||||
|
txt:'Captura DESPUÉS del flasheo', ayuda:'Las dos capturas juntas son la prueba de si el firmware cambió algo.' },
|
||||||
|
{ tipo:'radio', clave:'reflash_result', si:v=>String(v.reflash||'').startsWith('Sí'),
|
||||||
|
txt:'¿Cambió algo?', opciones:['Sí, ahora arranca y se estabiliza','No, repite la misma falla','Empeoró: ya no arranca'] },
|
||||||
|
] },
|
||||||
|
|
||||||
|
{ titulo: '9 · Veredicto',
|
||||||
|
items: [
|
||||||
|
{ tipo:'radio', clave:'clasificacion', req:true, guardaEn:'clasificacion', txt:'Clasificación',
|
||||||
|
opciones:[
|
||||||
|
'Corto a tierra',
|
||||||
|
'No enciende, sin corto',
|
||||||
|
'Enciende pero la etapa RTK no arranca',
|
||||||
|
'Módem no registra en la red',
|
||||||
|
'GNSS sin correcciones (fix 1) con módem registrado',
|
||||||
|
'Batería dañada',
|
||||||
|
'Daño por calor visible',
|
||||||
|
'Agua o corrosión',
|
||||||
|
'Funciona en banco — no se reproduce la falla',
|
||||||
|
'No está claro'] },
|
||||||
|
{ tipo:'radio', clave:'accion', req:true, guardaEn:'accion', txt:'Disposición',
|
||||||
|
opciones:[
|
||||||
|
'Lista para reinstalar',
|
||||||
|
'Reflasheada, en observación',
|
||||||
|
'Se queda en el taller esperando refacción',
|
||||||
|
'Se envía a Querétaro',
|
||||||
|
'Baja definitiva'] },
|
||||||
|
{ tipo:'texto', clave:'observaciones', rows:'4', req:true,
|
||||||
|
txt:'Cuenta qué viste y qué hiciste',
|
||||||
|
ph:'Escríbelo como se lo contarías a un compañero. Esto es lo que va a leer ingeniería.' },
|
||||||
|
] },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -47,6 +47,34 @@ Acta.render({
|
|||||||
{ tipo:'check', clave:'reglas', req:true, txt:'<b>Leí las cuatro reglas</b> y voy a seguir el procedimiento en orden.' },
|
{ tipo:'check', clave:'reglas', req:true, txt:'<b>Leí las cuatro reglas</b> y voy a seguir el procedimiento en orden.' },
|
||||||
] },
|
] },
|
||||||
|
|
||||||
|
{ titulo: '0 · Permiso de acceso y máquina — antes de subirte a nada',
|
||||||
|
sub:'En ICAVE el equipo no se identifica por el HBOX sino por la MÁQUINA, y no se sube a una máquina sin autorización por escrito.',
|
||||||
|
items: [
|
||||||
|
{ tipo:'nota', txt:'<b>Cómo funciona.</b> Operaciones (Adriana) manda a ICAVE un correo pidiendo acceso a la '
|
||||||
|
+ '<b>máquina</b> por su número, nombrando a los técnicos que van a subir. Hasta que ICAVE contesta y se acuerda '
|
||||||
|
+ 'el horario, no hay visita. Si llegaste sin ese correo contestado, <b>no subas</b>: avisa y reagenda.' },
|
||||||
|
{ tipo:'fecha', clave:'fecha', req:true, guardaEn:'fecha', txt:'Fecha de la revisión',
|
||||||
|
ayuda:'Arranca con la de hoy. Cámbiala si estás capturando una visita de otro día.' },
|
||||||
|
{ tipo:'texto', clave:'maquina_id', req:true, guardaEn:'maquina_id', rows:'1',
|
||||||
|
txt:'Número de la máquina donde está instalado el HBOX, tal como lo nombra ICAVE',
|
||||||
|
ph:'FL-JUN-060 · RT-ZPMC-20 · HS-WES-62' },
|
||||||
|
{ tipo:'radio', clave:'coincide', req:true, txt:'¿Coincide con la máquina que aparece arriba, junto a la unidad que elegiste?',
|
||||||
|
opciones:['Sí, es la misma','No — la unidad está en otra máquina (explícalo abajo)','La lista no dice la máquina'],
|
||||||
|
aviso:'Si NO coincide, avisa a ingeniería: el registro de qué unidad está en qué máquina quedó mal y hay que corregirlo.' },
|
||||||
|
{ tipo:'radio', clave:'permiso', req:true, guardaEn:'permiso',
|
||||||
|
txt:'¿Hay autorización de acceso a esa máquina?',
|
||||||
|
opciones:['Sí, autorizada por correo','Sí, autorizada en sitio por el supervisor','No — solo la reviso desde afuera, sin subir','No aplica — la unidad ya está en el taller'] },
|
||||||
|
{ tipo:'texto', clave:'permiso_quien', si:v=>String(v.permiso||'').startsWith('Sí'), rows:'1',
|
||||||
|
txt:'¿Quién autorizó y a qué hora?', ph:'Abraham (ICAVE) — correo del 18-sep, acceso 09:40' },
|
||||||
|
{ tipo:'check', clave:'loto', si:v=>String(v.permiso||'').startsWith('Sí'),
|
||||||
|
txt:'La máquina está <b>detenida y bloqueada (LOTO)</b>, o el supervisor confirmó que es seguro trabajar en ella.' },
|
||||||
|
{ tipo:'nota', si:v=>String(v.permiso||'').startsWith('No —'),
|
||||||
|
txt:'<div class="peligro"><b>Sin autorización no te subas al equipo.</b> Haz lo que se pueda desde el piso '
|
||||||
|
+ '(fotos de lejos, patrón del LED si se alcanza a ver) y déjalo anotado. Una revisión incompleta '
|
||||||
|
+ 'es aceptable; un accidente no.</div>' },
|
||||||
|
{ tipo:'texto', clave:'operador', rows:'1', txt:'¿Quién te acompañó o te dio acceso? (operador o supervisor)' },
|
||||||
|
] },
|
||||||
|
|
||||||
{ titulo: '1 · Por qué vienes a esta unidad',
|
{ titulo: '1 · Por qué vienes a esta unidad',
|
||||||
sub:'Contesta con lo que te dijeron, aunque suene vago. La versión del operador vale.',
|
sub:'Contesta con lo que te dijeron, aunque suene vago. La versión del operador vale.',
|
||||||
items: [
|
items: [
|
||||||
@ -199,6 +227,11 @@ Acta.render({
|
|||||||
{ tipo:'medida', clave:'tlm_rec', si:conUsb, txt:'Campo <code>rec</code> (veces que se ha tenido que recuperar)' },
|
{ tipo:'medida', clave:'tlm_rec', si:conUsb, txt:'Campo <code>rec</code> (veces que se ha tenido que recuperar)' },
|
||||||
{ tipo:'medida', clave:'tlm_vbat', si:conUsb, txt:'Campo <code>vbat</code> (voltaje de la batería)', u:'V' },
|
{ tipo:'medida', clave:'tlm_vbat', si:conUsb, txt:'Campo <code>vbat</code> (voltaje de la batería)', u:'V' },
|
||||||
{ tipo:'medida', clave:'tlm_veh', si:conUsb, txt:'Campo <code>veh</code> (1 = le llega corriente de la máquina, 0 = no)' },
|
{ tipo:'medida', clave:'tlm_veh', si:conUsb, txt:'Campo <code>veh</code> (1 = le llega corriente de la máquina, 0 = no)' },
|
||||||
|
{ tipo:'archivo', clave:'log_blackpill', etiqueta:'captura-blackpill',
|
||||||
|
si:v=>String(v.usb||'').startsWith('Sí, con archivo'), req:true,
|
||||||
|
txt:'Sube aquí el archivo de la captura del Blackpill',
|
||||||
|
ayuda:'El .txt o .log que guardó PuTTY / TeraTerm. Es el registro más valioso de la visita: '
|
||||||
|
+ 'súbelo aunque no hayas entendido lo que dice.' },
|
||||||
{ tipo:'foto', clave:'f_pantalla', etiqueta:'captura-usb', si:conUsb,
|
{ tipo:'foto', clave:'f_pantalla', etiqueta:'captura-usb', si:conUsb,
|
||||||
txt:'Foto de la pantalla de la laptop con las líneas visibles (respaldo por si falla el copiado).' },
|
txt:'Foto de la pantalla de la laptop con las líneas visibles (respaldo por si falla el copiado).' },
|
||||||
] },
|
] },
|
||||||
|
|||||||
@ -1,29 +1,35 @@
|
|||||||
/* Flota HBOX desplegada — registro único de unidades en campo.
|
/* Flota HBOX desplegada — registro único de unidades en campo.
|
||||||
Lo usan el acta de diagnóstico y (después) la de instalación.
|
Lo usan el acta de diagnóstico y (después) la de instalación.
|
||||||
|
|
||||||
|
En ICAVE nadie conoce el equipo por el número del HBOX: lo conocen por la MÁQUINA.
|
||||||
|
El permiso de acceso se pide por máquina y el técnico llega buscando una máquina.
|
||||||
|
Por eso la etiqueta empieza siempre por el número de máquina.
|
||||||
|
|
||||||
|
⚠ NO CONFUNDIR: estas unidades de campo son «RTK-006» (3 dígitos, generación v3
|
||||||
|
desplegada). Los módems 4G NTRIP Master del v5 son «RTK-0006» (4 dígitos) y viven
|
||||||
|
en el acta modulo-4g. Son cosas distintas y se guardan en carpetas distintas.
|
||||||
|
|
||||||
PARA AGREGAR O CORREGIR UNA UNIDAD: edita este archivo y vuelve a publicar el sitio.
|
PARA AGREGAR O CORREGIR UNA UNIDAD: edita este archivo y vuelve a publicar el sitio.
|
||||||
serie identificador corto y ESTABLE de la unidad (va en el acta y en el servidor).
|
serie número del HBOX tal como viene etiquetado en la caja.
|
||||||
Sin espacios ni acentos. Es la etiqueta física de la caja.
|
etiqueta lo que ve el técnico en la lista: MÁQUINA primero.
|
||||||
etiqueta lo que ve el técnico en la lista desplegable.
|
maquina número de máquina TAL COMO LO NOMBRA ICAVE (el del correo de acceso).
|
||||||
maquina equipo donde está montada, con el número del terminal.
|
|
||||||
terminal ICAVE · TIMSA · EIT · LCT (agrupa la lista)
|
terminal ICAVE · TIMSA · EIT · LCT (agrupa la lista)
|
||||||
estado operativa | falla | desmontada | repuesto
|
estado operativa | falla | desmontada | repuesto
|
||||||
nota cualquier cosa que el técnico deba saber ANTES de subirse a la máquina.
|
nota lo que el técnico debe saber ANTES de subirse a la máquina. */
|
||||||
|
|
||||||
⚠ Los datos de abajo están PENDIENTES DE CONFIRMAR por Leo (series y número de máquina). */
|
|
||||||
window.FLOTA = [
|
window.FLOTA = [
|
||||||
{ serie: 'HBOX-V3-RTG-020', etiqueta: 'RTG ZPM-020 — unidad desmontada', maquina: 'RTG ZPM 020',
|
{ serie: 'RTK-014', etiqueta: 'FL-JUN-060 — Montacargas · RTK-014 (sigue instalada)',
|
||||||
terminal: 'ICAVE', estado: 'desmontada',
|
maquina: 'FL-JUN-060', terminal: 'ICAVE', estado: 'falla',
|
||||||
nota: 'Desmontada del gabinete y de la máquina; está en el escritorio del taller. Diagnóstico en banco, no en campo.' },
|
nota: '⚠ SIGUE INSTALADA Y NADIE LA HA TOCADO — dejó de reportar el 17-sep y es la única evidencia intacta '
|
||||||
|
+ 'que tenemos. Acceso solicitado a ICAVE el 18-sep. Sigue el procedimiento en orden y NO le cortes la '
|
||||||
|
+ 'energía hasta terminar el paso 5.' },
|
||||||
|
|
||||||
{ serie: 'HBOX-V3-TRACTO-01', etiqueta: 'Tractocamión / tractoplana — unidad desmontada', maquina: 'Tractoplana (confirmar número)',
|
{ serie: 'RTK-003', etiqueta: 'RT-ZPMC-20 — RTG · RTK-003 (desmontada, en taller)',
|
||||||
terminal: 'ICAVE', estado: 'desmontada',
|
maquina: 'RT-ZPMC-20', terminal: 'ICAVE', estado: 'desmontada',
|
||||||
nota: 'Desmontada del gabinete y de la máquina; está en el escritorio del taller. Diagnóstico en banco, no en campo.' },
|
nota: 'Ya desmontada del gabinete y de la máquina; está sobre el escritorio del taller. '
|
||||||
|
+ 'No requiere permiso de acceso a la máquina. Revisión en banco, no en campo.' },
|
||||||
|
|
||||||
{ serie: 'HBOX-V3-MONTA-01', etiqueta: 'Montacargas — dejó de reportar (sigue instalada)', maquina: 'Montacargas (confirmar número)',
|
{ serie: 'RTK-006', etiqueta: 'HS-WES-62 — Tractoplana · RTK-006 (desmontada, en taller)',
|
||||||
terminal: 'ICAVE', estado: 'falla',
|
maquina: 'HS-WES-62', terminal: 'ICAVE', estado: 'desmontada',
|
||||||
nota: '⚠ SIGUE INSTALADA Y NADIE LA HA TOCADO. Es la única evidencia intacta que tenemos: sigue el procedimiento en orden y NO le cortes la energía hasta terminar el paso 5.' },
|
nota: 'Ya desmontada del gabinete y de la máquina; está sobre el escritorio del taller. '
|
||||||
|
+ 'No requiere permiso de acceso a la máquina. Revisión en banco, no en campo.' },
|
||||||
{ serie: 'HBOX-V3-RS-SAN-001', etiqueta: 'Reach Stacker RS-SAN-001', maquina: 'Reach Stacker RS-SAN-001',
|
|
||||||
terminal: 'ICAVE', estado: 'falla', nota: 'Confirmar si sigue instalada.' },
|
|
||||||
];
|
];
|
||||||
|
|||||||
144
docs/index.md
144
docs/index.md
@ -1,25 +1,135 @@
|
|||||||
|
---
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
# HBOX — Documentación técnica
|
# HBOX — Documentación técnica
|
||||||
|
|
||||||
Sitio interno de Helgen Technologies para técnicos de banco y colaboradores del proyecto
|
Sitio interno de Helgen Technologies. **Busca abajo lo que vas a hacer hoy.**
|
||||||
HBOX (EinarBoard). Aquí vive la copia publicada de los procedimientos; la fuente de verdad
|
|
||||||
es el repositorio **Drogheda** — no edites estas páginas a mano, se regeneran con el
|
|
||||||
script de sincronización.
|
|
||||||
|
|
||||||
## Empieza aquí
|
## 🔧 Trabajo de campo y taller
|
||||||
|
|
||||||
- **¿Vas a probar y flashear el lote v5 (15 tarjetas)?** → [EinarBoard v5 — Prueba y flasheo](procedimientos/v5-prueba-y-flasheo.md)
|
<div class="grid cards" markdown>
|
||||||
- **¿Armado de arneses o gabinete?** → [Pinout de arneses](montaje/pinout-arneses.md) y la [guía de ensamble](montaje/assembly-guide.md)
|
|
||||||
- **¿Una unidad en campo falla?** → [Protocolo de diagnóstico remoto](diagnostico/protocolo-diagnostico-remoto.md)
|
|
||||||
- **¿Vas a trabajar con los archivos de diseño (KiCad)?** → [Guía Git para colaboradores](git/guia-git-es.md)
|
|
||||||
|
|
||||||
## Reglas de la casa
|
- ### 🚜 Una unidad falla en la máquina
|
||||||
|
|
||||||
1. Los procedimientos se siguen **en orden y completos**; una tarjeta que falla una sección
|
|
||||||
no avanza a la siguiente.
|
|
||||||
2. Toda medición fuera de rango se anota con su **valor**, no solo "FALLA".
|
|
||||||
3. Si algo del documento no coincide con lo que ves en la tarjeta, **detente y pregunta**
|
|
||||||
a ingeniería antes de continuar.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Última sincronización: la fecha aparece en el pie de cada página. Contacto: leo@helgen.tech*
|
Diagnóstico paso a paso **en sitio**, con el gabinete todavía montado. Empieza por el
|
||||||
|
permiso de acceso y termina con la unidad clasificada.
|
||||||
|
|
||||||
|
[**▶ Llenar el acta de diagnóstico en campo**](formularios/diagnostico.html){ .md-button .md-button--primary }
|
||||||
|
|
||||||
|
[Leer el procedimiento primero](diagnostico/procedimiento-diagnostico-campo.md)
|
||||||
|
|
||||||
|
- ### 🔬 Una unidad ya está en la mesa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Diagnóstico **en banco** con fuente y multímetro: continuidad, rampa de corriente,
|
||||||
|
rieles y las tres capturas por USB.
|
||||||
|
|
||||||
|
[**▶ Llenar el acta de diagnóstico en banco**](formularios/banco.html){ .md-button .md-button--primary }
|
||||||
|
|
||||||
|
[Leer el procedimiento primero](procedimientos/diagnostico-banco.md)
|
||||||
|
|
||||||
|
- ### ⚡ Reflashear una unidad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Solo en taller y **solo después** de subir la captura del diagnóstico.
|
||||||
|
|
||||||
|
[Procedimiento de reflasheo](procedimientos/reflasheo-unidades.md)
|
||||||
|
|
||||||
|
- ### 📊 ¿Qué se ha revisado?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Todas las visitas, sus fotos y sus archivos, unidad por unidad.
|
||||||
|
|
||||||
|
[Tablero de campo](diagnostico/tablero-campo.html)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 🏭 Producción — lote v5
|
||||||
|
|
||||||
|
<div class="grid cards" markdown>
|
||||||
|
|
||||||
|
- ### 🔌 Probar y flashear tarjetas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Las 15 tarjetas EinarBoard v5, una por una.
|
||||||
|
|
||||||
|
[Procedimiento](procedimientos/v5-prueba-y-flasheo.md) · [Acta V5](formularios/v5.html)
|
||||||
|
|
||||||
|
- ### 📡 Módulos RTK y 4G
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
simpleRTK2B Pro, módem 4G NTRIP Master y la integración de la pareja.
|
||||||
|
|
||||||
|
[Procedimiento](procedimientos/v5-modulos-integracion.md) · [ArduSimple](formularios/ardusimple.html) · [4G](formularios/modem4g.html) · [Integración](formularios/integracion.html)
|
||||||
|
|
||||||
|
- ### 🧰 Ensamblar la unidad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Armado, energizado y prueba final antes de embarcar.
|
||||||
|
|
||||||
|
[Ensamble](procedimientos/v5-ensamble.md) · [Prueba final](procedimientos/v5-prueba-final-unidad.md) · [Acta de ensamble](formularios/ensamble.html)
|
||||||
|
|
||||||
|
- ### 🔩 Arneses y gabinete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Pinout de conectores, BOM dividido y guía de ensamble.
|
||||||
|
|
||||||
|
[Pinout](montaje/pinout-arneses.md) · [BOM](montaje/bom-split.md) · [Guía](montaje/assembly-guide.md)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 📥 Archivos y referencia
|
||||||
|
|
||||||
|
<div class="grid cards" markdown>
|
||||||
|
|
||||||
|
- ### 💾 Descargas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Firmware por generación de tarjeta, parámetros del módem 4G y configuración del ZED-F9P.
|
||||||
|
|
||||||
|
[Ir a Descargas](descargas/index.md)
|
||||||
|
|
||||||
|
- ### 📋 Todas las actas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Índice completo de formularios y cómo se usan.
|
||||||
|
|
||||||
|
[Índice de actas](actas/index.md) · [Tablero de producción](tablero.html)
|
||||||
|
|
||||||
|
- ### 🌿 Git y archivos de diseño
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Para quien toque KiCad o el repositorio.
|
||||||
|
|
||||||
|
[Guía en español](git/guia-git-es.md) · [English](git/git-guide-en.md)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reglas de la casa
|
||||||
|
|
||||||
|
1. Los procedimientos se siguen **en orden y completos**; una tarjeta que falla una sección no
|
||||||
|
avanza a la siguiente.
|
||||||
|
2. Toda medición fuera de rango se anota **con su valor**, no solo "FALLA".
|
||||||
|
3. En campo, **no reinicies ni desconectes nada** antes de capturar la evidencia: el estado en que
|
||||||
|
encontraste la unidad es el dato que no se puede recuperar después.
|
||||||
|
4. Si algo del documento no coincide con lo que ves en la tarjeta, **detente y pregunta** a
|
||||||
|
ingeniería antes de continuar.
|
||||||
|
|
||||||
|
*La fuente de verdad es el repositorio **Drogheda**: no edites estas páginas a mano, se regeneran
|
||||||
|
con el script de sincronización. Contacto: leo@helgen.tech*
|
||||||
|
|||||||
222
docs/procedimientos/diagnostico-banco.md
Normal file
222
docs/procedimientos/diagnostico-banco.md
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
# Diagnóstico en banco — unidad HBOX sobre la mesa
|
||||||
|
|
||||||
|
**Para:** Alan y Daniel · **Versión:** 1.0 · 18-sep-2026 · **Responsable:** Leo Rodríguez
|
||||||
|
|
||||||
|
Acta que se llena mientras trabajas:
|
||||||
|
**[▶ Abrir el acta de diagnóstico en banco](../formularios/banco.html)**
|
||||||
|
|
||||||
|
Aplica a las unidades que ya están desmontadas: **RTK-003** (RTG `RT-ZPMC-20`) y **RTK-006**
|
||||||
|
(tractoplana `HS-WES-62`). Para una unidad todavía montada en la máquina, el procedimiento es
|
||||||
|
otro: [Diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Qué podemos y qué ya no podemos saber
|
||||||
|
|
||||||
|
Sé honesto con lo que el banco puede dar, para no perseguir fantasmas.
|
||||||
|
|
||||||
|
**Ya se perdió** (y no se recupera): la temperatura máxima, la cuenta de recuperaciones y la causa
|
||||||
|
del último arranque. El firmware v3 guarda esos tres datos en memoria RAM, así que se borraron al
|
||||||
|
desconectar la unidad para desmontarla. Por eso en estas dos unidades **no vamos a poder probar la
|
||||||
|
hipótesis térmica con datos del propio equipo** — solo con lo que se vea físicamente.
|
||||||
|
|
||||||
|
**Sí podemos saber:** si hay un corto, cuánta corriente consume, si arranca, si los rieles están
|
||||||
|
donde deben, si el GNSS ve satélites, si el módem registra en Telcel, en qué estado está la
|
||||||
|
batería, y si el problema sigue después de reflashear.
|
||||||
|
|
||||||
|
> De ahí la regla que aplica a la unidad del montacargas (**RTK-014**, todavía instalada):
|
||||||
|
> primero la captura por USB en sitio, y hasta después se desmonta.
|
||||||
|
|
||||||
|
## 2. Equipo necesario
|
||||||
|
|
||||||
|
| Equipo | Para qué |
|
||||||
|
|---|---|
|
||||||
|
| **Fuente de banco con límite de corriente** | Energizar sin quemar nada y **leer el consumo** |
|
||||||
|
| Multímetro | Continuidad y rieles |
|
||||||
|
| Laptop + cables USB | Las tres capturas (Blackpill, 4G, ArduSimple) |
|
||||||
|
| Antena GNSS y 2 antenas LTE de prueba | Para que el receptor y el módem puedan funcionar |
|
||||||
|
| Cielo abierto o ventana | Sin vista al cielo el GNSS nunca dará fix y parecerá falla |
|
||||||
|
| Cámara | Fotos obligatorias del acta |
|
||||||
|
|
||||||
|
## 3. Seguridad
|
||||||
|
|
||||||
|
1. **Nunca energices antes del paso 5 (continuidad).** Si hay un corto, la fuente lo va a encontrar
|
||||||
|
quemando algo.
|
||||||
|
2. **Batería de litio:** si está hinchada, caliente, con fuga o huele, **desconéctala primero** y
|
||||||
|
trabaja sin ella. No la perfores, no la cargues, no la metas en un cajón. Foto y aviso.
|
||||||
|
3. Empieza siempre con el **límite de corriente bajo** (500 mA). Subirlo es fácil; deshacer un
|
||||||
|
componente quemado, no.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Paso 1 — Inspección sin energía
|
||||||
|
|
||||||
|
Con la unidad **desconectada de todo**, y antes de mover nada:
|
||||||
|
|
||||||
|
- [ ] Foto del exterior de la caja, de los cuatro lados.
|
||||||
|
- [ ] Abrir. **Foto del interior completo antes de tocar cables.**
|
||||||
|
- [ ] ¿Huele a quemado? ¿Hay partes oscurecidas, derretidas o abombadas?
|
||||||
|
- [ ] ¿Hay agua, marcas de agua, sarro blanco o **corrosión verde** en conectores?
|
||||||
|
- [ ] Batería: ¿hinchada, caliente, con manchas o fuga? Foto de los dos lados.
|
||||||
|
- [ ] Prueba de meneo suave en cada conector, Dupont y SMA: ¿alguno se mueve o se sale?
|
||||||
|
- [ ] Conectores de antena: ¿el centro del SMA está doblado o falta?
|
||||||
|
|
||||||
|
Cualquier hallazgo aquí ya es un resultado: fotografíalo y anótalo aunque después todo encienda.
|
||||||
|
|
||||||
|
## 5. Paso 2 — Continuidad a tierra, **antes** de energizar
|
||||||
|
|
||||||
|
Multímetro en ohms, **punta negra en el negativo/chasis**. La lectura sube lentamente (los
|
||||||
|
capacitores se están cargando con la pila del multímetro): eso es normal, espera a que se estabilice.
|
||||||
|
|
||||||
|
| Punto | Esperado | Lectura mala |
|
||||||
|
|---|---|---|
|
||||||
|
| Entrada 24 V (pines 1-2 del GX16) | **> 10 kΩ** | Fija en 0–20 Ω = **corto: no energizar** |
|
||||||
|
| Riel de 5 V (salida de la Mean Well) | **> 1 kΩ** | Fija en 0–20 Ω = corto |
|
||||||
|
| Riel de 3.3 V | **> 1 kΩ** | Fija en 0–20 Ω = corto |
|
||||||
|
| Alimentación del simpleRTK2B (riel conmutado) | **> 1 kΩ** | Fija en 0–20 Ω = corto |
|
||||||
|
| Batería (bornes, con la batería **desconectada**) | — | Anota su voltaje en vacío |
|
||||||
|
|
||||||
|
!!! danger "Si encuentras un corto"
|
||||||
|
**No energices.** Anótalo, fotografía la zona, marca la unidad como *corto a tierra* y déjala
|
||||||
|
aparte. Energizar un corto convierte una reparación de un componente en una tarjeta perdida.
|
||||||
|
|
||||||
|
## 6. Paso 3 — Energizado con rampa de corriente
|
||||||
|
|
||||||
|
Este paso es el que más información da por minuto invertido.
|
||||||
|
|
||||||
|
1. Fuente a **24.0 V**, límite de corriente en **500 mA**, salida **apagada**.
|
||||||
|
2. Conecta solo la entrada de 24 V (sin antenas todavía).
|
||||||
|
3. Enciende la salida y **mira el amperímetro en los primeros 3 segundos**.
|
||||||
|
|
||||||
|
| Lo que hace la corriente | Qué significa | Qué haces |
|
||||||
|
|---|---|---|
|
||||||
|
| **0 mA**, la fuente no entrega nada | Circuito abierto: fusible, conector o etapa de entrada muerta | Revisa fusible y cableado de entrada |
|
||||||
|
| **Se pega al límite** (500 mA) y el voltaje se cae | **Corto** | **Apaga de inmediato.** Regresa al paso 2 |
|
||||||
|
| **50–150 mA** estables, sin LED | Vive el MCU pero la etapa RTK no enciende | Sigue al paso 4 y a la captura del Blackpill |
|
||||||
|
| **150–400 mA** y el LED parpadea | Arranca normal | Sigue al paso 4 |
|
||||||
|
| Sube despacio y **algo se calienta** | Componente en corto parcial | Apaga y busca con el dorso de la mano qué quema |
|
||||||
|
|
||||||
|
Anota **el pico de arranque** y **la corriente estable a los 60 segundos**. Son dos números
|
||||||
|
distintos y los dos importan.
|
||||||
|
|
||||||
|
## 7. Paso 4 — Rieles
|
||||||
|
|
||||||
|
Con la unidad energizada y estable:
|
||||||
|
|
||||||
|
| Punto | Esperado |
|
||||||
|
|---|---|
|
||||||
|
| Entrada 24 V | 23 – 26 V |
|
||||||
|
| Riel 5 V (salida Mean Well) | 4.8 – 5.2 V |
|
||||||
|
| Riel 3.3 V | 3.20 – 3.40 V |
|
||||||
|
| Alimentación del simpleRTK2B **al arrancar** | **0 V** — el firmware enciende la etapa RTK solo cuando decide que es seguro |
|
||||||
|
| Batería de respaldo | 3.0 – 4.2 V |
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
5 V en la etapa RTK **en el instante de arrancar** es un defecto, no una buena señal: el
|
||||||
|
invariante del producto dice *apagado es el estado seguro*. Si aparece energía ahí antes de que
|
||||||
|
el MCU la pida, anótalo.
|
||||||
|
|
||||||
|
## 8. Paso 5 — Captura del Blackpill (la principal)
|
||||||
|
|
||||||
|
1. Conecta el **USB del Blackpill** a la laptop.
|
||||||
|
2. Abre el puerto serie nuevo a **115200 baudios** (PuTTY, TeraTerm o el monitor de Arduino).
|
||||||
|
3. Activa **guardar a archivo**.
|
||||||
|
4. Deja correr **3 minutos**. Luego presiona **RESET** y graba también el arranque.
|
||||||
|
5. Guarda el archivo como `RTK-0NN_blackpill_AAAA-MM-DD.log` y **súbelo en el acta**.
|
||||||
|
|
||||||
|
Qué vas a ver (firmware v3.0.x):
|
||||||
|
|
||||||
|
```
|
||||||
|
BOOT,1240,fw=3.0.3,reset=PIN
|
||||||
|
COLS,ms,state,link,fix,sat,rx,rate,vbat,v5v,veh,ign,tc,tmax,der,rec,limp,…
|
||||||
|
TLM,182000,RUN,DEGR,1,11,4210,1HZ,3.92,5.02,1,1,58,81,0,3,1,900,…
|
||||||
|
```
|
||||||
|
|
||||||
|
| Campo | Léelo así |
|
||||||
|
|---|---|
|
||||||
|
| `state` | `RUN` trabajando · `BOOT` arrancando · `SLEEP` dormida · `PWRCYC` reiniciando la etapa RTK |
|
||||||
|
| `link` | `OK` con correcciones · `DEGR` intermitentes · `LIMP` casi sin ellas · `SRCH` buscando |
|
||||||
|
| `fix` | 4 o 5 = RTK · 1 = GPS solo · 0 = sin posición |
|
||||||
|
| `vbat` / `v5v` | Voltajes que mide el propio equipo — compáralos con tu multímetro |
|
||||||
|
| `veh` / `ign` | 1 = presente. En banco, `veh` debe ser 1 y `ign` 0 si no conectaste ignición |
|
||||||
|
| `tc` / `tmax` | Temperatura actual y máxima **desde este arranque** |
|
||||||
|
| `rec` / `limp` | Recuperaciones y si quedó enganchada en modo limitado |
|
||||||
|
|
||||||
|
Si en vez de estas líneas ves un tablero que se redibuja, la unidad trae otro firmware: **anótalo y
|
||||||
|
avísale a ingeniería antes de seguir**.
|
||||||
|
|
||||||
|
## 9. Paso 6 — Captura del módem 4G NTRIP Master
|
||||||
|
|
||||||
|
El módem se configura y se escucha por el **puerto USB "XBEE"** del simpleRTK2B Pro (el módulo 4G va
|
||||||
|
montado sobre el receptor y ese USB es su consola).
|
||||||
|
|
||||||
|
1. **Sin energía**, verifica que el módulo 4G esté bien asentado en el zócalo XBee y que las
|
||||||
|
**dos antenas LTE** estén conectadas.
|
||||||
|
2. Conecta el USB **POWER+GPS** (alimenta todo) **y** el USB **XBEE** (consola del módem).
|
||||||
|
3. Terminal serie en el puerto **XBEE** a **115200 baudios**, con **guardar a archivo**.
|
||||||
|
4. Deja grabar **3 minutos** desde el arranque, sin escribir nada. Anota qué hace el **LED de red**
|
||||||
|
del módulo: parpadeo rápido = buscando; lento o fijo = registrado.
|
||||||
|
5. Para leer su configuración: `$$$AS+I=1111` entra a modo configuración. Anota el **ID de estación**
|
||||||
|
(`AS+K`) y el mountpoint.
|
||||||
|
6. Guarda como `RTK-0NN_modem4g_AAAA-MM-DD.log` y súbelo al acta.
|
||||||
|
|
||||||
|
!!! warning "Esta consola miente por omisión"
|
||||||
|
Una unidad **sana** imprime en esta consola prácticamente lo mismo que una **averiada**: unas
|
||||||
|
pocas líneas de registro y nada más. Ya nos pasó — en junio dimos por muerto un módem sano
|
||||||
|
leyendo solo esta consola. **La consola del 4G no prueba que las correcciones estén llegando ni
|
||||||
|
que los datos se estén enviando.** Eso solo se confirma con el `link` del Blackpill, con un fix
|
||||||
|
≥4 en el ArduSimple, o viendo el tráfico en el servidor.
|
||||||
|
|
||||||
|
Úsala para lo que sí demuestra: que el módem enciende, que registra en Telcel y con qué
|
||||||
|
configuración quedó.
|
||||||
|
|
||||||
|
## 10. Paso 7 — Captura del simpleRTK2B Pro (ArduSimple)
|
||||||
|
|
||||||
|
1. Antena GNSS conectada y **con vista al cielo** (ventana abierta o afuera; dentro del taller no
|
||||||
|
habrá fix y parecerá falla).
|
||||||
|
2. Conecta el USB **POWER+GPS** a la laptop.
|
||||||
|
3. **Opción rápida (terminal):** abre el puerto serie a **115200 baudios** y graba 3 minutos de
|
||||||
|
NMEA. Guarda como `RTK-0NN_ardusimple_AAAA-MM-DD.nmea`.
|
||||||
|
4. **Opción completa (u-center):** *Receiver → Connect*, y revisa:
|
||||||
|
- **UBX-MON-VER** → versión de firmware del ZED-F9P (anótala en el acta).
|
||||||
|
- **Fix Mode** → debe llegar a **3D** en menos de 2 minutos a cielo abierto.
|
||||||
|
- Número de satélites y su nivel de señal.
|
||||||
|
- Si hay correcciones llegando, el fix sube a **3D/DGNSS/FIXED**.
|
||||||
|
|
||||||
|
Cómo leer el NMEA sin u-center: busca líneas `$GNGGA` y mira el **sexto campo** (calidad):
|
||||||
|
|
||||||
|
| Calidad | Significa |
|
||||||
|
|---|---|
|
||||||
|
| `0` | Sin posición |
|
||||||
|
| `1` | GPS normal, **sin correcciones** |
|
||||||
|
| `2` | Corregido básico |
|
||||||
|
| **`4`** | **RTK fijo** — es el objetivo |
|
||||||
|
| **`5`** | RTK flotante — casi |
|
||||||
|
|
||||||
|
Un receptor que da `1` con buenos satélites **no está roto**: está diciendo que las correcciones no
|
||||||
|
le llegan, y eso apunta al módem o al servidor, no al GNSS.
|
||||||
|
|
||||||
|
## 11. Paso 8 — Batería
|
||||||
|
|
||||||
|
| Medida | Esperado | Lectura mala |
|
||||||
|
|---|---|---|
|
||||||
|
| Voltaje en vacío (desconectada) | 3.0 – 4.2 V | < 2.5 V = celda muy descargada o dañada |
|
||||||
|
| Voltaje con la unidad encendida | Sube lentamente si está cargando | Baja = no está cargando |
|
||||||
|
| Aspecto | Plana, sin marcas | Hinchada, con fuga o caliente = **fuera de servicio** |
|
||||||
|
|
||||||
|
Una batería hinchada **no se reinstala, no se carga y no viaja en avión**. Sepárala, foto, aviso.
|
||||||
|
|
||||||
|
## 12. Paso 9 — Veredicto
|
||||||
|
|
||||||
|
| Hallazgo | Disposición |
|
||||||
|
|---|---|
|
||||||
|
| Corto a tierra | No reparar en sitio → enviar a Querétaro |
|
||||||
|
| No enciende, sin corto | Enviar a Querétaro |
|
||||||
|
| Enciende y todo normal en banco | Reflashear ([procedimiento](reflasheo-unidades.md)) y volver a probar |
|
||||||
|
| Enciende pero el módem no registra | Probar la SIM en otro módulo antes de culpar al módem |
|
||||||
|
| GNSS con fix `1` y módem registrado | Revisar servidor/caster — no es hardware |
|
||||||
|
| Batería dañada | Retirar la batería, anotarlo, y decidir con ingeniería si se reinstala sin ella |
|
||||||
|
| Daño por calor visible | Enviar a Querétaro **con las fotos** — es evidencia del rediseño térmico |
|
||||||
|
|
||||||
|
Al terminar, **sube al acta los tres archivos de captura** (Blackpill, 4G y ArduSimple). Son el
|
||||||
|
insumo del análisis en Querétaro: sin ellos, el diagnóstico se queda en tu memoria.
|
||||||
143
docs/procedimientos/reflasheo-unidades.md
Normal file
143
docs/procedimientos/reflasheo-unidades.md
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
# Reflasheo de unidades HBOX — solo en taller
|
||||||
|
|
||||||
|
**Para:** Alan y Daniel (ejecución) · ingeniería (generación del binario)
|
||||||
|
**Versión:** 1.0 · 18-sep-2026 · **Responsable:** Leo Rodríguez
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Cuándo se reflashea y cuándo no
|
||||||
|
|
||||||
|
Reflashear es una **prueba diagnóstica**, no una reparación: sirve para contestar *«¿el problema
|
||||||
|
desaparece con firmware nuevo?»*. Si la unidad tiene un corto, un conector roto o una batería
|
||||||
|
hinchada, el firmware no va a arreglar nada.
|
||||||
|
|
||||||
|
!!! danger "Dos reglas que no se rompen"
|
||||||
|
**1 · Solo en taller, nunca sobre la máquina.** Un flasheo interrumpido deja la unidad muerta.
|
||||||
|
Si eso pasa en la mesa, se vuelve a intentar; si pasa arriba de un RTG, hay que bajarla otra vez.
|
||||||
|
|
||||||
|
**2 · Primero la evidencia, después el flasheo.** El flasheo borra la memoria del equipo
|
||||||
|
(temperatura máxima, recuperaciones, causa del último arranque) y sustituye el firmware que
|
||||||
|
estaba fallando. **Haz y sube las capturas del diagnóstico antes de tocar el botón.** El acta de
|
||||||
|
banco bloquea este paso hasta que el archivo de captura esté subido.
|
||||||
|
|
||||||
|
| Situación | ¿Reflashear? |
|
||||||
|
|---|---|
|
||||||
|
| Ya hiciste el diagnóstico y las capturas están subidas | **Sí** |
|
||||||
|
| La unidad tiene corto, humedad o daño visible | No — enviar a Querétaro |
|
||||||
|
| No sabes qué firmware trae | Primero anótalo (línea `BOOT` de la captura) |
|
||||||
|
| La unidad está instalada en la máquina | **No.** Se desmonta primero |
|
||||||
|
| No encuentras el `.bin` correcto en Descargas | No inventes: pregunta a ingeniería |
|
||||||
|
|
||||||
|
## 2. Qué binario le toca a cada unidad
|
||||||
|
|
||||||
|
Los binarios viven en **[Descargas](../descargas/index.md)**, con su versión y su SHA256. Cada
|
||||||
|
generación de tarjeta tiene el suyo y **no son intercambiables**: la polaridad del control de carga
|
||||||
|
está invertida entre generaciones, así que un binario equivocado puede dejar la batería cargando
|
||||||
|
cuando debería estar cortada.
|
||||||
|
|
||||||
|
| Generación de tarjeta | Binario | Estado |
|
||||||
|
|---|---|---|
|
||||||
|
| **V3** (flota desplegada: RTK-003, RTK-006, RTK-014) | `hbox-fw-v3_*.bin` | ⬜ pendiente de publicar |
|
||||||
|
| **V4** | `hbox-fw-v4_*.bin` | ⬜ pendiente de publicar |
|
||||||
|
| **V5** (EinarBoard v5, banco) | `hbox-v5-bringup_0.4.0.bin` | ✅ publicado |
|
||||||
|
|
||||||
|
Si el renglón dice *pendiente*, **no hay binario aprobado todavía**: el flasheo de esa generación
|
||||||
|
está detenido hasta que ingeniería lo publique (§6).
|
||||||
|
|
||||||
|
## 3. Qué necesitas
|
||||||
|
|
||||||
|
- Laptop con **STM32CubeProgrammer** (o `dfu-util`).
|
||||||
|
- Cable **USB con datos** (muchos cables de carga no llevan datos — si la laptop no ve nada, prueba otro).
|
||||||
|
- El `.bin` descargado del sitio, con su SHA256 verificado.
|
||||||
|
- La unidad **en la mesa**, energizada solo por el USB.
|
||||||
|
|
||||||
|
Verifica el archivo antes de flashear:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS / Linux
|
||||||
|
shasum -a 256 hbox-fw-v3_X.Y.Z.bin
|
||||||
|
# Windows (PowerShell)
|
||||||
|
Get-FileHash .\hbox-fw-v3_X.Y.Z.bin -Algorithm SHA256
|
||||||
|
```
|
||||||
|
|
||||||
|
Debe coincidir **carácter por carácter** con el que publica `SHA256SUMS.txt`. Si no coincide,
|
||||||
|
descárgalo otra vez; si sigue sin coincidir, no lo uses y avisa.
|
||||||
|
|
||||||
|
## 4. El flasheo, paso a paso
|
||||||
|
|
||||||
|
1. **Anota el firmware que traía.** De la captura del diagnóstico, la línea `BOOT,…,fw=3.0.3,…`.
|
||||||
|
Va en el acta. Sin este dato el reflasheo no sirve como prueba.
|
||||||
|
2. Conecta el **USB del Blackpill** a la laptop.
|
||||||
|
3. **Entra a modo DFU:**
|
||||||
|
- Mantén presionado **BOOT0**.
|
||||||
|
- Presiona y suelta **RESET (NRST)**.
|
||||||
|
- Suelta **BOOT0**.
|
||||||
|
- En la laptop debe aparecer **«STM32 BOOTLOADER»** (identificador `0483:DF11`).
|
||||||
|
Si no aparece: repite la secuencia; si sigue sin salir, el cable no lleva datos o el MCU no arranca.
|
||||||
|
4. **Graba el binario** en la dirección **`0x08000000`**:
|
||||||
|
- *STM32CubeProgrammer* → conectar en **USB/DFU** → *Open file* → *Download*.
|
||||||
|
- o bien: `dfu-util -a 0 -s 0x08000000:leave -D hbox-fw-v3_X.Y.Z.bin`
|
||||||
|
5. Espera a **«Download complete»** y **«Verify successful»**. Si falla a la mitad, **no desconectes**:
|
||||||
|
repite el paso 3 y vuelve a grabar.
|
||||||
|
6. Presiona **RESET**.
|
||||||
|
|
||||||
|
## 5. Verificación — el flasheo no termina hasta aquí
|
||||||
|
|
||||||
|
1. Abre el puerto serie a **115200 baudios**, con **guardar a archivo**.
|
||||||
|
2. Presiona RESET otra vez y captura el arranque.
|
||||||
|
3. Comprueba:
|
||||||
|
|
||||||
|
- [ ] Aparece la línea `BOOT` con la **versión nueva** en `fw=`.
|
||||||
|
- [ ] El LED vuelve a parpadear (no se queda apagado ni en parpadeo rápido permanente).
|
||||||
|
- [ ] Empiezan a salir líneas `TLM,` cada pocos segundos.
|
||||||
|
- [ ] Ningún `EVT` de falla repitiéndose sin parar.
|
||||||
|
|
||||||
|
4. Deja correr **10 minutos** con antenas conectadas y vuelve a mirar: `state` debe llegar a `RUN`,
|
||||||
|
y `link` debe salir de `SRCH`.
|
||||||
|
5. **Sube esta captura al acta**, junto con la de antes del flasheo. Las dos juntas son la prueba de
|
||||||
|
si el firmware cambió algo.
|
||||||
|
|
||||||
|
| Resultado | Qué sigue |
|
||||||
|
|---|---|
|
||||||
|
| Arranca y se estabiliza en `RUN` | Candidata a reinstalar — coordina con ICAVE |
|
||||||
|
| Arranca pero repite la misma falla | El firmware no era la causa → enviar a Querétaro |
|
||||||
|
| Ya no arranca después del flasheo | Repite DFU con otro cable; si sigue, enviar a Querétaro |
|
||||||
|
|
||||||
|
## 6. Para ingeniería — cómo se genera el binario
|
||||||
|
|
||||||
|
*(Esta sección no es para el técnico: es para quien publica el `.bin`.)*
|
||||||
|
|
||||||
|
Hoy el repositorio **no contiene binarios compilados de V3 ni de V4** — solo el de bring-up del V5,
|
||||||
|
que sí está publicado y versionado. Los `.bin` de firmware están en `.gitignore` a propósito. Para
|
||||||
|
que el procedimiento de arriba funcione, cada generación necesita un binario **compilado, probado en
|
||||||
|
banco y publicado con su SHA256**.
|
||||||
|
|
||||||
|
Fuentes: `04-Source-Code/firmware/blackpillrtkcode_v3/` y `…_v4/`.
|
||||||
|
|
||||||
|
Compilación reproducible con `arduino-cli` (verifica el FQBN contra tu instalación del core
|
||||||
|
STM32duino antes de darlo por bueno):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
arduino-cli core install STMicroelectronics:stm32
|
||||||
|
arduino-cli compile \
|
||||||
|
--fqbn STMicroelectronics:stm32:GenF4:pnum=BLACKPILL_F411CE \
|
||||||
|
--build-property "compiler.cpp.extra_flags=-DTARGET_EINARBOARD=1" \
|
||||||
|
--output-dir release/ \
|
||||||
|
blackpillrtkcode_v3/
|
||||||
|
shasum -a 256 release/*.bin >> release/SHA256SUMS
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning "El flag de destino de tarjeta no es cosmético"
|
||||||
|
`TARGET_EINARBOARD` y `TARGET_PROTO` **invierten la polaridad del control de carga** (en
|
||||||
|
producción `CHRG_EN` en alto = carga **apagada**; en protoboard `CHRG_RUN` en alto = carga
|
||||||
|
**encendida**). Compilar con el destino equivocado puede dejar la batería cargando justo cuando
|
||||||
|
el firmware cree que la está protegiendo por temperatura — precisamente la falla #1 del RCA.
|
||||||
|
|
||||||
|
Antes de publicar:
|
||||||
|
|
||||||
|
- [ ] Compilado con el `TARGET_` correcto para la generación de tarjeta.
|
||||||
|
- [ ] **Probado en banco en una unidad real**, no solo compilado.
|
||||||
|
- [ ] Verificada la polaridad del control de carga sobre hardware.
|
||||||
|
- [ ] Publicado en `docs/descargas/` con versión en el nombre y su línea en `SHA256SUMS.txt`.
|
||||||
|
- [ ] Anotado en el `CHANGELOG.md` del repositorio de firmware, con la fecha.
|
||||||
|
- [ ] Actualizada la tabla de §2 de este documento.
|
||||||
79
mkdocs.yml
79
mkdocs.yml
@ -20,7 +20,8 @@ theme:
|
|||||||
icon: material/weather-sunny
|
icon: material/weather-sunny
|
||||||
name: Modo claro
|
name: Modo claro
|
||||||
features:
|
features:
|
||||||
- navigation.sections
|
- navigation.tracking
|
||||||
|
- navigation.indexes
|
||||||
- navigation.top
|
- navigation.top
|
||||||
- search.highlight
|
- search.highlight
|
||||||
- content.code.copy
|
- content.code.copy
|
||||||
@ -39,41 +40,53 @@ markdown_extensions:
|
|||||||
permalink: true
|
permalink: true
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
- Inicio: index.md
|
- "🏠 Inicio": index.md
|
||||||
- "Tablero de estado": tablero.html
|
|
||||||
- Actas y control de calidad:
|
- "🚜 Campo — la unidad está en la máquina":
|
||||||
- "Índice de actas": actas/index.md
|
- "Cómo se diagnostica en campo": diagnostico/procedimiento-diagnostico-campo.md
|
||||||
- "Acta de diagnóstico en campo": formularios/diagnostico.html
|
- "▶ Acta de diagnóstico en campo": formularios/diagnostico.html
|
||||||
- "Acta de prueba V5 (tarjeta)": formularios/v5.html
|
- "Tablero de campo (qué se ha revisado)": diagnostico/tablero-campo.html
|
||||||
- "Acta de módulo ArduSimple (simpleRTK2B, ARD-NNN)": formularios/ardusimple.html
|
|
||||||
- "Acta de módem 4G (NTRIP Master, RTK-NNNN)": formularios/modem4g.html
|
- "🔬 Taller — la unidad está en la mesa":
|
||||||
- "Acta de integración ArduSimple + 4G": formularios/integracion.html
|
- "Cómo se diagnostica en banco": procedimientos/diagnostico-banco.md
|
||||||
- "Acta de ensamble (unidad HBOX v5)": formularios/ensamble.html
|
- "▶ Acta de diagnóstico en banco": formularios/banco.html
|
||||||
- "Acta de prueba V4 (borrador)": formularios/v4.html
|
- "⚡ Reflasheo de unidades": procedimientos/reflasheo-unidades.md
|
||||||
- Procedimientos de banco:
|
|
||||||
- "EinarBoard v5 — Prueba y flasheo (lote 15 pzas)": procedimientos/v5-prueba-y-flasheo.md
|
- "🏭 Producción — lote v5":
|
||||||
- "HBOX v5 — Módulos RTK/4G e integración (antes del ensamble)": procedimientos/v5-modulos-integracion.md
|
- "1 · Prueba y flasheo de tarjetas": procedimientos/v5-prueba-y-flasheo.md
|
||||||
- "HBOX v5 — Ensamble de la unidad y energizado final": procedimientos/v5-ensamble.md
|
- "▶ Acta de tarjeta V5": formularios/v5.html
|
||||||
- "HBOX v5 — Prueba final de unidad (fw 0.4.0, antes de embarcar)": procedimientos/v5-prueba-final-unidad.md
|
- "2 · Módulos RTK y 4G": procedimientos/v5-modulos-integracion.md
|
||||||
- "EinarBoard 1.0 — Bring-up runbook": procedimientos/einarboard-1.0-bringup.md
|
- "▶ Acta ArduSimple": formularios/ardusimple.html
|
||||||
- "FW v3.0.3 — Prueba de banco (política de ventilador)": procedimientos/fan-policy-bench-test.md
|
- "▶ Acta módem 4G": formularios/modem4g.html
|
||||||
- "Histórico — Proto bench v3": procedimientos/historico-proto-bench-v3.md
|
- "▶ Acta de integración": formularios/integracion.html
|
||||||
- "Histórico — Protoboard": procedimientos/historico-protoboard.md
|
- "3 · Ensamble de la unidad": procedimientos/v5-ensamble.md
|
||||||
- Montaje y arneses:
|
- "▶ Acta de ensamble": formularios/ensamble.html
|
||||||
|
- "4 · Prueba final antes de embarcar": procedimientos/v5-prueba-final-unidad.md
|
||||||
|
- "Tablero de producción": tablero.html
|
||||||
|
|
||||||
|
- "🔩 Montaje y arneses":
|
||||||
- "Pinout de arneses y conectores": montaje/pinout-arneses.md
|
- "Pinout de arneses y conectores": montaje/pinout-arneses.md
|
||||||
- "BOM dividido para ensamble": montaje/bom-split.md
|
- "BOM dividido para ensamble": montaje/bom-split.md
|
||||||
- "Guía de ensamble y control de calidad": montaje/guia-ensamble-qc.md
|
- "Guía de ensamble y control de calidad": montaje/guia-ensamble-qc.md
|
||||||
- "Guía de ensamble (PDF)": montaje/assembly-guide.md
|
- "Guía de ensamble (PDF)": montaje/assembly-guide.md
|
||||||
- Diagnóstico:
|
|
||||||
- "⚑ Diagnóstico en campo — procedimiento": diagnostico/procedimiento-diagnostico-campo.md
|
- "📥 Descargas":
|
||||||
- "⚑ Acta de diagnóstico en campo (llenar en la máquina)": formularios/diagnostico.html
|
- "Firmware, parámetros 4G y ZED-F9P": descargas/index.md
|
||||||
- "Tablero de campo (ingeniería)": diagnostico/tablero-campo.html
|
- "README firmware bring-up v5": descargas/hbox-v5-bringup-README.md
|
||||||
- "Protocolo de diagnóstico remoto": diagnostico/protocolo-diagnostico-remoto.md
|
|
||||||
- Git:
|
- "📋 Actas — índice y cómo se usan": actas/index.md
|
||||||
|
|
||||||
|
- "🌿 Git y archivos de diseño":
|
||||||
- "Guía Git (español)": git/guia-git-es.md
|
- "Guía Git (español)": git/guia-git-es.md
|
||||||
- "Git guide (English)": git/git-guide-en.md
|
- "Git guide (English)": git/git-guide-en.md
|
||||||
- Descargas:
|
|
||||||
- "Firmware y herramientas": descargas/index.md
|
- "📚 Referencia e histórico":
|
||||||
- "README firmware bring-up v5": descargas/hbox-v5-bringup-README.md
|
- "Protocolo de diagnóstico remoto (2026-06)": diagnostico/protocolo-diagnostico-remoto.md
|
||||||
- "Administración (Leo)": admin.html
|
- "EinarBoard 1.0 — Bring-up runbook": procedimientos/einarboard-1.0-bringup.md
|
||||||
- "Salir": salir.html
|
- "FW v3.0.3 — Prueba de banco (ventilador)": procedimientos/fan-policy-bench-test.md
|
||||||
|
- "Acta de prueba V4 (borrador)": formularios/v4.html
|
||||||
|
- "Histórico — Proto bench v3": procedimientos/historico-proto-bench-v3.md
|
||||||
|
- "Histórico — Protoboard": procedimientos/historico-protoboard.md
|
||||||
|
|
||||||
|
- "⚙ Administración (Leo)": admin.html
|
||||||
|
- "🚪 Salir": salir.html
|
||||||
|
|||||||
@ -11,4 +11,14 @@ PORT="${SSH_PORT:-22}"
|
|||||||
|
|
||||||
mkdocs build --strict
|
mkdocs build --strict
|
||||||
rsync -az --delete -e "ssh -p $PORT" site/ "$REMOTE:$REMOTE_PATH/"
|
rsync -az --delete -e "ssh -p $PORT" site/ "$REMOTE:$REMOTE_PATH/"
|
||||||
echo "Publicado en $REMOTE:$REMOTE_PATH (puerto $PORT)"
|
echo "Sitio publicado en $REMOTE:$REMOTE_PATH (puerto $PORT)"
|
||||||
|
|
||||||
|
# El backend NO vive en git en el servidor: se copia igual que el sitio.
|
||||||
|
# Sin --delete (ahí junto al app.py viven el venv y __pycache__).
|
||||||
|
BACKEND_PATH="$(dirname "$REMOTE_PATH")/backend"
|
||||||
|
rsync -az -e "ssh -p $PORT" backend/app.py backend/requirements.txt \
|
||||||
|
backend/parche-nginx-api.py backend/actas-hbox.service "$REMOTE:$BACKEND_PATH/"
|
||||||
|
echo "Backend copiado en $REMOTE:$BACKEND_PATH"
|
||||||
|
echo
|
||||||
|
echo "FALTA reiniciar el servicio (pide sudo, por eso no se hace aquí):"
|
||||||
|
echo " ssh $REMOTE 'sudo systemctl restart actas-hbox'"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user