From 86af91fe4bc993e536b8b3a060187d1d3f72e40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leopoldo=20Rodr=C3=ADguez?= Date: Fri, 18 Sep 2026 11:27:40 -0600 Subject: [PATCH] diagnostico en campo y banco: actas con fotos y logs, reflasheo, portada e indice --- DESPLIEGUE.md | 5 +- backend/actas-hbox.service | 5 + backend/app.py | 72 +++- backend/app.py.bak | 229 ----------- backend/parche-nginx-api.py | 91 +++++ .../4G_NTRIP_HelgenSRV-NMEA.example.txt | 59 +++ docs/descargas/index.md | 61 +++ .../procedimiento-diagnostico-campo.md | 82 +++- docs/diagnostico/tablero-campo.html | 21 +- docs/formularios/acta.css | 9 + docs/formularios/acta.js | 82 +++- docs/formularios/acta.js.bak | 381 ------------------ docs/formularios/banco.html | 202 ++++++++++ docs/formularios/diagnostico.html | 33 ++ docs/formularios/flota.js | 44 +- docs/index.md | 142 ++++++- docs/procedimientos/diagnostico-banco.md | 222 ++++++++++ docs/procedimientos/reflasheo-unidades.md | 143 +++++++ mkdocs.yml | 79 ++-- scripts/deploy.sh | 12 +- 20 files changed, 1268 insertions(+), 706 deletions(-) delete mode 100644 backend/app.py.bak create mode 100644 backend/parche-nginx-api.py create mode 100644 docs/descargas/4G_NTRIP_HelgenSRV-NMEA.example.txt delete mode 100644 docs/formularios/acta.js.bak create mode 100644 docs/formularios/banco.html create mode 100644 docs/procedimientos/diagnostico-banco.md create mode 100644 docs/procedimientos/reflasheo-unidades.md diff --git a/DESPLIEGUE.md b/DESPLIEGUE.md index 9a34f52..d839fce 100644 --- a/DESPLIEGUE.md +++ b/DESPLIEGUE.md @@ -53,6 +53,7 @@ El backend la deduce sola de `ACTAS_DIR`; si prefieres fijarla, agrega a ```ini Environment=ADJUNTOS_DIR=/srv/projects/drogheda/adjuntos Environment=ADJUNTO_MAX_MB=8 +Environment=ARCHIVO_MAX_MB=20 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 X-Forwarded-For $proxy_add_x_forwarded_for; 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 } ``` 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 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 diff --git a/backend/actas-hbox.service b/backend/actas-hbox.service index 93b2c4a..d826880 100644 --- a/backend/actas-hbox.service +++ b/backend/actas-hbox.service @@ -7,6 +7,11 @@ User=leo Group=leo WorkingDirectory=/srv/projects/drogheda/hbox-docs/backend 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 ExecStart=/srv/projects/drogheda/hbox-docs/backend/venv/bin/gunicorn -w 2 -b 127.0.0.1:8081 app:app Restart=on-failure diff --git a/backend/app.py b/backend/app.py index 2f723df..91733f8 100644 --- a/backend/app.py +++ b/backend/app.py @@ -40,8 +40,12 @@ TIPOS_REPETIBLES = {t.strip() for t in os.environ.get( # Subida de fotos 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"} +# 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_FILE = re.compile(r"[^A-Za-z0-9_\-.]") @@ -172,6 +176,12 @@ def crear(): if isinstance(f, dict) and isinstance(f.get("ruta"), str) 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) iso = ts.isoformat() d["_recibido"] = iso @@ -195,11 +205,12 @@ def crear(): with open(idx, "a", newline="", encoding="utf-8") as f: w = csv.writer(f) if nuevo: - w.writerow(["recibido", "serie", "tecnico", "resultado", "clasificacion", - "accion", "fotos", "archivo"]) - w.writerow([iso, serie, tecnico, d.get("resultado", ""), + w.writerow(["recibido", "fecha", "serie", "maquina", "tecnico", "resultado", + "clasificacion", "accion", "fotos", "logs", "archivo"]) + w.writerow([iso, d.get("fecha", ""), serie, d.get("maquina_id", d.get("maquina", "")), + tecnico, d.get("resultado", ""), 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}" return jsonify(ok=True, archivo=sub, prueba=es_prueba) @@ -282,6 +293,9 @@ def listar(): for k in ("clasificacion", "accion", "motivo", "maquina", "terminal", "unidad"): if d.get(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"): fila["fotos"] = [f.get("ruta") for f in d["fotos"] if f.get("ruta")] fila["archivo_n"] = fn @@ -310,7 +324,9 @@ def historial(): out.append({"archivo": fn, "recibido": d.get("_recibido"), "tecnico": d.get("tecnico"), "resultado": d.get("resultado"), "clasificacion": d.get("clasificacion"), "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() return jsonify(ok=True, total=len(out), visitas=out) @@ -362,6 +378,46 @@ def subir_adjuntos(): 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") def ver_adjunto(): ruta = (request.args.get("ruta") or "").strip().lstrip("/") @@ -375,8 +431,8 @@ def ver_adjunto(): @app.errorhandler(413) def _muy_grande(e): - return jsonify(ok=False, error=f"La foto pesa demasiado (máximo {MAX_MB} MB). " - "Vuelve a tomarla o espera a tener mejor señal."), 413 + return jsonify(ok=False, error=f"El archivo pesa demasiado (fotos: máximo {MAX_MB} MB; " + f"logs: máximo {LOG_MAX_MB} MB)."), 413 if __name__ == "__main__": diff --git a/backend/app.py.bak b/backend/app.py.bak deleted file mode 100644 index 5da0df1..0000000 --- a/backend/app.py.bak +++ /dev/null @@ -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"))) diff --git a/backend/parche-nginx-api.py b/backend/parche-nginx-api.py new file mode 100644 index 0000000..6e9db1b --- /dev/null +++ b/backend/parche-nginx-api.py @@ -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() diff --git a/docs/descargas/4G_NTRIP_HelgenSRV-NMEA.example.txt b/docs/descargas/4G_NTRIP_HelgenSRV-NMEA.example.txt new file mode 100644 index 0000000..c01b8e2 --- /dev/null +++ b/docs/descargas/4G_NTRIP_HelgenSRV-NMEA.example.txt @@ -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 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= +$$$AS+Q= +$$$AS+q= +$$$AS+O=0 +$$$AS+L=1 +$$$AS+D=1 +$$$AS+f=0 +$$$AS+B=115200 +$$$AS+N=1 +$$$AS+S= +$$$AS+P= +$$$AS+U=: +$$$AS+M= +$$$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= +$$$AS+h= +$$$AS+k=2 +$$$AS+K= +$$$AS+j=300 +$$$AS+C=1 +$$$AS+c=0 +$$$AS+F=1 diff --git a/docs/descargas/index.md b/docs/descargas/index.md index 0c9c4b4..22df8b6 100644 --- a/docs/descargas/index.md +++ b/docs/descargas/index.md @@ -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). +## 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 - **STM32CubeProgrammer** (Windows/macOS/Linux): https://www.st.com/en/development-tools/stm32cubeprog.html diff --git a/docs/diagnostico/procedimiento-diagnostico-campo.md b/docs/diagnostico/procedimiento-diagnostico-campo.md index 45a0cc2..c1038f0 100644 --- a/docs/diagnostico/procedimiento-diagnostico-campo.md +++ b/docs/diagnostico/procedimiento-diagnostico-campo.md @@ -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.** -## 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? | |---|---|---| @@ -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í | | 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 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 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 | |---|---|---| +| **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 | | 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 | @@ -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 | | 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: 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. | | 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: @@ -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, 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. @@ -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** (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; 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 | | 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. 2. Aparece en el **[tablero de campo](tablero-campo.html)**, que ingeniería revisa en Querétaro. diff --git a/docs/diagnostico/tablero-campo.html b/docs/diagnostico/tablero-campo.html index cae2303..cc11c8d 100644 --- a/docs/diagnostico/tablero-campo.html +++ b/docs/diagnostico/tablero-campo.html @@ -43,6 +43,9 @@ section.bloque h2{margin:0 0 2px;font-size:17px} .visita .f{color:var(--tinta2);font-size:12.5px} .visita .cl{font-weight:700} .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 img{width:100%;height:100%;object-fit:cover;display:block;background:var(--pend-bg)} details{margin-top:8px} @@ -68,6 +71,7 @@ table.det td.k{color:var(--tinta2);width:45%} + + + + diff --git a/docs/formularios/diagnostico.html b/docs/formularios/diagnostico.html index 20351b3..3e4b31a 100644 --- a/docs/formularios/diagnostico.html +++ b/docs/formularios/diagnostico.html @@ -47,6 +47,34 @@ Acta.render({ { tipo:'check', clave:'reglas', req:true, txt:'Leí las cuatro reglas 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:'Cómo funciona. Operaciones (Adriana) manda a ICAVE un correo pidiendo acceso a la ' + + 'máquina 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, no subas: 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á detenida y bloqueada (LOTO), o el supervisor confirmó que es seguro trabajar en ella.' }, + { tipo:'nota', si:v=>String(v.permiso||'').startsWith('No —'), + txt:'
Sin autorización no te subas al equipo. 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.
' }, + { 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', sub:'Contesta con lo que te dijeron, aunque suene vago. La versión del operador vale.', items: [ @@ -199,6 +227,11 @@ Acta.render({ { tipo:'medida', clave:'tlm_rec', si:conUsb, txt:'Campo rec (veces que se ha tenido que recuperar)' }, { tipo:'medida', clave:'tlm_vbat', si:conUsb, txt:'Campo vbat (voltaje de la batería)', u:'V' }, { tipo:'medida', clave:'tlm_veh', si:conUsb, txt:'Campo veh (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, txt:'Foto de la pantalla de la laptop con las líneas visibles (respaldo por si falla el copiado).' }, ] }, diff --git a/docs/formularios/flota.js b/docs/formularios/flota.js index 6200055..8d97f99 100644 --- a/docs/formularios/flota.js +++ b/docs/formularios/flota.js @@ -1,29 +1,35 @@ /* Flota HBOX desplegada — registro único de unidades en campo. 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. - serie identificador corto y ESTABLE de la unidad (va en el acta y en el servidor). - Sin espacios ni acentos. Es la etiqueta física de la caja. - etiqueta lo que ve el técnico en la lista desplegable. - maquina equipo donde está montada, con el número del terminal. + serie número del HBOX tal como viene etiquetado en la caja. + etiqueta lo que ve el técnico en la lista: MÁQUINA primero. + maquina número de máquina TAL COMO LO NOMBRA ICAVE (el del correo de acceso). terminal ICAVE · TIMSA · EIT · LCT (agrupa la lista) estado operativa | falla | desmontada | repuesto - nota cualquier cosa que el técnico deba 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). */ + nota lo que el técnico debe saber ANTES de subirse a la máquina. */ window.FLOTA = [ - { serie: 'HBOX-V3-RTG-020', etiqueta: 'RTG ZPM-020 — unidad desmontada', maquina: 'RTG ZPM 020', - 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.' }, + { serie: 'RTK-014', etiqueta: 'FL-JUN-060 — Montacargas · RTK-014 (sigue instalada)', + maquina: 'FL-JUN-060', terminal: 'ICAVE', estado: 'falla', + 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)', - 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.' }, + { serie: 'RTK-003', etiqueta: 'RT-ZPMC-20 — RTG · RTK-003 (desmontada, en taller)', + maquina: 'RT-ZPMC-20', terminal: 'ICAVE', estado: 'desmontada', + 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)', - terminal: 'ICAVE', estado: 'falla', - 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.' }, - - { 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.' }, + { serie: 'RTK-006', etiqueta: 'HS-WES-62 — Tractoplana · RTK-006 (desmontada, en taller)', + maquina: 'HS-WES-62', terminal: 'ICAVE', estado: 'desmontada', + 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.' }, ]; diff --git a/docs/index.md b/docs/index.md index 54989c4..81875e7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,25 +1,135 @@ +--- +hide: + - toc +--- + # HBOX — Documentación técnica -Sitio interno de Helgen Technologies para técnicos de banco y colaboradores del proyecto -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. +Sitio interno de Helgen Technologies. **Busca abajo lo que vas a hacer hoy.** -## 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) -- **¿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. + --- + + 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) + +
+ +## 🏭 Producción — lote v5 + +
+ +- ### 🔌 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) + +
+ +## 📥 Archivos y referencia + +
+ +- ### 💾 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) + +
--- -*Última sincronización: la fecha aparece en el pie de cada página. Contacto: leo@helgen.tech* +## 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* diff --git a/docs/procedimientos/diagnostico-banco.md b/docs/procedimientos/diagnostico-banco.md new file mode 100644 index 0000000..4a11bed --- /dev/null +++ b/docs/procedimientos/diagnostico-banco.md @@ -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. diff --git a/docs/procedimientos/reflasheo-unidades.md b/docs/procedimientos/reflasheo-unidades.md new file mode 100644 index 0000000..862951e --- /dev/null +++ b/docs/procedimientos/reflasheo-unidades.md @@ -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. diff --git a/mkdocs.yml b/mkdocs.yml index 33fdc96..0b05248 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,7 +20,8 @@ theme: icon: material/weather-sunny name: Modo claro features: - - navigation.sections + - navigation.tracking + - navigation.indexes - navigation.top - search.highlight - content.code.copy @@ -39,41 +40,53 @@ markdown_extensions: permalink: true nav: - - Inicio: index.md - - "Tablero de estado": tablero.html - - Actas y control de calidad: - - "Índice de actas": actas/index.md - - "Acta de diagnóstico en campo": formularios/diagnostico.html - - "Acta de prueba V5 (tarjeta)": formularios/v5.html - - "Acta de módulo ArduSimple (simpleRTK2B, ARD-NNN)": formularios/ardusimple.html - - "Acta de módem 4G (NTRIP Master, RTK-NNNN)": formularios/modem4g.html - - "Acta de integración ArduSimple + 4G": formularios/integracion.html - - "Acta de ensamble (unidad HBOX v5)": formularios/ensamble.html - - "Acta de prueba V4 (borrador)": formularios/v4.html - - Procedimientos de banco: - - "EinarBoard v5 — Prueba y flasheo (lote 15 pzas)": procedimientos/v5-prueba-y-flasheo.md - - "HBOX v5 — Módulos RTK/4G e integración (antes del ensamble)": procedimientos/v5-modulos-integracion.md - - "HBOX v5 — Ensamble de la unidad y energizado final": procedimientos/v5-ensamble.md - - "HBOX v5 — Prueba final de unidad (fw 0.4.0, antes de embarcar)": procedimientos/v5-prueba-final-unidad.md - - "EinarBoard 1.0 — Bring-up runbook": procedimientos/einarboard-1.0-bringup.md - - "FW v3.0.3 — Prueba de banco (política de ventilador)": procedimientos/fan-policy-bench-test.md - - "Histórico — Proto bench v3": procedimientos/historico-proto-bench-v3.md - - "Histórico — Protoboard": procedimientos/historico-protoboard.md - - Montaje y arneses: + - "🏠 Inicio": index.md + + - "🚜 Campo — la unidad está en la máquina": + - "Cómo se diagnostica en campo": diagnostico/procedimiento-diagnostico-campo.md + - "▶ Acta de diagnóstico en campo": formularios/diagnostico.html + - "Tablero de campo (qué se ha revisado)": diagnostico/tablero-campo.html + + - "🔬 Taller — la unidad está en la mesa": + - "Cómo se diagnostica en banco": procedimientos/diagnostico-banco.md + - "▶ Acta de diagnóstico en banco": formularios/banco.html + - "⚡ Reflasheo de unidades": procedimientos/reflasheo-unidades.md + + - "🏭 Producción — lote v5": + - "1 · Prueba y flasheo de tarjetas": procedimientos/v5-prueba-y-flasheo.md + - "▶ Acta de tarjeta V5": formularios/v5.html + - "2 · Módulos RTK y 4G": procedimientos/v5-modulos-integracion.md + - "▶ Acta ArduSimple": formularios/ardusimple.html + - "▶ Acta módem 4G": formularios/modem4g.html + - "▶ Acta de integración": formularios/integracion.html + - "3 · Ensamble de la unidad": procedimientos/v5-ensamble.md + - "▶ 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 - "BOM dividido para ensamble": montaje/bom-split.md - "Guía de ensamble y control de calidad": montaje/guia-ensamble-qc.md - "Guía de ensamble (PDF)": montaje/assembly-guide.md - - Diagnóstico: - - "⚑ Diagnóstico en campo — procedimiento": diagnostico/procedimiento-diagnostico-campo.md - - "⚑ Acta de diagnóstico en campo (llenar en la máquina)": formularios/diagnostico.html - - "Tablero de campo (ingeniería)": diagnostico/tablero-campo.html - - "Protocolo de diagnóstico remoto": diagnostico/protocolo-diagnostico-remoto.md - - Git: + + - "📥 Descargas": + - "Firmware, parámetros 4G y ZED-F9P": descargas/index.md + - "README firmware bring-up v5": descargas/hbox-v5-bringup-README.md + + - "📋 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 - "Git guide (English)": git/git-guide-en.md - - Descargas: - - "Firmware y herramientas": descargas/index.md - - "README firmware bring-up v5": descargas/hbox-v5-bringup-README.md - - "Administración (Leo)": admin.html - - "Salir": salir.html + + - "📚 Referencia e histórico": + - "Protocolo de diagnóstico remoto (2026-06)": diagnostico/protocolo-diagnostico-remoto.md + - "EinarBoard 1.0 — Bring-up runbook": procedimientos/einarboard-1.0-bringup.md + - "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 diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 0482d55..3706661 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -11,4 +11,14 @@ PORT="${SSH_PORT:-22}" mkdocs build --strict 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'"