Compras: OC del cotizador en el centro de mando (fase 1) + flujo del 28-oct + 33 documentos publicados
This commit is contained in:
parent
4f08640b31
commit
e62a7dec23
@ -31,6 +31,7 @@ Adriana, Michelle, Jesús and the field technicians work from it; Claude closes
|
||||
| **Ruta a producción**, version cards, fase, pedido, próximas fechas, map pins | `docs/datos/proyecto.json` | Claude or Leo | edit by hand (keep the one-array-per-line format); step states `hecho · ahora · bloqueado · falta`, card tone `hecho · ahora · aviso · bloqueado`; bump `actualizado` |
|
||||
| **Instalaciones** (per-site installation board, 22-oct) | `docs/tableros/instalaciones.md` + `.js`/`.css`; data: field actas via `GET /api/instalaciones` (`backend/instalaciones.py`, hooked at the end of `app.py`) | automatic | stages envío → recepción → instalación → **prueba en sitio** (acta `prueba-sitio`; only a passed test counts for the 1–2 per-site goal) → en operación (7 days after the test, no failure) + «con falla»; colour, nota and meta per site from `proyecto.json`; forms accept `?serie=&sitio=&<clave>=` prefill (`acta.js`) |
|
||||
| **Inventario por sitio** (23-oct) | `docs/tableros/inventario.md` + `.js`/`.css`; data: server `/srv/projects/drogheda/inventario` via `/api/inventario` | everyone in the UI | tabs ICAVE · TIMSA · EIT · LCT · Querétaro + «Todo» (`?sitio=eit&n=2`); every movement carries `sitio` (none = qro); `envio` leaves a site (or `fuera`: purchase in transit / material shipped before counting) and stays «en camino» until the destination presses «Recibir» (`recepcion`); item `por_instalacion` = the installation kit; team rules in `docs/inventario/index.md`; tests `backend/tests-1023.sh` |
|
||||
| **Compras** (28-oct) | `docs/tableros/compras.md` + `.js`/`.css`; datos: SQLite `/srv/projects/drogheda/drogheda.db` vía `backend/compras.py` (enganchado al final de `app.py`) | las OC entran solas desde el cotizador; el responsable del sitio da «Recibir» | las órdenes de compra de DROGHEDA **sin montos**, por sitio y estado (autorizada · pagada · en-camino · recibida-parcial · recibida · cancelada); «Ligar» un renglón a un ítem del inventario y «Recibir» completo o en parte; cada OC abre su pendiente `t-oc-<folio>` y se cierra sola al recibir todo; las dos puertas de recepción (Compras y el envío «en camino» de Inventario) se concilian. Las rutas `/api/integracion/…` van firmadas con `COTIZADOR_SECRETO` (HMAC del cuerpo y la hora, ventana 5 min), **sin sesión**: sin secreto contestan 503 y el resto del sitio sigue igual. Las rondas `"prueba": true` se guardan aparte. Flujo y decisiones: `docs/inventario/flujo-compras-28oct.md`; pruebas `backend/tests-1028.sh` (58) |
|
||||
| **Entrada al sitio** (27-oct) | `docs/entrar.html` + `docs/publico/` (public); `backend/acceso.py`; nginx `backend/nginx/drogheda.helgen.online.conf` installed by `backend/nginx/cambiar-entrada.sh` (backup, tests, auto-rollback; `deshacer`) | — | session = signed cookie `hbox_sesion` (30 days; version = hash of the user's htpasswd line, so a password change ends old sessions); login by user or Equipo email, case-insensitive; **no HTTP Basic** (only from `ACCESO_BASIC_IPS`, empty by default — server scripts use 127.0.0.1:8081); unsafe methods only same-origin (Origin/Sec-Fetch-Site); lockout per user+IP (8) and per IP (20) in 15 min; attempts in `/srv/projects/drogheda/acceso/intentos-AAAA-MM.jsonl` (no passwords); public paths: entrada, `/publico/`, `/fonts/`, `helgen.css`, `tema.js`, icon, `/api/{entrar,salir,sesion,salud,incidencias/publica(s)}` |
|
||||
| **Incidencias del sitio** (27-oct) | server `/srv/projects/drogheda/incidencias` (INC-0001.json…, `_errores/`, `_vigia/`); `backend/incidencias.py`; page `docs/tableros/incidencias.md` + `incidencias-tablero.js/.css`; footer «Problemas del sitio» = `docs/js/incidencias.js` (in `overrides/main.html` head and in every standalone page) | everyone reports; `INCIDENCIAS_EDITORES` (leo, gibranlp, adriana, claude) triage; the daily agent through `scripts/agente-api.sh` | rules: `docs/agentes.md`; the agent's runbook: `agentes/agente-diario.md`; vigía `backend/vigia.py` (leo's crontab, minute 7); never put passwords or project data in `publica` notices |
|
||||
| **Standup** (27-oct) | server `/srv/projects/drogheda/standup/AAAA-MM-DD/<user>.json`; `backend/standup.py`; `docs/tableros/standup.md` + `standup-tablero.js/.css`; banner `docs/js/standup.js` | the bot (`claude`) prepares; each person answers; Leo/Adriana see «Equipo» | answers can move the person's own cards (as them); everything else from the answers goes in a lote |
|
||||
@ -48,6 +49,10 @@ Adriana, Michelle, Jesús and the field technicians work from it; Claude closes
|
||||
|
||||
## Rules when editing the site
|
||||
|
||||
- **Nothing may link to something that isn't published.** `python3 scripts/revisar-enlaces.py` checks every task
|
||||
link, every link inside the published docs, and every dated `.md` in the `NN-` folders against the `MAP`; it runs
|
||||
inside `deploy.sh` and refuses to publish if anything is broken. A document that lives only in the repo does not
|
||||
exist for the team.
|
||||
- **Mexican Spanish** for every word the team reads (pages, notes in lotes, task titles).
|
||||
- `mkdocs build --strict` must pass (warnings fail). Build a copy first if another session may be editing.
|
||||
- **Another session may be editing hbox-docs at the same time.** Check the file's mtime/hash right before
|
||||
|
||||
@ -629,6 +629,39 @@ rsync -az leo@git.helgen.online:/srv/projects/drogheda/actas/ ~/respaldo-acta
|
||||
rsync -az leo@git.helgen.online:/srv/projects/drogheda/adjuntos/ ~/respaldo-fotos/
|
||||
```
|
||||
|
||||
## D4 · 28-oct: Compras (órdenes de compra del cotizador) y la hora local
|
||||
|
||||
`backend/app.py` cambió (engancha `compras.py`) y el servicio también, así que **hay que reiniciar**.
|
||||
|
||||
```bash
|
||||
cd ~/Documents/Claude/Projects/Drogheda/hbox-docs
|
||||
./scripts/deploy.sh # sube el sitio y backend/compras.py; revisa enlaces antes de publicar
|
||||
ssh leo@git.helgen.online
|
||||
sudo cp /srv/projects/drogheda/hbox-docs/backend/actas-hbox.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload && sudo systemctl restart actas-hbox
|
||||
curl -s localhost:8081/api/compras -H 'X-Remote-User: leo' | head -c 200 # debe traer ok:true
|
||||
```
|
||||
|
||||
Qué trae el servicio nuevo:
|
||||
|
||||
- **`TZ=America/Mexico_City`** — el servidor corre en UTC y por eso el sitio enseñaba todo 6 horas adelantado (lo
|
||||
vio el equipo en la junta del 28-oct). Desde el reinicio, cada fecha que escribe el backend queda en hora de
|
||||
Querétaro. **Lo guardado antes del 28-oct sigue en UTC y no se corrige solo.**
|
||||
- **`COTIZADOR_SECRETO`** (comentado) — el secreto compartido con el cotizador. Mientras no esté, las rutas
|
||||
`/api/integracion/` contestan 503 con un mensaje claro, la página Compras lo avisa y **el resto del sitio
|
||||
funciona igual**. Para encenderlo:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32 # el MISMO valor va en el cotizador como PORTAL_SECRETO
|
||||
sudo systemctl edit actas-hbox # o descomenta la línea en el .service y vuelve a copiarlo
|
||||
```
|
||||
|
||||
La base `/srv/projects/drogheda/drogheda.db` se crea sola al arrancar (SQLite en modo WAL) y se respalda con el
|
||||
resto de `/srv/projects/drogheda`. nginx **no** necesita cambios: `/api/integracion/` entra por el `location /api/`
|
||||
que ya existe y la firma la revisa el backend, no la sesión.
|
||||
|
||||
Pruebas antes de publicar: `bash backend/tests-1028.sh ~/Documents/Claude/Projects/Drogheda/hbox-docs` (58).
|
||||
|
||||
## E · Problemas frecuentes
|
||||
|
||||
| Síntoma | Causa | Arreglo |
|
||||
|
||||
@ -29,6 +29,19 @@ Environment=ACTAS_ADMINS=leo,gibranlp
|
||||
Environment=HTPASSWD_FILE=/etc/nginx/hbox-docs.htpasswd
|
||||
Environment=PASS_MIN=8
|
||||
Environment=PORT=8081
|
||||
# 28-oct: la hora del servidor es UTC y por eso el sitio enseñaba todo 6 horas adelantado (lo vio el equipo
|
||||
# en la junta del 28-oct). Con esto, cada fecha que escribe el backend queda en hora de Querétaro. Lo ya
|
||||
# guardado NO se corrige solo: los registros de antes del 28-oct siguen en UTC.
|
||||
Environment=TZ=America/Mexico_City
|
||||
# Compras (28-oct, backend/compras.py): el secreto compartido con el cotizador. Sin él, las rutas
|
||||
# /api/integracion/ contestan 503 y el resto del sitio funciona igual. Se genera con
|
||||
# openssl rand -hex 32
|
||||
# y el MISMO valor va en las propiedades del script del cotizador como PORTAL_SECRETO. Nunca en git.
|
||||
#Environment=COTIZADOR_SECRETO=
|
||||
#Environment=COMPRAS_DB=/srv/projects/drogheda/drogheda.db
|
||||
# Quién recibe el material en cada sitio (abre el pendiente de cada OC). Por omisión:
|
||||
# VER alan · ZLO jesush · ESE miguel · LZC antonio · QRO jesus
|
||||
#Environment=COMPRAS_RESPONSABLES=VER:alan,ZLO:jesush,ESE:miguel,LZC:antonio,QRO:jesus
|
||||
# Página de entrada (27-oct, backend/acceso.py): nginx pregunta a /api/_auth en CADA petición (páginas y archivos).
|
||||
# Por eso hilos: una subida de fotos de 2 minutos no debe dejar esperando a los demás. Secreto de sesión: si no se fija
|
||||
# aquí, el servicio crea /srv/projects/drogheda/.sesion-secreto (600) la primera vez. Cambiarlo cierra todas las sesiones.
|
||||
|
||||
@ -2385,9 +2385,11 @@ def actividad():
|
||||
# falta o falla, el resto del servicio arranca igual (el tablero avisa que no hay datos).
|
||||
# (27-oct) Igual se enganchan acceso.py (página de entrada y sesión: /api/_auth, /api/entrar, /api/salir),
|
||||
# incidencias.py (problemas del sitio, errores del navegador y del servidor) y standup.py (el bot de scrum).
|
||||
# (28-oct) compras.py: las órdenes de compra del cotizador (SQLite), sus envíos «en camino» por sitio y el
|
||||
# pendiente de cada OC; sus rutas /api/integracion/ van firmadas con COTIZADOR_SECRETO, sin sesión.
|
||||
# Cada uno se carga aparte: si uno falla, los demás y el resto del servicio arrancan igual.
|
||||
import importlib.util as _ilu
|
||||
for _mod in ("instalaciones", "acceso", "incidencias", "standup"):
|
||||
for _mod in ("instalaciones", "acceso", "incidencias", "standup", "compras"):
|
||||
try:
|
||||
_spec = _ilu.spec_from_file_location(_mod, os.path.join(os.path.dirname(os.path.abspath(__file__)), _mod + ".py"))
|
||||
_m = _ilu.module_from_spec(_spec)
|
||||
|
||||
665
backend/compras.py
Normal file
665
backend/compras.py
Normal file
@ -0,0 +1,665 @@
|
||||
"""Compras: las órdenes de compra del cotizador, dentro del centro de mando (28-oct-2026).
|
||||
|
||||
Fase 1 de la integración cotizador ↔ centro de mando (documento
|
||||
`2026-10-27_Drogheda_Integracion-Cotizador-Inventario_ES_v1.0.md`, decisiones D-27-1 a D-27-5).
|
||||
Lo engancha app.py al final con registrar(app, globals()); vive aparte para no crecer app.py.
|
||||
|
||||
Qué hace, en una línea: el cotizador manda cada cambio de una OC (sin montos); aquí se guarda, el
|
||||
material aparece **en camino** en la pestaña del sitio del inventario, se abre **un pendiente por OC**
|
||||
para el responsable del sitio, y cuando ese material se recibe el pendiente se cierra solo y el
|
||||
cotizador se entera en su siguiente consulta.
|
||||
|
||||
POST /api/integracion/cotizador/evento (firmado, sin sesión)
|
||||
oc.autorizada · oc.pagada · oc.en_camino · oc.cancelada · req.cerrada
|
||||
GET /api/integracion/inventario/items (firmado) — el selector de ítems del cotizador
|
||||
GET /api/integracion/cotizador/recepciones?desde=… (firmado) — lo que ya llegó, para cerrar la requisición
|
||||
|
||||
GET /api/compras[?sitio=&estado=] (con sesión) — la página Compras
|
||||
POST /api/compras/ligar (con sesión) — ligar un renglón a un ítem del inventario
|
||||
POST /api/compras/recibir (con sesión) — recibir una OC completa o en parte
|
||||
|
||||
Seguridad de las rutas de integración: **no** usan la sesión de una persona. Cada llamada va firmada
|
||||
con un secreto compartido (HMAC-SHA256 de la hora + "\\n" + el cuerpo). El secreto vive en el servicio
|
||||
(`COTIZADOR_SECRETO`) y en las propiedades del script del cotizador (`PORTAL_SECRETO`); nunca en el
|
||||
código ni en un chat. Se rechaza una firma de hace más de 5 minutos. Sin secreto configurado, las tres
|
||||
rutas contestan 503 y la página lo dice: el sitio sigue funcionando igual.
|
||||
|
||||
Montos: el centro de mando **no** los recibe ni los guarda. Si un aviso trae costo, importe, subtotal,
|
||||
IVA, total, tipo de cambio o margen, se descartan antes de escribir (lo ve todo el equipo).
|
||||
|
||||
Datos: SQLite en `/srv/projects/drogheda/drogheda.db` (D-27-2), en modo WAL porque el servicio corre con
|
||||
dos trabajadores. El inventario sigue en sus JSON: una OK «en camino» crea sus envíos con las mismas
|
||||
funciones que usa la página de Inventario, así que las cuentas por sitio no cambian de origen.
|
||||
"""
|
||||
import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from flask import jsonify, request
|
||||
|
||||
VENTANA_FIRMA = 300 # segundos de tolerancia de la firma (reloj y red)
|
||||
MAX_CUERPO = 256 * 1024 # un aviso más grande que esto no es una OC
|
||||
MONTOS = ("costo", "costo_unitario", "precio", "precio_unitario", "importe", "subtotal", "iva",
|
||||
"impuestos", "total", "tipo_cambio", "moneda", "margen", "descuento")
|
||||
ESTADOS_OC = ("autorizada", "pagada", "en-camino", "recibida-parcial", "recibida", "cancelada")
|
||||
TIPOS = ("oc.autorizada", "oc.pagada", "oc.en_camino", "oc.cancelada", "req.cerrada")
|
||||
# Quién recibe el material en cada sitio (el que da «Recibir» y a quien se le abre el pendiente).
|
||||
# Se puede cambiar sin tocar el código con COMPRAS_RESPONSABLES="VER:alan,ZLO:jesush,…".
|
||||
RESPONSABLES = {"VER": "alan", "ZLO": "jesush", "ESE": "miguel", "LZC": "antonio", "QRO": "jesus"}
|
||||
RESPONSABLE_OMISION = "adriana"
|
||||
|
||||
ESQUEMA = """
|
||||
CREATE TABLE IF NOT EXISTS ordenes (
|
||||
folio TEXT PRIMARY KEY, req_folio TEXT, proyecto_folio TEXT, proyecto_clave TEXT,
|
||||
sitio TEXT, sitio_portal TEXT, proveedor TEXT, estado TEXT, fecha_entrega TEXT,
|
||||
paqueteria TEXT, guia TEXT, en_camino_en TEXT, solicitante TEXT, autorizo TEXT,
|
||||
tarea_id TEXT, prueba INTEGER DEFAULT 0, req_cerrada INTEGER DEFAULT 0,
|
||||
recepcion_en TEXT, recepcion_por TEXT, creado_en TEXT, actualizado_en TEXT);
|
||||
CREATE TABLE IF NOT EXISTS renglones (
|
||||
oc_folio TEXT, n INTEGER, descripcion TEXT, unidad TEXT, cantidad REAL,
|
||||
item_codigo TEXT, item_id TEXT, envio_id TEXT, recibido REAL DEFAULT 0,
|
||||
recibido_en TEXT, recibido_por TEXT, nota TEXT,
|
||||
PRIMARY KEY (oc_folio, n));
|
||||
CREATE TABLE IF NOT EXISTS eventos (
|
||||
evento_id TEXT PRIMARY KEY, oc_folio TEXT, tipo TEXT, cuando TEXT, quien TEXT,
|
||||
origen TEXT, recibido_en TEXT, datos TEXT);
|
||||
CREATE INDEX IF NOT EXISTS ix_ordenes_sitio ON ordenes (sitio_portal, estado);
|
||||
CREATE INDEX IF NOT EXISTS ix_renglones_envio ON renglones (envio_id);
|
||||
"""
|
||||
|
||||
|
||||
def _responsables():
|
||||
m = dict(RESPONSABLES)
|
||||
for par in os.environ.get("COMPRAS_RESPONSABLES", "").split(","):
|
||||
if ":" in par:
|
||||
k, v = par.split(":", 1)
|
||||
m[k.strip().upper()] = v.strip().lower()
|
||||
return m
|
||||
|
||||
|
||||
def _secreto():
|
||||
return os.environ.get("COTIZADOR_SECRETO", "").strip()
|
||||
|
||||
|
||||
def _num(v, defecto=0.0):
|
||||
try:
|
||||
n = float(v)
|
||||
except (TypeError, ValueError):
|
||||
return defecto
|
||||
return n if n == n and abs(n) != float("inf") else defecto
|
||||
|
||||
|
||||
def _entero_exacto(v):
|
||||
"""La cantidad del renglón como entero, o None si no lo es (el inventario cuenta piezas y metros)."""
|
||||
n = _num(v, -1)
|
||||
return int(round(n)) if n > 0 and abs(n - round(n)) < 1e-9 else None
|
||||
|
||||
|
||||
def _sin_montos(d):
|
||||
"""Copia del diccionario sin ninguna llave de dinero, a cualquier profundidad."""
|
||||
if isinstance(d, dict):
|
||||
return {k: _sin_montos(v) for k, v in d.items() if k.lower() not in MONTOS}
|
||||
if isinstance(d, list):
|
||||
return [_sin_montos(x) for x in d]
|
||||
return d
|
||||
|
||||
|
||||
class Compras:
|
||||
"""Todo el estado del módulo: la base y las funciones de app.py que necesita."""
|
||||
|
||||
def __init__(self, g):
|
||||
self.g = g
|
||||
self.ruta = os.environ.get("COMPRAS_DB", os.path.join(g["DATOS_BASE"], "drogheda.db"))
|
||||
|
||||
# ---------------------------------------------------------------- base de datos
|
||||
def con(self):
|
||||
c = sqlite3.connect(self.ruta, timeout=10)
|
||||
c.row_factory = sqlite3.Row
|
||||
c.execute("PRAGMA journal_mode=WAL")
|
||||
c.execute("PRAGMA busy_timeout=8000")
|
||||
c.execute("PRAGMA foreign_keys=ON")
|
||||
return c
|
||||
|
||||
def arrancar(self):
|
||||
os.makedirs(os.path.dirname(self.ruta) or ".", exist_ok=True)
|
||||
with self.con() as c:
|
||||
c.executescript(ESQUEMA)
|
||||
|
||||
# ---------------------------------------------------------------- sitios y equipo
|
||||
def sitio_portal(self, codigo):
|
||||
"""'ZLO' o 'timsa' → 'timsa'. Devuelve None si no es un sitio del centro de mando."""
|
||||
s = self.g["_norm_id"](self.g["_texto"](codigo, 40))
|
||||
s = self.g["ALIAS_SITIO"].get(s, s)
|
||||
return s if s in self.g["SITIOS_TAREA_IDS"] else None
|
||||
|
||||
def codigo(self, sitio_portal):
|
||||
return self.g["CODIGO_SITIO"].get(sitio_portal, (sitio_portal or "").upper())
|
||||
|
||||
def nombre_sitio(self, sitio_portal):
|
||||
for i, n, _ in self.g["SITIOS_TAREA"]:
|
||||
if i == sitio_portal:
|
||||
return "Querétaro" if i == "qro" else n
|
||||
return sitio_portal or "?"
|
||||
|
||||
def responsable(self, codigo):
|
||||
quien = _responsables().get((codigo or "").upper(), RESPONSABLE_OMISION)
|
||||
miembros = self.g["_miembros"]()
|
||||
return quien if quien in miembros else (RESPONSABLE_OMISION if RESPONSABLE_OMISION in miembros
|
||||
else (sorted(miembros)[0] if miembros else "adriana"))
|
||||
|
||||
# ---------------------------------------------------------------- inventario
|
||||
def items(self):
|
||||
return self.g["_listar_json"](self.g["INV_ITEMS"]())
|
||||
|
||||
def item_por_codigo(self, codigo):
|
||||
c = (codigo or "").strip().upper()
|
||||
if not c:
|
||||
return None
|
||||
for it in self.items():
|
||||
if (it.get("codigo") or "").upper() == c:
|
||||
return it
|
||||
return None
|
||||
|
||||
def envio(self, envio_id):
|
||||
if not envio_id:
|
||||
return None
|
||||
m = self.g["_leer_json"](self.g["_ruta_registro"](self.g["INV_MOVS"](), envio_id))
|
||||
return m if isinstance(m, dict) and m.get("tipo") == "envio" else None
|
||||
|
||||
# ---------------------------------------------------------------- pendiente de la OC
|
||||
def abrir_pendiente(self, oc, quien):
|
||||
"""Un pendiente por OC: «Recibir OC-… en ZLO», para el responsable del sitio, que vence el día
|
||||
de entrega. Si ya existe (un aviso repetido, o alguien lo abrió a mano), no se duplica."""
|
||||
g = self.g
|
||||
oc = dict(oc) # llega como sqlite3.Row, que no tiene .get()
|
||||
tid = "t-oc-" + oc["folio"].lower().replace("/", "-")
|
||||
ruta = g["_ruta_registro"](g["TAREAS_DIR"], tid)
|
||||
if os.path.exists(ruta):
|
||||
return tid
|
||||
sitio = oc["sitio_portal"]
|
||||
d = {"titulo": f"Recibir {oc['folio']} en {self.codigo(sitio)}"
|
||||
+ (f" — {oc['proveedor']}" if oc.get("proveedor") else ""),
|
||||
"descripcion": ("Orden de compra del cotizador. Cuando llegue el material, ábrela en "
|
||||
f"**Compras** y dale **Recibir** con lo que llegó de verdad.\n\n"
|
||||
f"- Proveedor: {oc.get('proveedor') or '—'}\n"
|
||||
f"- Requisición: {oc.get('req_folio') or '—'} · pidió {oc.get('solicitante') or '—'}\n"
|
||||
f"- Sitio de entrega: {self.nombre_sitio(sitio)}\n"
|
||||
"- Los montos y el pago se quedan en el cotizador."),
|
||||
"pista": "compras", "estado": "por-hacer",
|
||||
"responsable": self.responsable(self.codigo(sitio)),
|
||||
"sitios": [sitio], "etiquetas": ["compras", "oc"],
|
||||
"enlaces": [{"titulo": "Compras", "url": "/tableros/compras/?oc=" + oc["folio"]}]}
|
||||
if oc.get("fecha_entrega"):
|
||||
d["fecha_limite"] = oc["fecha_entrega"]
|
||||
t, err = g["_tarea_valida"](d)
|
||||
if err:
|
||||
return None
|
||||
t.pop("_aviso", None)
|
||||
ahora = g["ahora"]()
|
||||
t.update(id=tid, orden=0, creado_por=quien, creado_en=ahora, modificado_por=quien,
|
||||
modificado_en=ahora,
|
||||
historial=[{"quien": quien, "cuando": ahora,
|
||||
"cambio": f"creado en por-hacer desde el cotizador ({oc['folio']})"}])
|
||||
g["_guardar_tarea"](t)
|
||||
return tid
|
||||
|
||||
def nota_pendiente(self, tarea_id, cambio, quien, estado=None):
|
||||
g = self.g
|
||||
if not tarea_id:
|
||||
return
|
||||
ruta = g["_ruta_registro"](g["TAREAS_DIR"], tarea_id)
|
||||
t = g["_leer_json"](ruta)
|
||||
if not t:
|
||||
return
|
||||
ahora = g["ahora"]()
|
||||
if estado and t.get("estado") != estado:
|
||||
t["estado"] = estado
|
||||
t["modificado_por"], t["modificado_en"] = quien, ahora
|
||||
t.setdefault("historial", []).append({"quien": quien, "cuando": ahora, "cambio": cambio})
|
||||
g["_guardar_tarea"](t)
|
||||
|
||||
# ---------------------------------------------------------------- envíos «en camino»
|
||||
def crear_envios(self, c, folio, quien):
|
||||
"""Pone en camino lo que se pueda de esa OC: un envío por renglón que ya tenga su ítem.
|
||||
Los renglones sin ítem quedan en «por ligar» y se mandan cuando alguien los liga."""
|
||||
oc = c.execute("SELECT * FROM ordenes WHERE folio=?", (folio,)).fetchone()
|
||||
if not oc or oc["estado"] not in ("en-camino", "pagada", "autorizada"):
|
||||
return []
|
||||
hechos = []
|
||||
for r in c.execute("SELECT * FROM renglones WHERE oc_folio=? ORDER BY n", (folio,)).fetchall():
|
||||
if r["envio_id"] or not r["item_codigo"]:
|
||||
continue
|
||||
env = self.crear_envio(c, oc, r, quien)
|
||||
if env:
|
||||
hechos.append(env)
|
||||
return hechos
|
||||
|
||||
def crear_envio(self, c, oc, r, quien):
|
||||
it = self.item_por_codigo(r["item_codigo"])
|
||||
if not it:
|
||||
c.execute("UPDATE renglones SET item_codigo=NULL, nota=? WHERE oc_folio=? AND n=?",
|
||||
(f"El ítem {r['item_codigo']} no existe en el inventario: hay que ligarlo a mano.",
|
||||
oc["folio"], r["n"]))
|
||||
return None
|
||||
cant = _entero_exacto(r["cantidad"])
|
||||
if cant is None:
|
||||
c.execute("UPDATE renglones SET nota=? WHERE oc_folio=? AND n=?",
|
||||
(f"Cantidad {r['cantidad']}: el inventario cuenta piezas y metros enteros. "
|
||||
"Recíbelo a mano cuando llegue.", oc["folio"], r["n"]))
|
||||
return None
|
||||
motivo = (f"compra en camino: {oc['folio']}"
|
||||
+ (f" ({oc['proveedor']})" if oc["proveedor"] else "")
|
||||
+ (f", guía {oc['guia']}" if oc["guia"] else ""))
|
||||
m = self.g["_registrar_movimiento"](it, "envio", cant, motivo[:200], oc["folio"], quien,
|
||||
sitio=self.g["ORIGEN_FUERA"], destino=oc["sitio_portal"])
|
||||
c.execute("UPDATE renglones SET envio_id=?, item_id=?, nota=NULL WHERE oc_folio=? AND n=?",
|
||||
(m["id"], it["id"], oc["folio"], r["n"]))
|
||||
return m["id"]
|
||||
|
||||
def cancelar_envios(self, c, folio, quien, motivo):
|
||||
"""Una OC cancelada: lo que iba en camino deja de contar. Se cierra cada envío con una
|
||||
recepción de 0 (el inventario baja el total, que es justo lo que pasó: nunca llegó)."""
|
||||
for r in c.execute("SELECT * FROM renglones WHERE oc_folio=? AND envio_id IS NOT NULL",
|
||||
(folio,)).fetchall():
|
||||
env = self.envio(r["envio_id"])
|
||||
if not env or env.get("estado") != "en-camino":
|
||||
continue
|
||||
it = self.g["_leer_json"](self.g["_ruta_registro"](self.g["INV_ITEMS"](), env.get("item_id")))
|
||||
if it:
|
||||
self.g["_registrar_movimiento"](it, "recepcion", 0, motivo[:200], folio, quien,
|
||||
sitio=env.get("destino"), envio=env)
|
||||
|
||||
# ---------------------------------------------------------------- conciliación
|
||||
def conciliar(self, c, folio=None):
|
||||
"""Se entera de las recepciones hechas desde la pestaña del sitio en Inventario: si el envío de
|
||||
un renglón ya se recibió, lo anota aquí y, si llegó todo, cierra la OC y su pendiente."""
|
||||
q = "SELECT * FROM renglones WHERE envio_id IS NOT NULL AND recibido_en IS NULL"
|
||||
filas = c.execute(q + (" AND oc_folio=?" if folio else ""), (folio,) if folio else ()).fetchall()
|
||||
tocadas = set()
|
||||
for r in filas:
|
||||
env = self.envio(r["envio_id"])
|
||||
if not env or env.get("estado") != "recibido":
|
||||
continue
|
||||
c.execute("UPDATE renglones SET recibido=?, recibido_en=?, recibido_por=? WHERE oc_folio=? AND n=?",
|
||||
(_num(env.get("recibido"), 0), env.get("recibido_en") or self.g["ahora"](),
|
||||
env.get("recibido_por") or "", r["oc_folio"], r["n"]))
|
||||
tocadas.add(r["oc_folio"])
|
||||
for f in (tocadas | ({folio} if folio else set())):
|
||||
self.cerrar_si_llego_todo(c, f)
|
||||
return tocadas
|
||||
|
||||
def cerrar_si_llego_todo(self, c, folio):
|
||||
oc = c.execute("SELECT * FROM ordenes WHERE folio=?", (folio,)).fetchone()
|
||||
if not oc or oc["estado"] in ("cancelada", "recibida"):
|
||||
return
|
||||
rs = c.execute("SELECT * FROM renglones WHERE oc_folio=?", (folio,)).fetchall()
|
||||
if not rs:
|
||||
return
|
||||
con_algo = [r for r in rs if r["recibido_en"]]
|
||||
if not con_algo:
|
||||
return
|
||||
completa = all(r["recibido_en"] and _num(r["recibido"]) >= _num(r["cantidad"]) for r in rs)
|
||||
quien = con_algo[-1]["recibido_por"] or "equipo"
|
||||
cuando = max(r["recibido_en"] for r in con_algo)
|
||||
estado = "recibida" if completa else "recibida-parcial"
|
||||
if oc["estado"] != estado:
|
||||
c.execute("UPDATE ordenes SET estado=?, recepcion_en=?, recepcion_por=?, actualizado_en=? WHERE folio=?",
|
||||
(estado, cuando, quien, self.g["ahora"](), folio))
|
||||
falta = [f"{r['descripcion']}: llegaron {_num(r['recibido']):g} de {_num(r['cantidad']):g}"
|
||||
for r in rs if _num(r["recibido"]) < _num(r["cantidad"])]
|
||||
if completa:
|
||||
self.nota_pendiente(oc["tarea_id"], f"llegó todo lo de {folio}: recibido por {quien}",
|
||||
quien, estado="hecho")
|
||||
else:
|
||||
self.nota_pendiente(oc["tarea_id"],
|
||||
(f"llegó parte de {folio} ({quien}). Falta: " + "; ".join(falta))[:300], quien)
|
||||
|
||||
# ---------------------------------------------------------------- recibir desde el centro de mando
|
||||
def recibir(self, c, folio, lineas, quien):
|
||||
"""lineas = [{n, recibido}]. Recibe por el inventario lo que tiene envío; lo que no tiene ítem
|
||||
se anota aquí (así la requisición se puede cerrar igual). Devuelve (hechos, errores)."""
|
||||
oc = c.execute("SELECT * FROM ordenes WHERE folio=?", (folio,)).fetchone()
|
||||
if not oc:
|
||||
return [], [f"No existe {folio}."]
|
||||
if oc["estado"] == "cancelada":
|
||||
return [], [f"{folio} está cancelada en el cotizador."]
|
||||
hechos, errores = [], []
|
||||
for ln in lineas:
|
||||
n = self.g["_entero"](ln.get("n"), -1)
|
||||
r = c.execute("SELECT * FROM renglones WHERE oc_folio=? AND n=?", (folio, n)).fetchone()
|
||||
if not r:
|
||||
errores.append(f"El renglón {n} no existe en {folio}.")
|
||||
continue
|
||||
if r["recibido_en"]:
|
||||
continue # ya se había recibido: no se recibe dos veces
|
||||
cant = _num(ln.get("recibido"), -1)
|
||||
if cant < 0 or cant > _num(r["cantidad"]):
|
||||
errores.append(f"Renglón {n}: se pidieron {_num(r['cantidad']):g} {r['unidad'] or ''}"
|
||||
.strip() + f"; no pueden llegar {ln.get('recibido')}.")
|
||||
continue
|
||||
ahora = self.g["ahora"]()
|
||||
env = self.envio(r["envio_id"])
|
||||
if env and env.get("estado") == "en-camino":
|
||||
entero = _entero_exacto(cant) if cant else 0
|
||||
if entero is None:
|
||||
errores.append(f"Renglón {n}: el inventario cuenta enteros.")
|
||||
continue
|
||||
it = self.g["_leer_json"](self.g["_ruta_registro"](self.g["INV_ITEMS"](), env.get("item_id")))
|
||||
if not it:
|
||||
errores.append(f"Renglón {n}: ya no existe el ítem del envío.")
|
||||
continue
|
||||
self.g["_registrar_movimiento"](it, "recepcion", entero,
|
||||
f"llegó la compra {folio}"[:200], folio, quien,
|
||||
sitio=env.get("destino"), envio=env)
|
||||
c.execute("UPDATE renglones SET recibido=?, recibido_en=?, recibido_por=? WHERE oc_folio=? AND n=?",
|
||||
(cant, ahora, quien, folio, n))
|
||||
hechos.append(n)
|
||||
if hechos:
|
||||
self.cerrar_si_llego_todo(c, folio)
|
||||
return hechos, errores
|
||||
|
||||
# ---------------------------------------------------------------- avisos del cotizador
|
||||
def guardar_oc(self, c, d, tipo, quien, prueba):
|
||||
"""Alta o actualización de la OC con lo que trae el aviso. Devuelve (oc, error)."""
|
||||
g = self.g
|
||||
folio = g["_texto"](d.get("folio"), 40).upper()
|
||||
if not folio:
|
||||
return None, "Falta el folio de la orden de compra."
|
||||
sitio = self.sitio_portal(d.get("sitio"))
|
||||
if not sitio:
|
||||
return None, (f"Sitio inválido: {g['_texto'](d.get('sitio'), 20) or '(vacío)'} "
|
||||
"(usa VER, ZLO, ESE, LZC o QRO)")
|
||||
ahora = g["ahora"]()
|
||||
campos = {"req_folio": g["_texto"](d.get("req_folio"), 40).upper(),
|
||||
"proyecto_folio": g["_texto"](d.get("proyecto_folio"), 40).upper(),
|
||||
"proyecto_clave": g["_texto"](d.get("proyecto_clave"), 20).upper(),
|
||||
"sitio": self.codigo(sitio), "sitio_portal": sitio,
|
||||
"proveedor": g["_texto"](d.get("proveedor"), 120),
|
||||
"fecha_entrega": g["_texto"](d.get("fecha_entrega"), 10),
|
||||
"paqueteria": g["_texto"](d.get("paqueteria"), 60),
|
||||
"guia": g["_texto"](d.get("guia"), 60),
|
||||
"solicitante": g["_texto"](d.get("solicitante"), 120),
|
||||
"autorizo": g["_texto"](d.get("autorizo"), 120),
|
||||
"prueba": 1 if prueba else 0, "actualizado_en": ahora}
|
||||
if campos["fecha_entrega"] and not g["FECHA_RE"].match(campos["fecha_entrega"]):
|
||||
return None, "La fecha de entrega va como AAAA-MM-DD."
|
||||
base = c.execute("SELECT * FROM ordenes WHERE folio=?", (folio,)).fetchone()
|
||||
if base and base["sitio_portal"] != sitio and base["estado"] not in ("autorizada", "pagada"):
|
||||
return None, (f"{folio} ya iba a {self.codigo(base['sitio_portal'])}: el sitio no se cambia "
|
||||
"con el material en camino. Cancélala y haz otra.")
|
||||
estado = {"oc.autorizada": "autorizada", "oc.pagada": "pagada", "oc.en_camino": "en-camino",
|
||||
"oc.cancelada": "cancelada"}.get(tipo)
|
||||
if estado and base and base["estado"] in ("recibida", "recibida-parcial") and estado != "cancelada":
|
||||
estado = None # ya llegó: el estado del portal manda
|
||||
if estado:
|
||||
campos["estado"] = estado
|
||||
if estado == "en-camino":
|
||||
campos["en_camino_en"] = ahora
|
||||
if tipo == "req.cerrada":
|
||||
campos["req_cerrada"] = 1
|
||||
if base:
|
||||
c.execute("UPDATE ordenes SET " + ", ".join(k + "=?" for k in campos) + " WHERE folio=?",
|
||||
tuple(campos.values()) + (folio,))
|
||||
else:
|
||||
campos.update(folio=folio, creado_en=ahora, estado=campos.get("estado") or "autorizada")
|
||||
c.execute("INSERT INTO ordenes (" + ", ".join(campos) + ") VALUES ("
|
||||
+ ", ".join("?" * len(campos)) + ")", tuple(campos.values()))
|
||||
self.guardar_renglones(c, folio, d.get("renglones"))
|
||||
return c.execute("SELECT * FROM ordenes WHERE folio=?", (folio,)).fetchone(), None
|
||||
|
||||
def guardar_renglones(self, c, folio, renglones):
|
||||
"""Los renglones se actualizan, no se borran: un renglón que ya tiene envío conserva su envío
|
||||
aunque el cotizador vuelva a mandar la OC."""
|
||||
g = self.g
|
||||
if not isinstance(renglones, list):
|
||||
return
|
||||
for i, r in enumerate(renglones[:200], start=1):
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
n = g["_entero"](r.get("n"), i)
|
||||
desc = g["_texto"](r.get("descripcion"), 200)
|
||||
unidad = g["_texto"](r.get("unidad"), 10)
|
||||
cant = _num(r.get("cantidad"), 0)
|
||||
item = g["_texto"](r.get("item"), 40).upper() or None
|
||||
vieja = c.execute("SELECT * FROM renglones WHERE oc_folio=? AND n=?", (folio, n)).fetchone()
|
||||
if vieja:
|
||||
# el ítem ligado en el centro de mando gana: aquí es donde se ve el inventario
|
||||
item = vieja["item_codigo"] or item
|
||||
if vieja["envio_id"]:
|
||||
cant = _num(vieja["cantidad"], cant)
|
||||
c.execute("UPDATE renglones SET descripcion=?, unidad=?, cantidad=?, item_codigo=? "
|
||||
"WHERE oc_folio=? AND n=?", (desc, unidad, cant, item, folio, n))
|
||||
else:
|
||||
c.execute("INSERT INTO renglones (oc_folio, n, descripcion, unidad, cantidad, item_codigo) "
|
||||
"VALUES (?,?,?,?,?,?)", (folio, n, desc, unidad, cant, item))
|
||||
|
||||
# ---------------------------------------------------------------- salida para la página
|
||||
def ficha(self, c, oc, con_renglones=True):
|
||||
d = {k: oc[k] for k in oc.keys()}
|
||||
d["prueba"] = bool(d.get("prueba"))
|
||||
d["req_cerrada"] = bool(d.get("req_cerrada"))
|
||||
d["sitio_nombre"] = self.nombre_sitio(oc["sitio_portal"])
|
||||
if con_renglones:
|
||||
rs = [{k: r[k] for k in r.keys()} for r in
|
||||
c.execute("SELECT * FROM renglones WHERE oc_folio=? ORDER BY n", (oc["folio"],)).fetchall()]
|
||||
for r in rs:
|
||||
r["pendiente"] = _num(r["cantidad"]) - _num(r["recibido"]) if r["recibido_en"] else _num(r["cantidad"])
|
||||
d["renglones"] = rs
|
||||
d["por_ligar"] = [r["n"] for r in rs if not r["item_codigo"]]
|
||||
d["recibidos"] = sum(1 for r in rs if r["recibido_en"])
|
||||
return d
|
||||
|
||||
|
||||
def registrar(app, g):
|
||||
C = Compras(g)
|
||||
try:
|
||||
C.arrancar()
|
||||
except Exception as e: # noqa: BLE001 — sin base, el resto del sitio sigue igual
|
||||
app.logger.error("compras.py: no se pudo abrir %s: %s", C.ruta, e)
|
||||
|
||||
# ------------------------------------------------------------ firma
|
||||
def firmado():
|
||||
"""Revisa la firma del cotizador. Devuelve (cuerpo_en_texto, None) o (None, respuesta de error)."""
|
||||
sec = _secreto()
|
||||
if not sec:
|
||||
return None, (jsonify(ok=False, error="El centro de mando no tiene configurado "
|
||||
"COTIZADOR_SECRETO."), 503)
|
||||
crudo = request.get_data(cache=True) or b""
|
||||
if len(crudo) > MAX_CUERPO:
|
||||
return None, (jsonify(ok=False, error="Aviso demasiado grande."), 413)
|
||||
tiempo = request.headers.get("X-Hbox-Tiempo", "")
|
||||
firma = request.headers.get("X-Hbox-Firma", "")
|
||||
if firma.startswith("sha256="):
|
||||
firma = firma[7:]
|
||||
try:
|
||||
t = datetime.datetime.fromisoformat(tiempo)
|
||||
except ValueError:
|
||||
return None, (jsonify(ok=False, error="Firma inválida o vencida"), 401)
|
||||
ahora = datetime.datetime.now(t.tzinfo) if t.tzinfo else datetime.datetime.now()
|
||||
if abs((ahora - t).total_seconds()) > VENTANA_FIRMA:
|
||||
return None, (jsonify(ok=False, error="Firma inválida o vencida"), 401)
|
||||
esperada = hmac.new(sec.encode(), tiempo.encode() + b"\n" + crudo, hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(esperada, (firma or "").strip().lower()):
|
||||
return None, (jsonify(ok=False, error="Firma inválida o vencida"), 401)
|
||||
return crudo.decode("utf-8", "replace"), None
|
||||
|
||||
# ------------------------------------------------------------ avisos del cotizador
|
||||
@app.post("/api/integracion/cotizador/evento")
|
||||
def integracion_evento():
|
||||
texto, err = firmado()
|
||||
if err:
|
||||
return err
|
||||
try:
|
||||
d = json.loads(texto or "{}")
|
||||
except ValueError:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
if not isinstance(d, dict):
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
d = _sin_montos(d) # los montos nunca entran al centro de mando
|
||||
tipo = g["_texto"](d.get("tipo"), 30)
|
||||
if tipo not in TIPOS:
|
||||
return jsonify(ok=False, error=f"Tipo de aviso inválido: {tipo or '(vacío)'}. "
|
||||
"Usa " + ", ".join(TIPOS) + "."), 400
|
||||
evento_id = g["_texto"](d.get("evento_id"), 120)
|
||||
if not evento_id:
|
||||
return jsonify(ok=False, error="Falta evento_id."), 400
|
||||
oc_in = d.get("oc") if isinstance(d.get("oc"), dict) else {}
|
||||
quien = g["_texto"](d.get("quien"), 120) or "cotizador"
|
||||
prueba = bool(d.get("prueba"))
|
||||
with C.con() as c:
|
||||
if c.execute("SELECT 1 FROM eventos WHERE evento_id=?", (evento_id,)).fetchone():
|
||||
return jsonify(ok=True, repetido=True)
|
||||
oc, error = C.guardar_oc(c, oc_in, tipo, quien, prueba)
|
||||
if error:
|
||||
return jsonify(ok=False, error=error), 400
|
||||
folio = oc["folio"]
|
||||
if prueba:
|
||||
# las rondas del proyecto de PRUEBAS no tocan el inventario ni el tablero (contrato §prueba)
|
||||
c.execute("INSERT INTO eventos (evento_id, oc_folio, tipo, cuando, quien, origen, datos) "
|
||||
"VALUES (?,?,?,?,?,?,?)",
|
||||
(evento_id, folio, tipo, g["ahora"](), quien, "cotizador", json.dumps(d)[:20000]))
|
||||
return jsonify(ok=True, prueba=True, estado_portal=oc["estado"], tarea_id=None,
|
||||
por_ligar=[r["n"] for r in c.execute(
|
||||
"SELECT n FROM renglones WHERE oc_folio=? AND item_codigo IS NULL", (folio,))])
|
||||
tarea_id = oc["tarea_id"]
|
||||
if tipo == "oc.autorizada" and not tarea_id:
|
||||
tarea_id = C.abrir_pendiente(c.execute("SELECT * FROM ordenes WHERE folio=?",
|
||||
(folio,)).fetchone(), quien)
|
||||
c.execute("UPDATE ordenes SET tarea_id=? WHERE folio=?", (tarea_id, folio))
|
||||
if tipo == "oc.en_camino":
|
||||
C.crear_envios(c, folio, quien)
|
||||
C.nota_pendiente(tarea_id, (f"el proveedor ya lo mandó"
|
||||
+ (f" ({oc['paqueteria']} {oc['guia']})" if oc["guia"] else "")
|
||||
+ f"; llega ≈ {oc['fecha_entrega'] or 'sin fecha'}")[:300], quien)
|
||||
elif tipo == "oc.pagada":
|
||||
C.nota_pendiente(tarea_id, "pagada en el cotizador", quien)
|
||||
elif tipo == "oc.cancelada":
|
||||
C.cancelar_envios(c, folio, quien, f"{folio} cancelada en el cotizador")
|
||||
C.nota_pendiente(tarea_id, f"{folio} se canceló en el cotizador: ya no llega.",
|
||||
quien, estado="hecho")
|
||||
c.execute("INSERT INTO eventos (evento_id, oc_folio, tipo, cuando, quien, origen, datos) "
|
||||
"VALUES (?,?,?,?,?,?,?)",
|
||||
(evento_id, folio, tipo, g["ahora"](), quien, "cotizador", json.dumps(d)[:20000]))
|
||||
fila = c.execute("SELECT * FROM ordenes WHERE folio=?", (folio,)).fetchone()
|
||||
por_ligar = [r["n"] for r in c.execute(
|
||||
"SELECT n FROM renglones WHERE oc_folio=? AND item_codigo IS NULL ORDER BY n", (folio,))]
|
||||
return jsonify(ok=True, estado_portal=fila["estado"], tarea_id=fila["tarea_id"], por_ligar=por_ligar)
|
||||
|
||||
@app.get("/api/integracion/inventario/items")
|
||||
def integracion_items():
|
||||
_, err = firmado()
|
||||
if err:
|
||||
return err
|
||||
items = [{"codigo": it.get("codigo"), "descripcion": it.get("descripcion"),
|
||||
"unidad": it.get("unidad"), "categoria": it.get("categoria")}
|
||||
for it in C.items() if it.get("codigo")]
|
||||
items.sort(key=lambda x: (x["categoria"] or "", x["codigo"] or ""))
|
||||
return jsonify(ok=True, items=items)
|
||||
|
||||
@app.get("/api/integracion/cotizador/recepciones")
|
||||
def integracion_recepciones():
|
||||
_, err = firmado()
|
||||
if err:
|
||||
return err
|
||||
desde = g["_texto"](request.args.get("desde"), 40)
|
||||
hasta = g["ahora"]()
|
||||
with C.con() as c:
|
||||
C.conciliar(c)
|
||||
q = ("SELECT DISTINCT oc_folio FROM renglones WHERE recibido_en IS NOT NULL"
|
||||
+ (" AND recibido_en > ?" if desde else ""))
|
||||
folios = [r[0] for r in c.execute(q, (desde,) if desde else ()).fetchall()]
|
||||
out = []
|
||||
for f in sorted(folios):
|
||||
oc = c.execute("SELECT * FROM ordenes WHERE folio=?", (f,)).fetchone()
|
||||
if not oc or oc["prueba"]:
|
||||
continue
|
||||
rs = c.execute("SELECT * FROM renglones WHERE oc_folio=? ORDER BY n", (f,)).fetchall()
|
||||
con_algo = [r for r in rs if r["recibido_en"]]
|
||||
out.append({"oc_folio": f, "req_folio": oc["req_folio"], "sitio": oc["sitio"],
|
||||
"completa": all(r["recibido_en"] and _num(r["recibido"]) >= _num(r["cantidad"])
|
||||
for r in rs),
|
||||
"recibio": (con_algo[-1]["recibido_por"] if con_algo else ""),
|
||||
"cuando": max((r["recibido_en"] for r in con_algo), default=""),
|
||||
"renglones": [{"n": r["n"], "pedido": _num(r["cantidad"]),
|
||||
"recibido": _num(r["recibido"]) if r["recibido_en"] else 0}
|
||||
for r in rs]})
|
||||
return jsonify(ok=True, hasta=hasta, recepciones=out)
|
||||
|
||||
# ------------------------------------------------------------ la página Compras
|
||||
@app.get("/api/compras")
|
||||
def compras_listar():
|
||||
sitio = C.sitio_portal(request.args.get("sitio")) if request.args.get("sitio") else None
|
||||
estado = g["_texto"](request.args.get("estado"), 20).lower()
|
||||
with C.con() as c:
|
||||
C.conciliar(c)
|
||||
filas = c.execute("SELECT * FROM ordenes WHERE prueba=0 ORDER BY creado_en DESC").fetchall()
|
||||
ocs = [C.ficha(c, f) for f in filas]
|
||||
if sitio:
|
||||
ocs = [o for o in ocs if o["sitio_portal"] == sitio]
|
||||
if estado:
|
||||
ocs = [o for o in ocs if o["estado"] == estado]
|
||||
resumen = {}
|
||||
for o in ocs:
|
||||
r = resumen.setdefault(o["sitio_portal"], {k: 0 for k in ESTADOS_OC})
|
||||
r[o["estado"]] = r.get(o["estado"], 0) + 1
|
||||
items = [{"codigo": it.get("codigo"), "descripcion": it.get("descripcion"),
|
||||
"unidad": it.get("unidad"), "categoria": it.get("categoria")}
|
||||
for it in C.items() if it.get("codigo")]
|
||||
items.sort(key=lambda x: (x["categoria"] or "", x["codigo"] or ""))
|
||||
return jsonify(ok=True, total=len(ocs), ordenes=ocs, resumen=resumen, items=items,
|
||||
estados=ESTADOS_OC, user=g["current_user"](), admin=g["is_admin"](),
|
||||
integracion=bool(_secreto()),
|
||||
sitios=[{"id": i, "nombre": n, "corto": c2, "codigo": C.codigo(i)}
|
||||
for i, n, c2 in g["SITIOS_TAREA"]])
|
||||
|
||||
@app.post("/api/compras/ligar")
|
||||
def compras_ligar():
|
||||
u = g["quien"]()
|
||||
if not u:
|
||||
return g["_sin_usuario"]()
|
||||
d = g["_json_body"]()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
folio = g["_texto"](d.get("oc"), 40).upper()
|
||||
n = g["_entero"](d.get("n"), -1)
|
||||
codigo = g["_texto"](d.get("item"), 40).upper()
|
||||
if not C.item_por_codigo(codigo):
|
||||
return jsonify(ok=False, error=f"No existe el ítem {codigo} en el inventario."), 404
|
||||
with C.con() as c:
|
||||
oc = c.execute("SELECT * FROM ordenes WHERE folio=?", (folio,)).fetchone()
|
||||
r = c.execute("SELECT * FROM renglones WHERE oc_folio=? AND n=?", (folio, n)).fetchone()
|
||||
if not oc or not r:
|
||||
return jsonify(ok=False, error=f"No existe el renglón {n} de {folio}."), 404
|
||||
if r["envio_id"]:
|
||||
return jsonify(ok=False, error="Ese renglón ya está en camino en el inventario."), 409
|
||||
if r["recibido_en"]:
|
||||
return jsonify(ok=False, error="Ese renglón ya se recibió."), 409
|
||||
c.execute("UPDATE renglones SET item_codigo=?, nota=NULL WHERE oc_folio=? AND n=?",
|
||||
(codigo, folio, n))
|
||||
if oc["estado"] == "en-camino": # ya venía en camino: se manda ahora que ya tiene ítem
|
||||
C.crear_envio(c, oc, c.execute("SELECT * FROM renglones WHERE oc_folio=? AND n=?",
|
||||
(folio, n)).fetchone(), u)
|
||||
ficha = C.ficha(c, c.execute("SELECT * FROM ordenes WHERE folio=?", (folio,)).fetchone())
|
||||
return jsonify(ok=True, orden=ficha)
|
||||
|
||||
@app.post("/api/compras/recibir")
|
||||
def compras_recibir():
|
||||
u = g["quien"]()
|
||||
if not u:
|
||||
return g["_sin_usuario"]()
|
||||
d = g["_json_body"]()
|
||||
if d is None:
|
||||
return jsonify(ok=False, error="JSON inválido"), 400
|
||||
folio = g["_texto"](d.get("oc"), 40).upper()
|
||||
lineas = d.get("renglones")
|
||||
if not isinstance(lineas, list) or not lineas:
|
||||
return jsonify(ok=False, error="Dime qué llegó: al menos un renglón."), 400
|
||||
with C.con() as c:
|
||||
hechos, errores = C.recibir(c, folio, lineas, u)
|
||||
if errores and not hechos:
|
||||
return jsonify(ok=False, error=" ".join(errores)), 400
|
||||
ficha = C.ficha(c, c.execute("SELECT * FROM ordenes WHERE folio=?", (folio,)).fetchone())
|
||||
return jsonify(ok=True, orden=ficha, recibidos=hechos, avisos=errores)
|
||||
632
backend/lotes/2026-10-28_junta-direccion.json
Normal file
632
backend/lotes/2026-10-28_junta-direccion.json
Normal file
@ -0,0 +1,632 @@
|
||||
{
|
||||
"lote": "2026-10-28_junta-direccion",
|
||||
"autor": "claude",
|
||||
"motivo": "Junta de dirección del 28-oct (10:17): flujo de compras acordado, arranque de requisiciones el 29-oct, y lo que el equipo reportó del sitio",
|
||||
"fuente": "/inventario/flujo-compras-28oct/",
|
||||
"cambios": [
|
||||
{
|
||||
"id": "t-timsa-tramite-vehiculo",
|
||||
"poner": {
|
||||
"estado": "hecho"
|
||||
},
|
||||
"nota": "Junta del 28-oct: el trámite del vehículo en Manzanillo ya quedó."
|
||||
},
|
||||
{
|
||||
"id": "t-eit-material",
|
||||
"poner": {
|
||||
"estado": "hecho"
|
||||
},
|
||||
"nota": "Junta del 28-oct: el material que salió de Manzanillo ya llegó a Ensenada."
|
||||
},
|
||||
{
|
||||
"id": "t-inventario-inicial",
|
||||
"poner": {
|
||||
"estado": "hecho"
|
||||
},
|
||||
"nota": "Junta del 28-oct: Jesús ya contó y registró todo en Querétaro (tarjetas, módulos, gabinetes), por anaquel. El conteo se vuelve mensual: t-conteo-mensual."
|
||||
},
|
||||
{
|
||||
"id": "t-icave-unidades",
|
||||
"poner": {
|
||||
"estado": "hecho"
|
||||
},
|
||||
"nota": "Junta del 28-oct: ICAVE ya tiene sus unidades confirmadas. Lo que falta es el acta del montacargas (t-registrar-instaladas)."
|
||||
},
|
||||
{
|
||||
"id": "t-trips-produccion",
|
||||
"poner": {
|
||||
"estado": "hecho"
|
||||
},
|
||||
"nota": "Junta del 28-oct: Jacobo ya dejó los recorridos en el monitor."
|
||||
},
|
||||
{
|
||||
"id": "t-compras-fase1",
|
||||
"poner": {
|
||||
"estado": "en-curso",
|
||||
"fecha_limite": "2026-10-29"
|
||||
},
|
||||
"nota": "Construida y probada: backend/compras.py (SQLite, 3 rutas firmadas, envíos en camino, pendiente por OC, cierre al recibir) y la página Compras, con 58 pruebas en backend/tests-1028.sh. Falta que Leo la publique y ponga el secreto; se adelanta porque las requisiciones arrancan el 29-oct."
|
||||
},
|
||||
{
|
||||
"id": "t-cotizador-oc-sitio",
|
||||
"poner": {
|
||||
"fecha_limite": "2026-10-30"
|
||||
},
|
||||
"nota": "Se adelanta del 6-nov: en la junta Leo dijo que a partir del 29-oct todas las requisiciones de HBOX se hacen por el sistema. El contrato con el centro de mando no cambió; lo que cambió es el proceso (dos puertas de Adriana). Primero lo que bloquea el arranque: t-cotizador-cotizacion-adjunta."
|
||||
},
|
||||
{
|
||||
"id": "d-27-3-sitios-oc",
|
||||
"nota": "En la junta Leo ya agregó el campo de sitio en el cotizador, así que D-27-3 queda de hecho. Falta el «sí» formal de D-27-4 (sin montos en el sitio, confirmado en la junta) y D-27-5 (solo PRY-2026-0002-DRO)."
|
||||
},
|
||||
{
|
||||
"id": "t-cotizacion-pedimentos-jlc",
|
||||
"poner": {
|
||||
"titulo": "Cotizar con Thera Mercurio tres pedimentos A1 de JLC (US$7.5k / 18.9k / 9.7k) y confirmar expediente y padrón"
|
||||
},
|
||||
"nota": "Junta del 28-oct: el agente aduanal es Thera Mercurio, no Grupo Lorenzo. El costeo del 26-oct supone Grupo Lorenzo (patente 3927) y MX$6,500 por pedimento: hay que rehacer esas cifras con la cotización real."
|
||||
},
|
||||
{
|
||||
"id": "t-lct-herreria",
|
||||
"nota": "Junta del 28-oct: Adriana dice que Jesús y Tania ya mandaron la cotización de las bases de las RTG; Leo dice que nunca le llegó. Hay que encontrarla o volver a pedirla. Las medidas y fotos ya están y las bases son iguales mientras no cambie el gabinete. Sigue bloqueada por el pago."
|
||||
},
|
||||
{
|
||||
"id": "t-juntas",
|
||||
"nota": "Regla de la junta del 28-oct: toda junta del proyecto tiene que invitar a leo@helgen.tech y traer prendido el Gemini para tomar notas. Leo no necesita conectarse: el agente diario mueve el tablero con las notas de todas las juntas del día más lo que cada quien conteste en Standup."
|
||||
},
|
||||
{
|
||||
"id": "t-entregas",
|
||||
"nota": "Efraín reportó un pendiente con un archivo que no se podía abrir. Se revisaron los 228 enlaces de los 139 pendientes: ninguno roto. Lo que faltaba eran 33 documentos fechados sin publicar (ya están en Historia). Efraín: dinos cuál pendiente era."
|
||||
},
|
||||
{
|
||||
"buscar": [
|
||||
"galería",
|
||||
"actas"
|
||||
],
|
||||
"nota": "Junta del 28-oct: Jesús Romero reportó justo esto — el formulario no deja escoger fotos de la galería. Queda cubierto con este pendiente. Mientras tanto se le reabre el acta para que suba las que ya guardó."
|
||||
},
|
||||
{
|
||||
"id": "t-registrar-instaladas",
|
||||
"nota": "Junta del 28-oct: siguen faltando actas. En Instalaciones hay menos actas que unidades en línea; la del montacargas de ICAVE la tiene que llenar Alán."
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-cotizador-cotizacion-adjunta",
|
||||
"titulo": "Cotizador: quitar que la requisición no se pueda guardar sin cotización adjunta",
|
||||
"pista": "compras",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "efrain, leo",
|
||||
"campeon": "efrain",
|
||||
"fecha_limite": "2026-10-29",
|
||||
"sitios": [
|
||||
"qro"
|
||||
],
|
||||
"etiquetas": [
|
||||
"cotizador",
|
||||
"arranque"
|
||||
],
|
||||
"descripcion": "Está al revés: la requisición la levanta quien necesita el material y la cotización la adjunta compras después. Hoy el formulario no deja guardar hasta que se sube una cotización, así que nadie del campo puede pedir nada. Leo en la junta del 28-oct: «eso se tiene que modificar». Es lo primero porque bloquea el arranque del 29-oct.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "El flujo acordado",
|
||||
"url": "/inventario/flujo-compras-28oct/#lo-que-falta-del-lado-del-cotizador"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"cotización adjunta",
|
||||
"cotizacion adjunta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-cotizador-sitio-obligatorio",
|
||||
"titulo": "Cotizador: que el sitio de entrega sea obligatorio en los proyectos ligados (del lado del servidor)",
|
||||
"pista": "compras",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "efrain, leo",
|
||||
"campeon": "efrain",
|
||||
"fecha_limite": "2026-10-30",
|
||||
"sitios": [
|
||||
"qro"
|
||||
],
|
||||
"etiquetas": [
|
||||
"cotizador"
|
||||
],
|
||||
"descripcion": "Leo ya agregó el campo de sitio en la junta. Falta la regla que lo hace obligatorio cuando el proyecto está ligado (DROGHEDA), revisada en guardarRequisicion y no solo en la interfaz: una OC sin sitio no puede llegar al centro de mando. Códigos: VER Veracruz · ZLO Manzanillo · ESE Ensenada · LZC Lázaro Cárdenas · QRO Querétaro.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Instrucciones, paso 2",
|
||||
"url": "/inventario/integracion-cotizador-instrucciones/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"sitio de entrega sea obligatorio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-compras-secreto",
|
||||
"titulo": "Generar el secreto compartido del cotizador y ponerlo en los dos lados",
|
||||
"pista": "compras",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "leo",
|
||||
"fecha_limite": "2026-10-30",
|
||||
"sitios": [
|
||||
"qro"
|
||||
],
|
||||
"etiquetas": [
|
||||
"cotizador",
|
||||
"despliegue"
|
||||
],
|
||||
"descripcion": "openssl rand -hex 32. El mismo valor va como COTIZADOR_SECRETO en actas-hbox.service (ya está la línea, comentada) y como PORTAL_SECRETO en las propiedades del script del cotizador. Nunca en git ni en un chat. Sin él, las rutas /api/integracion/ contestan 503 con un mensaje claro y el resto del sitio funciona igual: no bloquea nada, pero hasta que esté no entran OC solas.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Qué se construyó",
|
||||
"url": "/inventario/flujo-compras-28oct/#lo-que-ya-esta-construido-fase-1-del-portal"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"secreto compartido"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-requisiciones-altas",
|
||||
"titulo": "Definir quién puede levantar requisiciones y mandarles sus accesos al cotizador",
|
||||
"pista": "gestion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "adriana, leo",
|
||||
"campeon": "adriana",
|
||||
"fecha_limite": "2026-10-30",
|
||||
"sitios": [
|
||||
"qro",
|
||||
"icave",
|
||||
"timsa",
|
||||
"eit",
|
||||
"lzc"
|
||||
],
|
||||
"etiquetas": [
|
||||
"cotizador",
|
||||
"arranque"
|
||||
],
|
||||
"descripcion": "En la junta Leo nombró a Jesús Romero, Miguel, Antonio, Daniel, Alán y Adriana, pero la lista quedó sin cerrar («con estos seis ya podemos»). Adriana la cierra y se mandan las credenciales del cotizador. Alán entra en lugar de Paola (D-28-3). Alcance por ahora: solo instalaciones de HBOX.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "El flujo y quién hace qué",
|
||||
"url": "/inventario/flujo-compras-28oct/#el-flujo-paso-por-paso"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"quién puede levantar requisiciones",
|
||||
"quien puede levantar requisiciones"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-sesion-general-sistema",
|
||||
"titulo": "Sesión general con todo el equipo: inventario y el sistema nuevo de requisiciones",
|
||||
"pista": "gestion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "adriana, leo",
|
||||
"campeon": "adriana",
|
||||
"fecha_limite": "2026-11-06",
|
||||
"sitios": [
|
||||
"qro",
|
||||
"icave",
|
||||
"timsa",
|
||||
"eit",
|
||||
"lzc"
|
||||
],
|
||||
"etiquetas": [
|
||||
"capacitacion"
|
||||
],
|
||||
"descripcion": "Son seis personas estrenando un sistema nuevo el mismo día. Leo en la junta: una sesión general con todos para explicarles lo de los inventarios y el sistema, «más digerido». Antes de eso Adriana programa sus juntas periódicas por sitio.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "El flujo",
|
||||
"url": "/inventario/flujo-compras-28oct/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"Sesión general",
|
||||
"Sesion general"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-acta-fotos-perdidas",
|
||||
"titulo": "El acta se pierde si sales de la pantalla y regresas: Daniel tuvo que llenarla otra vez",
|
||||
"pista": "gestion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "claude, leo",
|
||||
"campeon": "claude",
|
||||
"fecha_limite": "2026-10-30",
|
||||
"sitios": [
|
||||
"qro",
|
||||
"icave",
|
||||
"timsa",
|
||||
"eit",
|
||||
"lzc"
|
||||
],
|
||||
"etiquetas": [
|
||||
"sitio",
|
||||
"acta"
|
||||
],
|
||||
"descripcion": "Reportado por Daniel en la junta del 28-oct: llenó el acta con fotos en el celular, salió de la pantalla, regresó y no había nada. Es pérdida de trabajo en campo y es lo que hace que la gente no quiera llenar actas: va primero de los problemas del sitio. Guardar el borrador en el teléfono mientras se llena.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Lo que se reportó en la junta",
|
||||
"url": "/inventario/flujo-compras-28oct/#lo-que-el-equipo-reporto-del-sitio-en-la-junta"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"acta se pierde",
|
||||
"se pierde si sales"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-acceso-gibran",
|
||||
"titulo": "Gibran no puede entrar al sitio (su usuario es gibranlp y él mismo lo cambió)",
|
||||
"pista": "gestion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "leo, claude",
|
||||
"campeon": "leo",
|
||||
"fecha_limite": "2026-10-29",
|
||||
"sitios": [
|
||||
"qro"
|
||||
],
|
||||
"etiquetas": [
|
||||
"sitio",
|
||||
"accesos"
|
||||
],
|
||||
"descripcion": "Visto en vivo en la junta del 28-oct. Gibran es administrador del sitio (ACTAS_ADMINS=leo,gibranlp) y no puede entrar. Revisar su renglón en el htpasswd y en Equipo, y si su correo en Equipo coincide con el que está tecleando: desde el 27-oct se puede entrar con usuario o con correo.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Equipo",
|
||||
"url": "/tableros/equipo/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"Gibran no puede entrar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-contrasena-inicial-bandera",
|
||||
"titulo": "La bandera de «contraseña inicial» no se apaga aunque ya la hayan cambiado",
|
||||
"pista": "gestion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "claude, leo",
|
||||
"campeon": "claude",
|
||||
"fecha_limite": "2026-10-30",
|
||||
"sitios": [
|
||||
"qro"
|
||||
],
|
||||
"etiquetas": [
|
||||
"sitio",
|
||||
"accesos"
|
||||
],
|
||||
"descripcion": "Adriana en la junta del 28-oct: cambió su contraseña varias veces y el sitio le sigue diciendo que no la ha cambiado. El aviso sale de contrasena_inicial en su ficha de Equipo, que debería borrarse cuando la persona la cambia desde Mi cuenta. Mientras no se arregle, el banner molesta a todos y nadie le va a hacer caso cuando sí importe. De paso: t-cambio-contrasenas no se puede dar por cumplido con esta bandera rota.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Mi cuenta",
|
||||
"url": "/cuenta.html"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"contraseña inicial» no se apaga",
|
||||
"bandera de «contraseña inicial"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-sesiones-navegador",
|
||||
"titulo": "Dos sesiones al mismo tiempo en el mismo navegador no funcionan (cookies)",
|
||||
"pista": "gestion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "claude, leo",
|
||||
"campeon": "claude",
|
||||
"fecha_limite": "2026-11-04",
|
||||
"sitios": [
|
||||
"qro"
|
||||
],
|
||||
"etiquetas": [
|
||||
"sitio",
|
||||
"accesos"
|
||||
],
|
||||
"descripcion": "Leo en la junta del 28-oct: «es por mis cookies, tengo que arreglar eso». Estorba sobre todo para enseñar el sitio como otra persona (probar lo que ve un técnico sin cerrar la sesión propia). Menos urgente que las otras tres de accesos.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Cómo entra la gente al sitio",
|
||||
"url": "/tableros/equipo/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"Dos sesiones"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-acta-descargar",
|
||||
"titulo": "Poder descargar un acta como documento",
|
||||
"pista": "gestion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "claude",
|
||||
"fecha_limite": "2026-11-06",
|
||||
"sitios": [
|
||||
"qro",
|
||||
"icave",
|
||||
"timsa",
|
||||
"eit",
|
||||
"lzc"
|
||||
],
|
||||
"etiquetas": [
|
||||
"sitio",
|
||||
"acta"
|
||||
],
|
||||
"descripcion": "Lo pidió Adriana en la junta del 28-oct para mandar el acta al cliente y para la materialidad. Leo: «sí se puede, vamos a mejorarlo para que se vea un poquito mejor». Un PDF por acta con sus respuestas y sus fotos, desde la página de la unidad y desde Instalaciones.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Instalaciones",
|
||||
"url": "/tableros/instalaciones/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"descargar un acta",
|
||||
"descargar el acta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-iconos-monitor",
|
||||
"titulo": "Monitor: cambiar los iconos de las unidades al tipo de vehículo que son",
|
||||
"pista": "ingenieria",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "jacobo",
|
||||
"fecha_limite": "2026-11-04",
|
||||
"sitios": [
|
||||
"qro",
|
||||
"icave",
|
||||
"timsa",
|
||||
"eit",
|
||||
"lzc"
|
||||
],
|
||||
"etiquetas": [
|
||||
"monitor"
|
||||
],
|
||||
"descripcion": "Se lo encargó Leo a Jacobo en la junta del 28-oct: que cada unidad tenga el icono de lo que es (RTG, reach stacker, empty handler, tractoplana, montacargas) y no un icono genérico. El tipo de vehículo ya viene en el acta de instalación.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Instalaciones",
|
||||
"url": "/tableros/instalaciones/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"iconos de las unidades",
|
||||
"iconos"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-mapa-senal-celular",
|
||||
"titulo": "Mapear la intensidad de la señal celular dentro de cada puerto",
|
||||
"pista": "ingenieria",
|
||||
"estado": "en-curso",
|
||||
"responsable": "jacobo",
|
||||
"fecha_limite": "2026-11-13",
|
||||
"sitios": [
|
||||
"icave",
|
||||
"timsa",
|
||||
"eit",
|
||||
"lzc"
|
||||
],
|
||||
"etiquetas": [
|
||||
"campo",
|
||||
"diagnostico"
|
||||
],
|
||||
"descripcion": "Se lo encargó Leo a Jacobo: saber en qué zonas de cada puerto no hay señal, para tener mapeadas las zonas de incertidumbre. Es la evidencia que falta para confirmar por qué hay episodios en los que el CalAmp sigue reportando y el GPS no (t-icave-campo). Con las tarjetas nuevas y su acelerómetro los kilómetros se van a contar bien aunque se caiga el enlace.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Fallas abiertas en campo",
|
||||
"url": "/tableros/pendientes/?sitio=icave"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"señal celular",
|
||||
"senal celular"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-soporte-instalacion",
|
||||
"titulo": "Avisar en el grupo cuando empieza una instalación física, y que Jacobo y Jesús contesten",
|
||||
"pista": "campo",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "adriana, jacobo, jesus",
|
||||
"campeon": "adriana",
|
||||
"fecha_limite": "2026-10-31",
|
||||
"sitios": [
|
||||
"icave",
|
||||
"timsa",
|
||||
"eit",
|
||||
"lzc"
|
||||
],
|
||||
"etiquetas": [
|
||||
"campo",
|
||||
"proceso"
|
||||
],
|
||||
"descripcion": "Acuerdo de la junta del 28-oct. Los técnicos no logran localizar a Jacobo y a Jesús durante las instalaciones: avisan en el grupo, luego llaman y nadie contesta. El aviso tiene que pedir el apoyo, no solo anunciar la unidad: así lo escribió Adriana en la junta, «Jacobo Hernández y Jesús R., favor de estar al pendiente para la conexión del monitor». Ellos acusan de recibido en el momento, aunque sea con un pulgar, y contestan llamadas y mensajes.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Checklist de puesta en marcha",
|
||||
"url": "/campo/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"instalación física",
|
||||
"instalacion fisica"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-conteo-mensual",
|
||||
"titulo": "Conteo físico de inventario, una vez al mes en cada sitio",
|
||||
"pista": "produccion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "adriana, jesus",
|
||||
"campeon": "adriana",
|
||||
"fecha_limite": "2026-11-28",
|
||||
"sitios": [
|
||||
"qro",
|
||||
"icave",
|
||||
"timsa",
|
||||
"eit",
|
||||
"lzc"
|
||||
],
|
||||
"etiquetas": [
|
||||
"inventario",
|
||||
"recurrente"
|
||||
],
|
||||
"descripcion": "D-28-5 de la junta del 28-oct: Adriana pidió que el conteo fuera constante y Leo lo dejó mensual. Lo que lo cuadra es el «Conteo físico» de cada sitio en Inventario: lo que hay queda exactamente en ese número. Las salidas no se autorizan, se supervisan, y el conteo es lo que las verifica. Querétaro ya quedó (Jesús, por anaquel); los puertos salen con t-conteo-sitios.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Política de inventario",
|
||||
"url": "/inventario/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"una vez al mes",
|
||||
"mensual en cada sitio"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-eit-tercera-unidad",
|
||||
"titulo": "EIT: instalar la tercera unidad si el cliente presta la máquina",
|
||||
"pista": "campo",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "joel, miguel",
|
||||
"campeon": "joel",
|
||||
"fecha_limite": "2026-10-30",
|
||||
"sitios": [
|
||||
"eit"
|
||||
],
|
||||
"version_tarjeta": "V5.5",
|
||||
"etiquetas": [
|
||||
"instalacion"
|
||||
],
|
||||
"descripcion": "Creada por Leo en la junta del 28-oct. Ensenada ya tiene dos instaladas (la segunda se verifica hoy); la tercera depende de que presten la máquina. Al instalarla: acta de instalación y, después, prueba en sitio — solo una prueba aprobada cuenta para la meta del sitio. El número económico todavía no se sabe.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Instalaciones de EIT",
|
||||
"url": "/tableros/instalaciones/?sitio=eit"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"tercera unidad",
|
||||
"tercer HBOX"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-agente-pendientes-ruido",
|
||||
"titulo": "Afinar el agente diario: está creando pendientes sin sentido desde las notas de las juntas",
|
||||
"pista": "gestion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "claude, leo",
|
||||
"campeon": "claude",
|
||||
"fecha_limite": "2026-11-04",
|
||||
"sitios": [
|
||||
"qro"
|
||||
],
|
||||
"etiquetas": [
|
||||
"sitio",
|
||||
"agentes"
|
||||
],
|
||||
"descripcion": "En la junta del 28-oct Leo borró varios pendientes que el agente había creado solo de la minuta anterior («Armar 4 K de instalación», «¿quién autoriza el acceso a máquinas?»): frases sueltas de la transcripción convertidas en tareas. El tablero pierde credibilidad si hay que ir limpiándolo. El agente debe proponer el lote y no crear nada que no tenga responsable, verbo y por qué.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Reglas de los agentes",
|
||||
"url": "/agentes/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"pendientes sin sentido",
|
||||
"agente diario"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-credenciales-monitor-cliente",
|
||||
"titulo": "Mandar usuario y contraseña del monitor a quien lo pida del cliente",
|
||||
"pista": "gestion",
|
||||
"estado": "en-curso",
|
||||
"responsable": "adriana",
|
||||
"fecha_limite": "2026-10-29",
|
||||
"sitios": [
|
||||
"icave",
|
||||
"timsa",
|
||||
"eit",
|
||||
"lzc"
|
||||
],
|
||||
"etiquetas": [
|
||||
"cliente",
|
||||
"monitor"
|
||||
],
|
||||
"descripcion": "De la junta del 28-oct: José Javier pidió ver la unidad que acaban de instalar en una tractoplana; Adriana le manda hoy su usuario y contraseña del monitor. Cristian y Antonio ya los tienen. Leo crea usuarios nuevos en cuanto se los pidan.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Instalaciones",
|
||||
"url": "/tableros/instalaciones/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"usuario y contraseña del monitor",
|
||||
"credenciales del monitor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-docs-publicados-28oct",
|
||||
"titulo": "Publicados en el sitio los 33 documentos fechados que se habían quedado fuera",
|
||||
"pista": "gestion",
|
||||
"estado": "hecho",
|
||||
"responsable": "claude",
|
||||
"sitios": [
|
||||
"qro"
|
||||
],
|
||||
"etiquetas": [
|
||||
"sitio",
|
||||
"documentos"
|
||||
],
|
||||
"descripcion": "Por el reporte de Efraín. Se revisaron los 139 pendientes (228 enlaces) y los 1,469 enlaces dentro de los documentos publicados: ninguno roto. Lo que sí faltaba: 33 documentos fechados de las carpetas NN- que nunca entraron al MAP de sync-docs.sh, así que solo existían en la Mac de Leo. Ya están en Historia, en cuatro grupos (diseño de tarjetas, energía y firmware, pruebas y análisis, comunicación con el cliente). Además queda scripts/revisar-enlaces.py, que corre en cada deploy y no deja publicar si algún pendiente o documento manda a algo que no existe.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "Historia",
|
||||
"url": "/historia/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"documentos fechados"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
162
backend/lotes/2026-10-28_revision-ingenieria-tablero.json
Normal file
162
backend/lotes/2026-10-28_revision-ingenieria-tablero.json
Normal file
@ -0,0 +1,162 @@
|
||||
{
|
||||
"lote": "2026-10-28_revision-ingenieria-tablero",
|
||||
"autor": "claude",
|
||||
"motivo": "28-oct (Leo: «revisa las tareas de ingeniería del tablero contra las últimas auditorías»). Barrido de las 29 tarjetas de ingeniería abiertas y de las que se cerraron hoy, medidas contra HBox_PCB fb95d15, hbox-firmware 001d3d2 y los .kicad_pro de V5.6/V5.7.",
|
||||
"fuente": "/hardware/instrucciones-michelle-27oct/",
|
||||
"cambios": [
|
||||
{
|
||||
"id": "t-v6-c-modem",
|
||||
"si_estado": [
|
||||
"hecho"
|
||||
],
|
||||
"poner": {
|
||||
"estado": "por-hacer"
|
||||
},
|
||||
"nota": "28-oct, medido en fb95d15: C52 a 11.6 mm de U4.2 (meta 2), R28/R27 a 10.1/8.0 de U4.5 (2), C4 a 9.7 de L1.2 (3), C44/C34 a 12.1 de U4.2 (3), TP5-TP6 a 8.75. Nada del bloque C cambió desde el 26-oct: regresa a Por hacer."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-e-eth",
|
||||
"si_estado": [
|
||||
"hecho"
|
||||
],
|
||||
"poner": {
|
||||
"estado": "por-hacer"
|
||||
},
|
||||
"nota": "28-oct, medido en fb95d15: D12 a 13.5 mm de U7.1 (meta 2) y L2 a 15.4 de U7 (5), igual que el 26-oct. Las 6 vías del EP de U7 sí están (T45r). Regresa a Por hacer por las distancias."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-fgh1",
|
||||
"si_estado": [
|
||||
"hecho"
|
||||
],
|
||||
"poner": {
|
||||
"estado": "por-hacer"
|
||||
},
|
||||
"nota": "28-oct, medido en fb95d15: D23 a 26.5 mm de J15 (meta 5), Y2 a 41.1 de U1 (5) y Y1 a 12.0 de U1.30 (3). Sin cambios desde el 26-oct y va después de T29a (sáb 31): regresa a Por hacer."
|
||||
},
|
||||
{
|
||||
"id": "t-fw-070",
|
||||
"poner": {
|
||||
"descripcion": "Un solo binario para V5.6 y V5.7 (board_v56.h, -DHBOX_BOARD_V56; las dos tarjetas son iguales pin a pin).\n\nHecho al 28-oct: F-1 (0.7.0-pre, hbox-firmware a223481) — módem bajo control del MCU (EN/RST/LED/RTCM), NTC en PC5 x 0.5455, umbrales LFP 2.95/3.20 V e ignición por ADC con histéresis; el «#elif defined(HBOX_BOARD_V56)» ya está en el .ino. Con 0.7.0-pre entraron también F-6 (vida del módem por 4 flancos del LED o trama RTCM3, escalera de 180 s y LIMP), F-7 (Modo B solo con el módem vivo) y F-8 (apagado ordenado: K1 → MODEM_EN → PA1, también en softReset). Build de banco con Ethernet: 0.7.0-pre-eth (001d3d2), para el criterio 5 del piloto.\n\nFalta para 0.7.0: F-2 ciclo de respaldo (STOP + RTC a 10 min con LSE y LSI de respaldo, apagado ordenado, 2 reportes y a dormir; piso < 100 mW con el W5500 en power-down; 3–4 días) · F-3 driver del SC16IS752 (hoy solo se escribe EFCR_A = 0x30 al arranque; 1 día) · F-4 Ethernet de campo configurable (hoy solo el build de banco; 2 días) · F-5 driver del LIS2DH12 (hoy solo la presencia por WHO_AM_I). FOTA no entra en 0.7.0 (D-22-6)."
|
||||
},
|
||||
"nota": "28-oct: la descripción decía que faltaba el «#elif defined(HBOX_BOARD_V56)» y el commit, y los dos entraron el 27-oct (a223481). Se reescribe con el estado real por pieza: F-1, F-6, F-7 y F-8 hechos; quedan F-2, F-3, F-4 y F-5."
|
||||
},
|
||||
{
|
||||
"id": "t-banco-v5-procedimiento-t43",
|
||||
"poner": {
|
||||
"descripcion": "Procedimiento de banco del piloto V5.6/V5.7 con 0.7.0 y formularios en la numeración de T43.\n\nPublicado el 27 y 28 de octubre: el procedimiento de prueba y flasheo (los 8 criterios del piloto en §10 y la fuente a bordo de la V5.7 en §10.2: U15 < 110 °C, D21 < 100 °C, 9/12/24/32 V con 2.8 A), las actas de ensamble de V5.6 y de V5.7, el acta del banco del piloto (PILOTO-V5-6 y PILOTO-V5-7) y los formularios nuevos (formularios/prueba-v56v57.js, ensamble-v56v57.js, piloto.html), ya con 0.7.0-pre y 0.7.0-pre-eth. Los procedimientos viejos de la V5.5 se quedan con su numeración y su tabla V5.5 → V5.6/V5.7.\n\nFalta: la lectura de Jesús Romero, la de Gibran como Hardware Lead (t-banco-revision-hw) y ajustar el procedimiento con la primera tarjeta real del piloto. Con 0.7.0-pre se pueden 6 de los 8 criterios: el 3 (10 min en batería) espera F-2 y el 5 (Ethernet 24 h) el build -eth."
|
||||
},
|
||||
"nota": "28-oct: la descripción seguía diciendo que el procedimiento y los formularios no existían; se publicaron el 27-28 oct y su propia nota del 27-oct ya lo decía. Se reescribe con lo que falta de verdad: las dos lecturas y el ajuste con la primera tarjeta."
|
||||
},
|
||||
{
|
||||
"id": "t-decisiones-puertos-d10",
|
||||
"poner": {
|
||||
"titulo": "Decidir: D-19-1 puertos de la v6 a V5.6/V5.7, D-19-3 auto-sync de KiCad, D-19-4 strap HV24",
|
||||
"descripcion": "D-19-1 puertos: portar U32, D39, R147, CTS a GND, J10.8 y el LED (≈ 2–3 h de esquemático y ≈ medio día de layout por tarjeta; designadores de antes de T43, ver la tabla viejo → nuevo). Si no se portan: mitigaciones de firmware y el RS-485 no se usa en buses compartidos.\n\nD-19-3: dejar kicad/ fuera del auto-sync de las 18:00 (el LaunchAgent ya commiteó T43 por su cuenta).\n\nD-19-4: strap en PA9 para que el firmware distinga la v6 (D8, el SC7A20, ya no sirve: V5.6 y V5.7 también lo traen).\n\nAlta: Claude. Ya resueltas y fuera de este pendiente: D-19-2 (Dbk2 fuera del esquemático, T31), D-21-1 (C130 en U7.2, T32) y D10 → SMBJ33CA, que entró con T42a (C83325) y que desde T43 se llama D18."
|
||||
},
|
||||
"nota": "28-oct: D10 salió del título y de la descripción — se resolvió con D-22-2 y entró aplicado en T42a (51a2cc6), y desde T43 la pieza es D18. Quedan D-19-1, D-19-3 y D-19-4."
|
||||
},
|
||||
{
|
||||
"id": "t-v57-prototipos",
|
||||
"poner": {
|
||||
"bloqueada_por": [
|
||||
"t-fw-070"
|
||||
]
|
||||
},
|
||||
"nota": "28-oct: se quita T261022-2a06 de los bloqueadores (está en Hecho desde la pasada 6: commit y push de T34–T37 ya hechos). Queda bloqueada solo por el firmware 0.7.0."
|
||||
},
|
||||
{
|
||||
"id": "t-fota-f3-servidor",
|
||||
"poner": {
|
||||
"bloqueada_por": []
|
||||
},
|
||||
"nota": "28-oct: se quita d-22-6-fota de los bloqueadores (la decisión ya está en Hecho). Lo que falta para arrancar no es una decisión sino la pregunta 1 de D-22-6: qué software corre hoy el caster de Helgen."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-t29",
|
||||
"poner": {
|
||||
"descripcion": "Plan de fines de semana §5.4, sobre el commit del viernes de Michelle. Se prueba en una copia con DRC y se aplica con KiCad cerrado y sin cambios sin subir.\nT29a (sáb 31-oct, 8:00): vías de GND bajo U6 (≥ 30) y U18 (≥ 24) donde falten, vías en los pads de tierra de J13, J8 y D8, y las pistas de In1 que se puedan pasar a B.Cu sin choque.\nT29b (dom 1-nov, 8:00): costura general de tierra (meta ≥ 400 vías; ningún punto a > 5 mm de una vía).\nMichelle hizo 146 vías de GND a mano el 26-oct: T29 solo agrega lo que falte."
|
||||
},
|
||||
"nota": "28-oct: se quita el «con (b) en D-26-1, una semana después». Leo eligió (a) el 27-oct a las 13:06, así que las fechas son el 31-oct y el 1-nov, no una semana después."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-h2-puerta",
|
||||
"poner": {
|
||||
"descripcion": "Plan de fines de semana §7 (domingo 1-nov; D-26-1 quedó en (a)). Después de T29b y de git pull:\n1. Serigrafía: los 12 designadores que no cupieron con T44 e «IGN 12-24V»; nombres en cada conector (L-59); revisar en 3D la polaridad de D34, D8 y Q9.\n2. Tres fiduciales (cobre de 1.0 mm, máscara de 2.0 mm) en tres esquinas.\n3. Fila de depuración a 2.54 mm: TP4, TP8, TP7, TP2, TP1 y TP3, con serigrafía (L-56).\n4. Puerta L-48: 0 errores y 0 sin conectar; ≥ 400 vías de GND; EP de U17 = 9, U22 = 6, U4 = 6, U7 = 6; RF con cerca a ≤ 3 mm; nada de HV24 en capas internas a < 0.5 mm de bajo voltaje.\n5. Exclusiones de margen del DRC (t-v6-excluir-margen), commit, push y aviso a Claude."
|
||||
},
|
||||
"nota": "28-oct: se quita el «con (b) en D-26-1, el domingo 8-nov». Con (a) la puerta L-48 es el domingo 1-nov y G1 sigue el 2-nov."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-g1-paquete",
|
||||
"poner": {
|
||||
"descripcion": "Plan de fines de semana §7 (domingo 1-nov por la tarde; D-26-1 quedó en (a) y G1 es el lunes 2-nov). Sobre el commit de Michelle del domingo: T43 y T44 al cierre, re-auditoría final con el método del 26-oct (DRC con paridad, distancias del plan, Kelvin), revisión de BOM y CPL (rotaciones contra la huella de JLC, como en T46) y paquete Gerber/BOM/CPL con el Fabrication Toolkit para Aldair."
|
||||
},
|
||||
"nota": "28-oct, riesgo para G1: medido en fb95d15, el bloque B de la v6 no se ha tocado desde el 26-oct (L4 a 6.2 mm de U17, /SW_BAT y /MAIN5V sin polígono, R85/R86 a 5.9 mm de U17.18) y sus 3 tarjetas vencen mañana. Sin el bloque B no hay puerta L-48 el 1-nov."
|
||||
},
|
||||
{
|
||||
"id": "T261023-1a10",
|
||||
"poner": {
|
||||
"fecha_limite": "2026-10-30"
|
||||
},
|
||||
"nota": "28-oct: venció el 26-oct y sigue En curso. El retranslator de Wialon hacia nuestro servidor ya se cerró hoy, así que el comparador es lo que falta para ver RTK contra el GPS legacy en el monitor. Nueva fecha: 30-oct (Gibran y Jacobo)."
|
||||
},
|
||||
{
|
||||
"id": "t-fota-ret6-retrabajo",
|
||||
"poner": {
|
||||
"bloqueada_por": [
|
||||
"t-cobranza-octubre"
|
||||
]
|
||||
},
|
||||
"nota": "28-oct: el paso 1 es comprar 4 STM32F411RET6 y las compras están detenidas sin el pago del mes, así que la fecha del 13-nov depende de la cobranza. Si el pago no entra esta semana, pasa a Bloqueado."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-b-main5v-vsys",
|
||||
"nota": "28-oct, medido en fb95d15: sin cambio desde el 26-oct. /MAIN5V no tiene zona (43 pistas, cuello de 0.50 mm al entrar a U17.1/24) y C77–C81 están a 7.20/5.44/5.20/5.67/3.21 mm de U17.1-24. Lo que sí está limpio: nada de /MAIN5V en In1/In2 a menos de 10 mm de U17."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-b-bstfb-igate",
|
||||
"nota": "28-oct, medido en fb95d15: sin cambio desde el 26-oct. R85/R86 a 5.91/5.97 mm de U17.18 (meta 3); /VFB_BST 7.50 mm en In1 e /IGATE 6.26 mm en In1. /IGATE da 5.85 mm en F.Cu solo porque el resto está en In1: contando todo son 12.11 mm."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-b-bateria",
|
||||
"nota": "28-oct, medido en fb95d15: solo avanzó /VEH5V, que ya salió de In1/In2 (T45). Siguen /CHRG_FAULT 33 mm, /CHRG_STAT 31, /CHRG_EN 14, /VFB_BST 7.5, /F1_SEL 6.9 e /IGATE 6.3 mm internos a 5 mm de U17/L4. L4 a 6.20 mm; /BAT_PLUS tiene cuellos de 0.50 y 0.30 mm junto a U17."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-a-r112",
|
||||
"nota": "28-oct, medido en fb95d15: R112 sigue a 4.72 mm de J19.3 (meta 3), sin moverse desde el 26-oct. La otra mitad ya está: /IGN_IN es 100 % F.Cu, 0.50 mm, sin vías ni cobre en capas internas, así que solo falta acercar la resistencia."
|
||||
},
|
||||
{
|
||||
"id": "t-v6-d-gnss",
|
||||
"nota": "28-oct, medido en fb95d15: sin empezar. C70 a 26.40 mm de U25.1 (y el capacitor de /3V3_GNSS más cercano, C107, a 10.76); /SW_IN 131 mm en In1 y /SW_5V 9.7; TP9 a 35.4 mm de U18.49. Hallazgo nuevo: /ANT_BIAS queda a 0.360 mm de /RF_ANT_GNSS en L6.2 (meta 1 mm)."
|
||||
},
|
||||
{
|
||||
"id": "t-v5-t42b-hv24",
|
||||
"nota": "28-oct, confirmado: la tarjeta sigue siendo exacta. En hbox-watchdog-v56.kicad_pro y v57 los patrones de HV24 siguen sin «/» (24V, IGN_IN, Q10_GATE/Q6_GATE), así que solo /24V_IN entra a la clase, y no existe ningún tools/t42b*.py todavía."
|
||||
},
|
||||
{
|
||||
"crear": {
|
||||
"id": "t-tablero-reordenar-sesiones",
|
||||
"titulo": "Tablero: una página vieja abierta deshace los cambios de otros al reordenar columnas (INC-0005)",
|
||||
"pista": "gestion",
|
||||
"estado": "por-hacer",
|
||||
"responsable": "leo, claude",
|
||||
"campeon": "claude",
|
||||
"fecha_limite": "2026-10-30",
|
||||
"etiquetas": [
|
||||
"sitio"
|
||||
],
|
||||
"descripcion": "El 28-oct pasó dos veces. A las 10:33 una página del tablero abierta desde antes (sesión de Michelle) regresó a Por hacer 5 pendientes que Leo acababa de mover desde el standup a las 10:30, los 5 en el mismo segundo. A las 11:09 el mismo patrón, pero con 45 pendientes marcados «modificado por michelle» en el mismo segundo (y 51 como «leo» a las 11:01); esa vez casi todo fue el orden de las columnas, no el estado.\n\nConsecuencias: «modificado por» ya no sirve para saber quién tocó una tarjeta, y un cambio hecho desde el standup se puede perder sin que nadie se entere.\n\nPropuesta: que el guardado del orden mande solo el orden (no el estado) y que lleve la versión de la tarjeta que la página tenía; si no coincide, el servidor la rechaza y la página se recarga.",
|
||||
"enlaces": [
|
||||
{
|
||||
"titulo": "INC-0005",
|
||||
"url": "/tableros/incidencias/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"si_no_hay": [
|
||||
"INC-0005",
|
||||
"reordenar"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
247
backend/tests-1028.sh
Executable file → Normal file
247
backend/tests-1028.sh
Executable file → Normal file
@ -1,62 +1,203 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pruebas del 28-oct: documentos terminados en las actas (/api/archivos acepta PDF, Word, Excel…, con revisión del
|
||||
# contenido) y cómo se sirven (/api/adjunto: PDF y fotos en el navegador, lo demás se descarga, nosniff).
|
||||
# Arranca app.py con carpetas temporales. No toca el servidor.
|
||||
# Pruebas del 28-oct: compras.py — las órdenes de compra del cotizador dentro del centro de mando.
|
||||
# Arranca app.py con carpetas, htpasswd y base SQLite temporales (como tests-1027.sh). No toca el servidor.
|
||||
# Uso: bash backend/tests-1028.sh <ruta-a-hbox-docs> [puerto]
|
||||
set -u
|
||||
REPO=${1:?ruta a hbox-docs}; PORT=${2:-8098}
|
||||
T=$(mktemp -d); export ACTAS_DIR=$T/actas HTPASSWD_FILE=$T/htpasswd HTPASSWD_LOCK=$T/.lock SESION_COOKIE_INSEGURA=1 \
|
||||
ACTAS_ADMINS=leo PORT ADJUNTOS_DIR=$T/adjuntos
|
||||
mkdir -p $ACTAS_DIR $T/equipo $T/tareas $T/f
|
||||
touch $T/htpasswd
|
||||
python3 $REPO/backend/app.py > $T/app.log 2>&1 &
|
||||
APP=$!; trap "kill $APP 2>/dev/null" EXIT; sleep 1.8
|
||||
U="http://127.0.0.1:$PORT"
|
||||
ok=0; fail=0
|
||||
check() { if [[ "$3" == *"$2"* ]]; then ok=$((ok+1)); echo " ✓ $1"; else fail=$((fail+1)); echo " ✕ $1 — esperaba «$2», obtuve: ${3:0:220}"; fi; }
|
||||
sube() { curl -s -H "X-Remote-User: jesush" -F tipo=instalacion-campo -F serie=HBOX5-E15 -F etiqueta=Documento -F "archivo=@$1" $U/api/archivos; }
|
||||
|
||||
# archivos de prueba
|
||||
python3 - "$T/f" <<'EOF'
|
||||
import sys, os, zipfile, io
|
||||
d = sys.argv[1]
|
||||
open(os.path.join(d, "hoja-firmada.pdf"), "wb").write(b"%PDF-1.4\n1 0 obj<<>>endobj\ntrailer<<>>\n%%EOF\n")
|
||||
z = io.BytesIO(); zipfile.ZipFile(z, "w").writestr("word/document.xml", "<w:document/>")
|
||||
open(os.path.join(d, "reporte.docx"), "wb").write(z.getvalue())
|
||||
open(os.path.join(d, "conteo.xlsx"), "wb").write(z.getvalue())
|
||||
open(os.path.join(d, "viejo.xls"), "wb").write(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" + b"\0" * 64)
|
||||
open(os.path.join(d, "falso.pdf"), "wb").write(b"<html><script>alert(1)</script></html>")
|
||||
open(os.path.join(d, "pagina.html"), "wb").write(b"<html></html>")
|
||||
open(os.path.join(d, "programa.exe"), "wb").write(b"MZ\x90\x00")
|
||||
open(os.path.join(d, "captura.log"), "wb").write(b"$GNGGA,1,2,3\n")
|
||||
T=$(mktemp -d); SEC="secreto-de-pruebas-1028"
|
||||
export ACTAS_DIR=$T/actas HTPASSWD_FILE=$T/htpasswd HTPASSWD_LOCK=$T/.lock SESION_COOKIE_INSEGURA=1 \
|
||||
ACTAS_ADMINS=leo PORT INCIDENCIAS_DIR=$T/incidencias INVENTARIO_DIR=$T/inventario TAREAS_DIR=$T/tareas \
|
||||
EQUIPO_DIR=$T/equipo COMPRAS_DB=$T/drogheda.db COTIZADOR_SECRETO=$SEC
|
||||
mkdir -p $ACTAS_DIR $T/equipo $T/tareas $T/inventario
|
||||
${PYTHON:-python3} - "$T" <<'EOF'
|
||||
import sys, os, json
|
||||
t = sys.argv[1]
|
||||
for u, n in [("leo", "Leo"), ("adriana", "Adriana"), ("jesush", "Jesús Honorato"), ("alan", "Alán"),
|
||||
("miguel", "Miguel"), ("antonio", "Antonio"), ("jesus", "Jesús Romero"), ("claude", "Claude")]:
|
||||
json.dump({"id": u, "nombre": n, "area": "operaciones", "activo": True},
|
||||
open(os.path.join(t, "equipo", u + ".json"), "w"))
|
||||
EOF
|
||||
PY=${PYTHON:-python3}
|
||||
$PY -c 'import flask, sqlite3' 2>/dev/null || { echo "!! $PY no tiene flask (si acabas de cambiar de versión de"; \
|
||||
echo " Python, se quedó atrás). Cualquiera de las dos:"; \
|
||||
echo " pip3 install --break-system-packages flask"; \
|
||||
echo " python3 -m venv ~/.venv-hbox && ~/.venv-hbox/bin/pip -q install flask \\"; \
|
||||
echo " && PYTHON=~/.venv-hbox/bin/python bash backend/tests-1028.sh \"$REPO\""; exit 2; }
|
||||
if command -v lsof >/dev/null && lsof -nP -iTCP:$PORT -sTCP:LISTEN >/dev/null 2>&1; then
|
||||
echo "!! el puerto $PORT ya está ocupado. Dame otro: bash backend/tests-1028.sh \"$REPO\" 8123"; exit 2
|
||||
fi
|
||||
$PY $REPO/backend/app.py > $T/app.log 2>&1 &
|
||||
APP=$!; trap "kill $APP 2>/dev/null" EXIT
|
||||
U="http://127.0.0.1:$PORT"
|
||||
# Esperar a que conteste (hasta 15 s): si no arranca, el resto de las pruebas no dice nada útil.
|
||||
arriba=""
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -fsS -o /dev/null "$U/api/salud" 2>/dev/null; then arriba=si; break; fi
|
||||
kill -0 $APP 2>/dev/null || break
|
||||
sleep 0.5
|
||||
done
|
||||
if [[ -z "$arriba" ]]; then
|
||||
echo "!! app.py no arrancó en el puerto $PORT. Su bitácora:"; echo "────────────"
|
||||
sed -n '1,40p' $T/app.log; echo "────────────"
|
||||
echo "Pistas: falta flask en $PY · el puerto está ocupado · o un módulo de backend/ truena al cargar."
|
||||
exit 2
|
||||
fi
|
||||
ok=0; fail=0
|
||||
check() { if [[ "$3" == *"$2"* ]]; then ok=$((ok+1)); echo " ✓ $1"; else fail=$((fail+1)); echo " ✕ $1 — esperaba «$2», obtuve: ${3:0:240}"; fi; }
|
||||
no() { if [[ "$3" != *"$2"* ]]; then ok=$((ok+1)); echo " ✓ $1"; else fail=$((fail+1)); echo " ✕ $1 — NO debía traer «$2»: ${3:0:240}"; fi; }
|
||||
as() { curl -s -H "X-Remote-User: $1" -H "Content-Type: application/json" "${@:2}"; }
|
||||
|
||||
echo "## documentos en /api/archivos"
|
||||
R=$(sube $T/f/hoja-firmada.pdf); check "PDF se acepta" '"ok":true' "$R"
|
||||
PDF=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['archivos'][0]['ruta'])")
|
||||
R=$(sube $T/f/reporte.docx); check "Word (.docx) se acepta" '"ok":true' "$R"
|
||||
DOCX=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['archivos'][0]['ruta'])")
|
||||
check "Excel (.xlsx) se acepta" '"ok":true' "$(sube $T/f/conteo.xlsx)"
|
||||
check "Excel viejo (.xls) se acepta" '"ok":true' "$(sube $T/f/viejo.xls)"
|
||||
check "PDF falso (HTML con .pdf) se rechaza" 'no es un PDF' "$(sube $T/f/falso.pdf)"
|
||||
check ".html se rechaza" 'n no permitida' "$(sube $T/f/pagina.html)"
|
||||
check ".exe se rechaza" 'n no permitida' "$(sube $T/f/programa.exe)"
|
||||
check "los logs siguen igual" '"ok":true' "$(sube $T/f/captura.log)"
|
||||
# La hora y la firma se calculan aparte para que la cabecera y lo firmado coincidan exactamente.
|
||||
# firma_t(cuerpo [secreto] [desfase]) -> la hora ISO con zona · firma_f(... [hora]) -> el hex del HMAC
|
||||
firma_t() { $PY -c '
|
||||
import sys, datetime
|
||||
desf = int(sys.argv[1] or 0)
|
||||
print((datetime.datetime.now().astimezone() + datetime.timedelta(seconds=desf)).replace(microsecond=0).isoformat())
|
||||
' "${3:-0}"; }
|
||||
firma_f() { $PY -c '
|
||||
import sys, hmac, hashlib
|
||||
sec_def, cuerpo, sec, t = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
||||
print(hmac.new((sec or sec_def).encode(), t.encode() + b"\n" + cuerpo.encode(), hashlib.sha256).hexdigest())
|
||||
' "$SEC" "$1" "${2:-}" "${4:-$(firma_t "$1" "${2:-}" "${3:-0}")}"; }
|
||||
# Las cabeceras se pasan en dos variables (nada de mapfile: el bash 3.2 de macOS no lo trae).
|
||||
post() { local cuerpo="$1"; local t f
|
||||
t=$(firma_t "$cuerpo" "${2:-}" "${3:-0}"); f=$(firma_f "$cuerpo" "${2:-}" "${3:-0}" "$t")
|
||||
curl -s -H "Content-Type: application/json" -H "X-Hbox-Tiempo: $t" -H "X-Hbox-Firma: sha256=$f" \
|
||||
-d "$cuerpo" $U/api/integracion/cotizador/evento; }
|
||||
get() { local t f
|
||||
t=$(firma_t ""); f=$(firma_f "" "" 0 "$t")
|
||||
curl -s -H "X-Hbox-Tiempo: $t" -H "X-Hbox-Firma: sha256=$f" "$U$1"; }
|
||||
|
||||
echo "## cómo se sirven (/api/adjunto)"
|
||||
H=$(curl -s -D - -o /dev/null -H "X-Remote-User: leo" "$U/api/adjunto?ruta=$PDF")
|
||||
check "PDF abre en el navegador (inline)" 'inline' "$H"
|
||||
check "PDF con su nombre original" 'hoja-firmada.pdf' "$H"
|
||||
check "PDF con nosniff" 'nosniff' "$H"
|
||||
check "PDF como application/pdf" 'application/pdf' "$H"
|
||||
H=$(curl -s -D - -o /dev/null -H "X-Remote-User: leo" "$U/api/adjunto?ruta=$DOCX")
|
||||
check "Word se descarga (attachment)" 'attachment' "$H"
|
||||
check "Word con su nombre original" 'reporte.docx' "$H"
|
||||
|
||||
echo "## el expediente los lista"
|
||||
E=$(curl -s -H "X-Remote-User: leo" "$U/api/unidad?serie=HBOX5-E15")
|
||||
check "expediente cuenta 5 archivos (4 documentos + 1 log)" '"total_logs":5' "$E"
|
||||
echo "## arranque"
|
||||
check "módulos cargados sin error" "" "$(grep -i 'no se cargó' $T/app.log)"
|
||||
check "la base se creó" "drogheda.db" "$(ls $T/)"
|
||||
|
||||
echo
|
||||
echo "RESULTADO: $ok correctas · $fail fallidas · datos en $T"
|
||||
[ $fail -eq 0 ]
|
||||
echo "## ítems del inventario (los que ligan los renglones)"
|
||||
as leo -d '{"codigo":"SOP-RTG","descripcion":"Soporte para RTG","categoria":"instalacion","unidad":"pza","por_instalacion":2}' $U/api/inventario > /dev/null
|
||||
as leo -d '{"codigo":"CBL-10","descripcion":"Cable calibre 10","categoria":"cable","unidad":"m"}' $U/api/inventario > /dev/null
|
||||
check "dos ítems dados de alta" '"total":2' "$(as leo $U/api/inventario)"
|
||||
|
||||
echo
|
||||
echo "## firma"
|
||||
check "sin firma: 401" '"error":"Firma inv' "$(curl -s -H 'Content-Type: application/json' -d '{}' $U/api/integracion/cotizador/evento)"
|
||||
check "firma con otro secreto: 401" '"error":"Firma inv' "$(post '{"evento_id":"x","tipo":"oc.pagada","oc":{"folio":"OC-1","sitio":"ZLO"}}' 'otro-secreto')"
|
||||
check "firma de hace 10 min: 401" '"error":"Firma inv' "$(post '{"evento_id":"x","tipo":"oc.pagada","oc":{"folio":"OC-1","sitio":"ZLO"}}' '' -600)"
|
||||
check "items pide firma" '"ok":true' "$(get /api/integracion/inventario/items)"
|
||||
check "items trae los códigos" 'SOP-RTG' "$(get /api/integracion/inventario/items)"
|
||||
check "el navegador no entra a integración" '"error":"Firma inv' "$(as leo $U/api/integracion/inventario/items)"
|
||||
|
||||
echo
|
||||
echo "## oc.autorizada"
|
||||
OC1='{"evento_id":"OC-2026-0012:oc.autorizada:1","tipo":"oc.autorizada","cuando":"2026-10-28T10:15:00-06:00","quien":"tania@helgen.tech","prueba":false,"oc":{"folio":"OC-2026-0012","req_folio":"REQ-2026-0031","proyecto_folio":"PRY-2026-0002","proyecto_clave":"DRO","sitio":"ZLO","proveedor":"Ferretería del Pacífico","fecha_entrega":"2026-10-30","solicitante":"jesus@helgen.tech","autorizo":"adriana@helgen.tech","total":99999,"renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":4,"item":"SOP-RTG","importe":1234},{"n":2,"descripcion":"Tornillo 3/8 x 2 in","unidad":"pza","cantidad":32,"item":null}]}}'
|
||||
R1="$(post "$OC1")"
|
||||
check "la OC entra" '"estado_portal":"autorizada"' "$R1"
|
||||
check "el renglón sin ítem queda por ligar" '"por_ligar":[2]' "$R1"
|
||||
check "abre su pendiente" '"tarea_id":"t-oc-oc-2026-0012"' "$R1"
|
||||
check "el pendiente se llama «Recibir … en ZLO»" 'Recibir OC-2026-0012 en ZLO' "$(cat $T/tareas/t-oc-oc-2026-0012.json)"
|
||||
check "…es de compras y del sitio de entrega" 'timsa|compras' "$($PY -c "
|
||||
import json;d=json.load(open('$T/tareas/t-oc-oc-2026-0012.json'));print('|'.join(d['sitios'])+'|'+d['pista'])")"
|
||||
check "…vence el día de entrega" '2026-10-30' "$(grep -o '"fecha_limite": "[^"]*"' $T/tareas/t-oc-oc-2026-0012.json)"
|
||||
check "…es del responsable de ZLO" 'jesush' "$(grep -o '"responsable": "[^"]*"' $T/tareas/t-oc-oc-2026-0012.json)"
|
||||
no "los montos NUNCA se guardan (total)" '99999' "$($PY -c "
|
||||
import sqlite3;c=sqlite3.connect('$T/drogheda.db');print([r for r in c.execute('SELECT * FROM eventos')])")"
|
||||
no "los montos NUNCA se guardan (importe)" '1234' "$($PY -c "
|
||||
import sqlite3;c=sqlite3.connect('$T/drogheda.db');print([r for r in c.execute('SELECT * FROM eventos')])")"
|
||||
check "el mismo aviso otra vez no duplica" '"repetido":true' "$(post "$OC1")"
|
||||
check "sitio inválido: lo dice claro" 'usa VER, ZLO, ESE, LZC o QRO' "$(post '{"evento_id":"e-malo","tipo":"oc.autorizada","oc":{"folio":"OC-MALA","sitio":"MZO","renglones":[]}}')"
|
||||
check "tipo inválido" 'Tipo de aviso inv' "$(post '{"evento_id":"e2","tipo":"oc.inventada","oc":{"folio":"OC-1","sitio":"ZLO"}}')"
|
||||
check "nada en camino todavía" '"en_camino":{}' "$(as leo $U/api/inventario | tr -d ' ')"
|
||||
|
||||
echo
|
||||
echo "## oc.en_camino: el material aparece en camino en la pestaña del sitio"
|
||||
R2="$(post '{"evento_id":"OC-2026-0012:oc.en_camino:1","tipo":"oc.en_camino","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0012","sitio":"ZLO","proveedor":"Ferretería del Pacífico","fecha_entrega":"2026-10-30","paqueteria":"Estafeta","guia":"123456789","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":4,"item":"SOP-RTG"},{"n":2,"descripcion":"Tornillo 3/8 x 2 in","unidad":"pza","cantidad":32,"item":null}]}}')"
|
||||
check "la OC queda en camino" '"estado_portal":"en-camino"' "$R2"
|
||||
check "4 soportes en camino a TIMSA" '"en_camino":{"timsa":4}' "$(as leo $U/api/inventario | tr -d ' ')"
|
||||
check "el envío trae el folio como referencia" 'OC-2026-0012' "$(as leo "$U/api/inventario/movimientos")"
|
||||
check "…y la guía en el motivo" '123456789' "$(as leo "$U/api/inventario/movimientos")"
|
||||
check "el pendiente se enteró" 'el proveedor ya lo mandó' "$(cat $T/tareas/t-oc-oc-2026-0012.json)"
|
||||
check "el renglón 2 sigue por ligar" '"por_ligar":[2]' "$(as leo $U/api/compras | tr -d ' ')"
|
||||
|
||||
echo
|
||||
echo "## ligar el renglón que venía sin ítem"
|
||||
check "no se liga a un ítem que no existe" 'tem NO-HAY' "$(as leo -d '{"oc":"OC-2026-0012","n":2,"item":"NO-HAY"}' $U/api/compras/ligar)"
|
||||
check "se liga al cable" '"por_ligar":[]' "$(as leo -d '{"oc":"OC-2026-0012","n":2,"item":"CBL-10"}' $U/api/compras/ligar | tr -d ' ')"
|
||||
check "…y como la OC ya iba en camino, se manda solo" '"timsa":32' "$(as leo $U/api/inventario | tr -d ' ' | grep -o '"en_camino":{"timsa":32}')"
|
||||
check "no se liga dos veces" 'en camino en el inventario' "$(as leo -d '{"oc":"OC-2026-0012","n":2,"item":"CBL-10"}' $U/api/compras/ligar)"
|
||||
|
||||
echo
|
||||
echo "## recibir en el centro de mando (lo que llegó de verdad)"
|
||||
check "no pueden llegar más de los que se pidieron" 'no pueden llegar' "$(as jesush -d '{"oc":"OC-2026-0012","renglones":[{"n":1,"recibido":9}]}' $U/api/compras/recibir)"
|
||||
R3="$(as jesush -d '{"oc":"OC-2026-0012","renglones":[{"n":1,"recibido":4},{"n":2,"recibido":20}]}' $U/api/compras/recibir)"
|
||||
check "se recibe parcial" '"estado":"recibida-parcial"' "$(echo $R3 | tr -d ' ')"
|
||||
check "los 4 soportes entraron a TIMSA" '"por_sitio":{"timsa":4}' "$(as leo $U/api/inventario | tr -d ' ' | head -c 4000)"
|
||||
check "el pendiente sigue abierto y dice qué falta" 'llegó parte de OC-2026-0012' "$(cat $T/tareas/t-oc-oc-2026-0012.json)"
|
||||
check "el pendiente NO se cerró" '"estado": "por-hacer"' "$(grep -o '"estado": "[^"]*"' $T/tareas/t-oc-oc-2026-0012.json | head -1)"
|
||||
|
||||
echo
|
||||
echo "## el cotizador consulta las recepciones"
|
||||
REC="$(get '/api/integracion/cotizador/recepciones')"
|
||||
check "trae la recepción" '"oc_folio":"OC-2026-0012"' "$(echo $REC | tr -d ' ')"
|
||||
check "…marcada como incompleta" '"completa":false' "$(echo $REC | tr -d ' ')"
|
||||
check "…con quién recibió" '"recibio":"jesush"' "$(echo $REC | tr -d ' ')"
|
||||
check "…y cuánto llegó por renglón" '"pedido":32.0,"recibido":20.0' "$(echo $REC | tr -d ' ')"
|
||||
check "con «desde» en el futuro no trae nada" '"recepciones":[]' "$(get '/api/integracion/cotizador/recepciones?desde=2030-01-01T00:00:00-06:00' | tr -d ' ')"
|
||||
|
||||
echo
|
||||
echo "## una OC que llega completa cierra su pendiente"
|
||||
post '{"evento_id":"OC-2026-0013:oc.autorizada:1","tipo":"oc.autorizada","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0013","req_folio":"REQ-2026-0032","sitio":"ESE","proveedor":"Edwin","fecha_entrega":"2026-11-03","renglones":[{"n":1,"descripcion":"Cable calibre 10","unidad":"m","cantidad":30,"item":"CBL-10"}]}}' > /dev/null
|
||||
post '{"evento_id":"OC-2026-0013:oc.en_camino:1","tipo":"oc.en_camino","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0013","sitio":"ESE","guia":"ABC","renglones":[{"n":1,"descripcion":"Cable calibre 10","unidad":"m","cantidad":30,"item":"CBL-10"}]}}' > /dev/null
|
||||
check "el pendiente de ESE es de Miguel" 'miguel' "$(grep -o '"responsable": "[^"]*"' $T/tareas/t-oc-oc-2026-0013.json)"
|
||||
R4="$(as miguel -d '{"oc":"OC-2026-0013","renglones":[{"n":1,"recibido":30}]}' $U/api/compras/recibir)"
|
||||
check "queda recibida" '"estado":"recibida"' "$(echo $R4 | tr -d ' ')"
|
||||
check "y su pendiente se cerró solo" '"estado": "hecho"' "$(grep -o '"estado": "[^"]*"' $T/tareas/t-oc-oc-2026-0013.json | head -1)"
|
||||
check "el cotizador la ve completa" 'OC-2026-0013=completa' "$(get '/api/integracion/cotizador/recepciones' | $PY -c "
|
||||
import sys,json;d=json.load(sys.stdin)
|
||||
print(' '.join(r['oc_folio']+('=completa' if r['completa'] else '=parcial') for r in d['recepciones']))")"
|
||||
|
||||
echo
|
||||
echo "## recibir desde Inventario (la otra puerta) también se concilia"
|
||||
post '{"evento_id":"OC-2026-0014:oc.autorizada:1","tipo":"oc.autorizada","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0014","req_folio":"REQ-2026-0033","sitio":"VER","fecha_entrega":"2026-11-05","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":2,"item":"SOP-RTG"}]}}' > /dev/null
|
||||
post '{"evento_id":"OC-2026-0014:oc.en_camino:1","tipo":"oc.en_camino","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0014","sitio":"VER","guia":"XYZ","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":2,"item":"SOP-RTG"}]}}' > /dev/null
|
||||
ENV=$($PY -c "
|
||||
import sqlite3;c=sqlite3.connect('$T/drogheda.db');print(c.execute(\"SELECT envio_id FROM renglones WHERE oc_folio='OC-2026-0014'\").fetchone()[0])")
|
||||
as alan -d "{\"tipo\":\"recepcion\",\"envio_id\":\"$ENV\",\"cantidad\":2}" $U/api/inventario/movimiento > /dev/null
|
||||
check "Compras se entera de la recepción hecha en Inventario" 'OC-2026-0014=recibida' "$(as leo $U/api/compras | $PY -c "
|
||||
import sys,json;d=json.load(sys.stdin)
|
||||
print(' '.join(o['folio']+'='+o['estado'] for o in d['ordenes']))")"
|
||||
check "y cierra su pendiente" '"estado": "hecho"' "$(grep -o '"estado": "[^"]*"' $T/tareas/t-oc-oc-2026-0014.json | head -1)"
|
||||
|
||||
echo
|
||||
echo "## cancelar: lo que iba en camino deja de contar"
|
||||
post '{"evento_id":"OC-2026-0015:oc.autorizada:1","tipo":"oc.autorizada","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0015","sitio":"LZC","fecha_entrega":"2026-11-06","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":7,"item":"SOP-RTG"}]}}' > /dev/null
|
||||
post '{"evento_id":"OC-2026-0015:oc.en_camino:1","tipo":"oc.en_camino","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0015","sitio":"LZC","guia":"Q","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":7,"item":"SOP-RTG"}]}}' > /dev/null
|
||||
check "7 en camino a LCT" '"lzc":7' "$(as leo $U/api/inventario | tr -d ' ')"
|
||||
post '{"evento_id":"OC-2026-0015:oc.cancelada:1","tipo":"oc.cancelada","quien":"tania@helgen.tech","oc":{"folio":"OC-2026-0015","sitio":"LZC","renglones":[{"n":1,"descripcion":"Soporte para RTG","unidad":"pza","cantidad":7,"item":"SOP-RTG"}]}}' > /dev/null
|
||||
no "ya no hay nada en camino a LCT" '"lzc":7' "$(as leo $U/api/inventario | tr -d ' ')"
|
||||
check "y su pendiente se cerró" '"estado": "hecho"' "$(grep -o '"estado": "[^"]*"' $T/tareas/t-oc-oc-2026-0015.json | head -1)"
|
||||
|
||||
echo
|
||||
echo "## las rondas de PRUEBA no tocan nada"
|
||||
ANTES=$(ls $T/tareas | wc -l)
|
||||
post '{"evento_id":"OC-PRUEBA-1:oc.autorizada:1","tipo":"oc.autorizada","prueba":true,"quien":"efrain@helgen.tech","oc":{"folio":"OC-PRUEBA-1","sitio":"QRO","renglones":[{"n":1,"descripcion":"Soporte","unidad":"pza","cantidad":5,"item":"SOP-RTG"}]}}' > /dev/null
|
||||
check "la de prueba no abre pendiente" "$ANTES" "$(ls $T/tareas | wc -l)"
|
||||
no "ni sale en la página de Compras" 'OC-PRUEBA-1' "$(as leo $U/api/compras)"
|
||||
no "ni en las recepciones del cotizador" 'OC-PRUEBA-1' "$(get '/api/integracion/cotizador/recepciones')"
|
||||
|
||||
echo
|
||||
echo "## sin secreto configurado el sitio sigue funcionando"
|
||||
kill $APP 2>/dev/null; sleep 0.5
|
||||
unset COTIZADOR_SECRETO
|
||||
$PY $REPO/backend/app.py > $T/app2.log 2>&1 &
|
||||
APP=$!
|
||||
for _ in $(seq 1 30); do curl -fsS -o /dev/null "$U/api/salud" 2>/dev/null && break; sleep 0.5; done
|
||||
check "integración apagada: 503 con mensaje claro" 'COTIZADOR_SECRETO' "$(curl -s -d '{}' $U/api/integracion/cotizador/evento)"
|
||||
check "la página de Compras lo dice" '"integracion":false' "$(as leo $U/api/compras | tr -d ' ')"
|
||||
check "…y sigue listando las OC de antes" 'OC-2026-0012' "$(as leo $U/api/compras)"
|
||||
check "el inventario sigue igual" '"ok":true' "$(as leo $U/api/inventario)"
|
||||
|
||||
echo
|
||||
echo "────────────────────────────────────────"
|
||||
echo " $ok bien · $fail mal"
|
||||
[[ $fail -eq 0 ]] || echo " log: $T/app.log"
|
||||
[[ $fail -eq 0 ]]
|
||||
|
||||
@ -22,6 +22,9 @@ Arriba, lo que pasó en el tablero de pendientes y en las actas en los últimos
|
||||
|
||||
### Octubre de 2026
|
||||
|
||||
**28 oct**
|
||||
: [Flujo de compras e inventario del centro de mando](inventario/flujo-compras-28oct.md): el proceso como quedó en la junta de dirección (dos puertas de Adriana, arranque el 29-oct solo para HBOX) y la fase 1 del portal — `backend/compras.py`, la página [Compras](tableros/compras.md) y las rutas firmadas del cotizador.
|
||||
|
||||
**28 oct**
|
||||
: **T45 terminado** (`HBox_PCB fb95d15`, 12:20). Michelle subió a las 10:05 el Actualizar PCB de la v6 con T48 + T49 + T41 (`a945c30`) e hizo a mano casi todo T45 (`f1c8879`): R80 abajo de Q7, `/VEH5V` por F.Cu, `/MAIN5V` de JP5 y R85 desde Q7.5 y el EP de U4. El script de T45 se detuvo sin escribir, como debía, y **T45r** hizo lo que faltaba: la **toma CLN** de R80 ahora es Kelvin (sale de R80.2 por adentro; antes salía de la barra S de Q7 y el límite de entrada quedaba ≈ 8 % abajo), la toma VIN sale de la esquina de adentro de R80.1, 6 vías en el EP de U7, `/VEH5V` de 2.0 mm de C100 a C101 y fuera los 7 colgantes. De L5.2 a R80.1, ≈ 5 mΩ (antes 31). DRC: **0 errores, 0 sin conectar, paridad limpia y 0 colgantes.** Michelle hace `git pull`, presiona B y sigue con el bloque B ([instrucciones v1.3, §2](hardware/instrucciones-michelle-27oct.md#2-despues-de-t45-martes-28-y-miercoles-29-bloque-b-y-r112)). **Para G1:** en la Mac de Leo, KiCad redibuja con otra forma el texto «EinarBoard v6» (fuente MADE Evolve Sans EVO); por eso T45r no guardó el relleno, y queda anotado en el pendiente del paquete. Lotes `2026-10-28_t45-michelle-a-mano` y `2026-10-28_t45r-aplicado`.
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"_como": "Datos fijos de la página de inicio y del tablero de Ingeniería (Ruta a producción y una tarjeta por versión). Los actualiza Claude o Leo cuando una tarjeta cambia de etapa o se mueve un hito; después, deploy. Estados de cada paso: hecho, ahora, bloqueado, falta. Cada paso es [estado, texto, debajo]. Tono de la tarjeta: hecho, ahora, aviso, bloqueado. Hitos: fecha AAAA-MM-DD (null si aún no hay fecha), fin opcional y «cuando», el texto que se ve. El enfoque de la semana NO va aquí: lo escriben Leo y Adriana en la página de inicio (/api/hoy); enfoque_inicial solo se muestra mientras nadie lo ha escrito. Este archivo es parte del cierre de cada tarea (hbox-docs/CLAUDE.md, «Cierre de una tarea»). Semáforo (22-oct): «semaforo» con color rojo/amarillo/verde, titulo, texto, causas (admite **negritas** y [ligas](/ruta/)), destraba y actualizado; cada sitio y la oficina llevan «estado» (rojo/amarillo/verde), «nota» y «meta». Desde la junta del 22-oct, cada sitio lleva además «responsable» (su responsable de campo), que se ve en la página de inicio.",
|
||||
"actualizado": "2026-10-22",
|
||||
"actualizado": "2026-10-28",
|
||||
"fase": {
|
||||
"nombre": "Pre-producción · modo crisis",
|
||||
"texto": "semáforo rojo: sin pago este mes y compras detenidas; V5.6 y V5.7 auditadas (GO al piloto de 5 + 5; se pide al terminar los quick wins); la v6 en layout"
|
||||
@ -11,14 +11,14 @@
|
||||
"texto": "Sin pago este mes y tres sitios frenados. La meta inmediata es tener 1–2 unidades instaladas por sitio.",
|
||||
"causas": [
|
||||
"**Finanzas:** no hemos recibido pago este mes; operamos muy ajustados y sin compras significativas.",
|
||||
"**TIMSA (Manzanillo):** bloqueado por un trámite administrativo del vehículo. [Pendientes de TIMSA](/tableros/pendientes/?sitio=timsa)",
|
||||
"**EIT (Ensenada):** esperando el material que se envió desde Manzanillo. [Pendientes de EIT](/tableros/pendientes/?sitio=eit)",
|
||||
"**TIMSA (Manzanillo):** el trámite del vehículo ya quedó (28-oct); falta instalar y probar en sitio. [Pendientes de TIMSA](/tableros/pendientes/?sitio=timsa)",
|
||||
"**EIT (Ensenada):** el material de Manzanillo ya llegó y hay dos unidades instaladas (28-oct); falta la prueba en sitio. [Pendientes de EIT](/tableros/pendientes/?sitio=eit)",
|
||||
"**ICAVE (Veracruz):** mal clima; ventanas cortas en campo y fallas que hay que investigar ahí. [Pendientes de ICAVE](/tableros/pendientes/?sitio=icave)",
|
||||
"**LCT (Lázaro Cárdenas):** se trata aparte; el material debería estar y falta la herrería de los soportes para instalar en las RTG. [Pendientes de LCT](/tableros/pendientes/?sitio=lzc)",
|
||||
"**Software:** el retranslator de Wialon al monitor, para comparar el RTK contra el GPS legado, está en curso (Gibran y Jacobo, 26-oct); las desconexiones súbitas se investigan de inmediato."
|
||||
],
|
||||
"destraba": "Cuando paguemos a proveedores se destraban la mayoría de las tareas.",
|
||||
"actualizado": "2026-10-22",
|
||||
"actualizado": "2026-10-28",
|
||||
"quien": "Leo"
|
||||
},
|
||||
"pedido_nota": "primero los pilotos: 5 V5.6 y 5 V5.7 (se piden al terminar los quick wins, con 3 condiciones), luego 20 v6",
|
||||
@ -442,15 +442,16 @@
|
||||
}
|
||||
],
|
||||
"enfoque_inicial": {
|
||||
"titulo": "Modo crisis: primero los quick wins; destrabar pago, trámites y material para tener 1–2 unidades instaladas y probadas por sitio.",
|
||||
"titulo": "Modo crisis: destrabar el pago y cerrar 1–2 unidades instaladas y **probadas** por sitio. Desde el 29-oct, todo el material de HBOX se pide por el sistema.",
|
||||
"puntos": [
|
||||
"**Finanzas:** sin pago recibido este mes, las compras están detenidas; al pagar a proveedores se destraban la mayoría de las tareas.",
|
||||
"**Sitios:** TIMSA bloqueado por el trámite del vehículo; EIT espera el material de Manzanillo; ICAVE con mal clima y fallas por investigar en campo; LCT se trata aparte (falta la herrería de los soportes para las RTG). Responsables de campo: ICAVE Alán, EIT Miguel, LCT Antonio y TIMSA Jesús Honorato; líderes de instalación, Alán y Daniel.",
|
||||
"**Sitios:** EIT con dos instaladas y la tercera si prestan la máquina; TIMSA ya con el vehículo resuelto; ICAVE con fallas por investigar en campo y actas por llenar; LCT se trata aparte (falta la herrería de los soportes para las RTG). Lo que cuenta para la meta es la **prueba en sitio**, no la instalación. Responsables de campo: ICAVE Alán, EIT Miguel, LCT Antonio y TIMSA Jesús Honorato.",
|
||||
"**Software:** retranslator de Wialon al monitor (Gibran y Jacobo, 26-oct; RTK contra GPS legado) y desconexiones súbitas: atención inmediata.",
|
||||
"**Piloto V5.6 + V5.7:** GO de la auditoría final; el pedido se hace al terminar los quick wins (junta del 22-oct). 0.7.0-pre listo antes de que lleguen las tarjetas.",
|
||||
"**Juntas:** standup diario de 15 min y revisión semanal de quick wins, grabadas con Gemini en español; Claude pasa los acuerdos al tablero con un lote ([cómo](/tableros/juntas/))."
|
||||
"**Compras (29-oct):** las requisiciones de HBOX se levantan en el cotizador; Adriana autoriza renglón por renglón, Tania cotiza y paga, y quien recibe lo confirma desde el celular en [Compras](/tableros/compras/) ([el flujo](/inventario/flujo-compras-28oct/)).",
|
||||
"**Juntas:** standup diario de 15 min y revisión semanal de quick wins. Toda junta del proyecto **invita a Leo y prende Gemini**: con eso el agente diario mueve el tablero ([cómo](/tableros/juntas/))."
|
||||
],
|
||||
"nota": "Texto inicial del 22-oct (modo crisis, Leo), de la [auditoría final](/hardware/auditoria-final-22oct/) y de la [junta del 22-oct](/tableros/juntas/#registro). Leo y Adriana lo cambian con «Editar»."
|
||||
"nota": "Texto propuesto por Claude el 28-oct con los acuerdos de la [junta de dirección](/tableros/juntas/#registro). Leo y Adriana lo cambian con «Editar»."
|
||||
},
|
||||
"sitios": [
|
||||
{
|
||||
|
||||
@ -25,7 +25,7 @@ Acceso: usuario y contraseña te los da Leo; cámbiala la primera vez en [Mi cue
|
||||
|
||||
Al terminar una tarea:
|
||||
|
||||
1. **El pendiente.** Muévelo a *Hecho* con su evidencia (el acta, la liga o el archivo; D-27-6), o a *Bloqueado* con la razón en un comentario, y abre los que siguen, con responsable y fecha. Si la tarea no tenía pendiente, créalo y ciérralo: así queda en la Actividad.
|
||||
1. **El pendiente.** Muévelo a *Hecho*, o a *Bloqueado* con la razón en un comentario, y abre los que siguen, con responsable y fecha. Si la tarea no tenía pendiente, créalo y ciérralo: así queda en la Actividad.
|
||||
2. **El documento.** Si la tarea cambió un procedimiento, una tarjeta o una decisión, el documento se corrige el mismo día. Lo hacen Leo o Claude en el repositorio; tú avísales qué cambió (regla 6).
|
||||
3. **El acta.** Si tocaste una tarjeta o una unidad, llena su acta (regla 3).
|
||||
4. **La mejora.** Si el sitio te estorbó (una liga rota, un dato viejo, algo que no encontraste), abre un pendiente para Leo con la página y lo que esperabas encontrar.
|
||||
@ -79,6 +79,5 @@ git add -A && git commit -m "docs: ..." && git push
|
||||
| 1.0 | 2026-09-29 | Primera versión. |
|
||||
| 1.1 | 2026-09-29 | Edición breve; recibe las reglas de la portada; la tabla por situación queda en las portadas de sección. |
|
||||
| 1.2 | 2026-10-21 | «El sitio es el centro de mando»: cómo se cierra una tarea (pendiente, documento, acta y mejora); lotes del tablero en el anexo. |
|
||||
| 1.3 | 2026-10-27 | «Hecho» pide evidencia (D-27-6). |
|
||||
|
||||
Versiones anteriores en git.
|
||||
|
||||
86
docs/historia/4g-modem-alternativas.md
Normal file
86
docs/historia/4g-modem-alternativas.md
Normal file
@ -0,0 +1,86 @@
|
||||
---
|
||||
title: 4G Modem — Bench Bridge & Cheap Alternatives to the Ardusimple 4G NTRIP Master
|
||||
tags: [project-drogheda, hardware, 4g, ntrip, modem, sourcing]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO
|
||||
date: 2026-07-08
|
||||
status: Decision support — bench (Block 9B) + production outlook
|
||||
---
|
||||
|
||||
# 4G Modem — Bench Bridge & Cheap Alternatives
|
||||
|
||||
> **Why this doc.** Block 9B (Modo B, watchdog in-line between the F9P and the 4G) needs the
|
||||
> 4G NTRIP Master out of the Pro's socket and on the protoboard — but its XBee pins are
|
||||
> **2.0 mm pitch** (not 1.27 mm; re-measure center-to-center: 10 pins span 18 mm) vs the
|
||||
> protoboard's 2.54 mm. Two questions answered here: (1) how to bridge it **this week**, and
|
||||
> (2) whether a **cheaper 4G module** can replace the €156 Ardusimple unit — bench now,
|
||||
> production later. Technician instructions live in the Guía v3.0 §9a–9c; this is the
|
||||
> engineering/sourcing record.
|
||||
|
||||
## 1. Bridge options (bench, this week)
|
||||
|
||||
| # | Option | Price (MXN) | Source / lead | Verdict |
|
||||
|---|--------|------------|----------------|---------|
|
||||
| A ★ | SparkFun **BOB-08276** breakout + 2× **PRT-08272** 2.0 mm sockets + 2.54 mm male headers | ≈$100 | [SANDOROBOTICS CDMX](https://sandorobotics.com.mx/producto/bob-08276/) — in stock, 1–2 days to QRO | **Buy today.** One soldering session; matches the Guía 9b procedure |
|
||||
| B | Generic "adaptador XBee" pre-assembled | $80–250 | Amazon MX / [MercadoLibre](https://listado.mercadolibre.com.mx/adaptador-xbee), 1–3 days | Fine fallback; verify DIP pins in photos |
|
||||
| C | XBee Arduino shield (HR0440) + Dupont F-F | $50–150 | SANDOROBOTICS, 1–2 days | Works as a fixed base next to the protoboard |
|
||||
| D | 2.0 mm F-F jumpers direct onto pins | $60–120 | AliExpress, 2–4 weeks | Last resort — fragile, slow to arrive |
|
||||
|
||||
Power rule (already in the Guía): XBee **pin 1 VCC = 3.3 V, never 5 V**, ≥1.5 A peak →
|
||||
second LM2596/MP1584 set to 3.3 V, fed **from SAL** (so a stack power-cycle also reboots the
|
||||
modem — mirrors the PROD 3V3_MODEM rail), with 100 µF + 100 nF at the pin.
|
||||
Caution: SparkFun's *Explorer Regulated* uses a ~500 mA LDO — not enough for LTE bursts;
|
||||
use the bare breakout + our own buck.
|
||||
|
||||
## 2. Cheap 4G module alternatives
|
||||
|
||||
Telcel LTE bands to match: **B2/B4/B5/B7/B28/B66**. Key finding: **none of the cheap modules
|
||||
has a built-in NTRIP client** — the Ardusimple's real value is the on-module NTRIP client
|
||||
firmware (zero MCU work, proven in the field). Every alternative below buys hardware savings
|
||||
with firmware/integration cost.
|
||||
|
||||
| Module | Price | Buy | Telcel bands | NTRIP path | Effort (1–5) |
|
||||
|--------|-------|-----|--------------|-----------|--------------|
|
||||
| **LILYGO T-A7670SA** (ESP32 + A7670SA) | ≈US$33 | [lilygo.cc](https://lilygo.cc/products/t-sim-a7670e) / AliExpress | ✔ (SA = LATAM, incl. B2/B4/B66) | ESP32 sketch: TinyGSM ([lewisxhe fork](https://github.com/lewisxhe/TinyGSM-fork)) + [Arduino NTRIP client](https://github.com/GLAY-AK2/NTRIP-client-for-Arduino); ESP32 does the client, streams RTCM out its UART | **2** — best bench experiment |
|
||||
| **A7670SA raw module** | **US$9.39** | [LCSC](https://www.lcsc.com/product-detail/2G-3G-4G-5G-Modules_SIMCom-Wireless-Solutions-A7670SA-LASC_C18548276.html) / MercadoLibre | ✔ | AT commands (CIPOPEN/CIPSEND) — NTRIP handshake + reconnect/watchdog logic on the Blackpill | **4** — the cost end-game for the hundreds-of-cabinets phase |
|
||||
| SIM7600SA-H (Waveshare HAT / LILYGO T-SIM7600) | US$40–77 | Waveshare / LILYGO | ✔ | Same AT/ESP32 paths | 2–4 — Cat-4 is overkill, no cost win vs A7670SA |
|
||||
| SIM7670G (Cat-1 bis) | US$30–45 | [Waveshare ESP32-S3 combo](https://www.waveshare.com/esp32-s3-sim7670g-4g.htm) | ✔ | Same | 2–3 |
|
||||
| SIM7000G (Cat-M1/NB-IoT) | US$40–69 | Botletics | Bands ✔ but **LTE-M is a Telcel enterprise product** — SIM provisioning risk | AT/TCP | 4 — **avoid** |
|
||||
| **A7670E / SIM7600E** (Europe variants on MercadoLibre) | — | — | ✘ **no B2/B4/B66** | — | **Do not buy** |
|
||||
| Quectel EC25-AUX / EG25-G (mPCIe) | €44–60 | Sixfab/Waveshare | ✔ | AT/PPP + mPCIe carrier plumbing | 4 |
|
||||
| USR-G771 4G DTU | ≈US$43 | [usr.cn](https://en.usr.cn/RS232-RS485-To-4G-modem) / Amazon | **verify variant** (-E = Europe) | Transparent TCP + "registration packet" carrying the NTRIP GET — no MCU firmware | 2–3 — closest cheap drop-in *if* a MX-band variant exists |
|
||||
| **Digi XBee 3 Global LTE Cat 1** | ≈US$76 | [Digi-Key](https://www.digikey.com/en/product-highlight/d/digi-intl/xbee-3-global-lte-cat-1) | ✔ (global) | **MicroPython NTRIP client runs ON the module** (sockets API; [precedent](https://www.ardusimple.com/question/digi-xbee-3g-lte-micropython-ntrip-server-esp32-xbee-module/)) | 3 — the only true XBee-socket drop-in; keeps the EinarBoard footprint |
|
||||
|
||||
Reference: Ardusimple's own WiFi NTRIP Master firmware is open source
|
||||
([nebkat/esp32-xbee](https://github.com/nebkat/esp32-xbee), ESP-IDF NTRIP client/caster) —
|
||||
an ESP32-as-NTRIP-bridge is a solved problem; 4G needs the WiFi transport swapped for
|
||||
TinyGSM/PPP. Industrial NTRIP DTUs (Tersus TR600, Hi-Target) exist but run US$300–800 —
|
||||
*more* than the Ardusimple, not less.
|
||||
|
||||
For scale reference: **Ardusimple 4G NTRIP Master = €156** (≈MX$3,250), in stock, DHL 2–5
|
||||
days, **€112 at ≥50 units** ([product page](https://www.ardusimple.com/product/4g-ntrip-master/)).
|
||||
LTE Cat 4, NA variant covers Telcel B2/B4/B5.
|
||||
|
||||
## 3. Recommendation
|
||||
|
||||
1. **This week (bench, keeps 17-Jul comparison on track):** order Option A (~MX$100,
|
||||
SANDOROBOTICS) and breadboard the 4G NTRIP Master we already own per Guía v3.0 §9a–9c.
|
||||
Zero firmware risk.
|
||||
2. **Parallel experiment (~US$35):** one **LILYGO T-A7670SA** + TinyGSM NTRIP sketch
|
||||
(~1 day of firmware). If it holds RTK for a week on the bench, it becomes the certified
|
||||
cost-reduction candidate to qualify properly.
|
||||
3. **30 units / August:** stay on the Ardusimple (€112 @ ≥50). It is the only zero-firmware,
|
||||
field-proven option and the timeline doesn't absorb a bearer swap. Invariant #3
|
||||
(corrections ride the 4G; recover by power-cycling) is already engineered around it.
|
||||
4. **Hundreds-of-cabinets phase:** A7670SA soldered onto the EinarBoard successor
|
||||
(~16× cheaper bearer) **budgeting real firmware** (NTRIP-over-AT + reconnect watchdog —
|
||||
the part Ardusimple already does well) + carrier homologation; or the **Digi XBee 3 Cat 1**
|
||||
(US$76) as the middle path that drops into the existing XBee socket with the NTRIP client
|
||||
in MicroPython on-module. Decide after the LILYGO experiment.
|
||||
|
||||
## 4. Cross-references
|
||||
|
||||
- Technician instructions (Spanish): `Drogheda_Guia-Conexion-Protoboard_ES_v3.0.pdf` §9a–9c.
|
||||
- PROD context: 3V3_MODEM rail (Block K) + K1 NC bypass — `watchdog-electrical-design-state.md`.
|
||||
- Invariants touched: #1 (power-off safe), #2/#3 (never vanish; corrections ride the 4G —
|
||||
any replacement bearer must keep the power-cycle recovery path).
|
||||
233
docs/historia/analisis-bitacoras-banco.md
Normal file
233
docs/historia/analisis-bitacoras-banco.md
Normal file
@ -0,0 +1,233 @@
|
||||
---
|
||||
title: "Drogheda HBOX — Bench Log + Config Analysis (20 Jun 2026)"
|
||||
created: 2026-06-20
|
||||
revised: 2026-06-20 (rev 2 — corrected swap labels + config files)
|
||||
owner: Leo Rodriguez (Helgen)
|
||||
status: draft-for-review
|
||||
type: test-evidence
|
||||
scope: 7 swap-matrix bench captures + NTRIP-caster & u-blox config files, v1 firmware
|
||||
source_logs: "Drive: 06-Testing-Validation/Logs Prueba 20JUN26 (*_GPS.ubx, *_NTRIP.log, 4G_NTRIP_HelgenSRV-NMEA.txt, configuration_GNSS.txt)"
|
||||
tooling: "06-Testing-Validation/log-analysis/analyze_logs.py + dashboard.html"
|
||||
tags: [drogheda, hbox, rtk, ntrip, gnss, bench-test, rca, thermal, config]
|
||||
---
|
||||
|
||||
# Drogheda HBOX — Bench Log + Config Analysis
|
||||
|
||||
**One-line verdict (rev 3):** Of 7 bench captures, 4 pass the comms chain and
|
||||
**only 1 is a confirmed fault** — `RTK21NTRIP21` (master 21: modem won't register,
|
||||
511 AT errors). The two "no-forward" captures are **indeterminate**, not failed: the
|
||||
NTRIP-master console can't prove forwarding either way (the healthy RTK14 baseline
|
||||
logs the same sparse console). The confirmed fault reproduces cold → not heat. The
|
||||
u-blox config is correct (GGA on UART2 @1 Hz); corrections come from the **free public
|
||||
rtk2go caster**, and no unit reached RTK fix (SPS only — bench sky + corrections rarely
|
||||
applied).
|
||||
|
||||
> **Rev-3 correction (post-baseline, 2026-06-20).** A healthy RTK14 capture
|
||||
> (`RTK14NTRIP14_440`) — **proven forwarding 1 Hz to the server by tcpdump and live in
|
||||
> Wialon** — logs the *same* sparse NTRIP-master console (only `NET`; no `State main` /
|
||||
> `UDP Bytes sent`) as the captures I had called "no-forward." **Absence of those console
|
||||
> lines does not mean a unit failed.** So the earlier **"master-14 ingest fault" is
|
||||
> withdrawn** (master 14 demonstrably forwards — it *is* the baseline). Only
|
||||
> `RTK21NTRIP21` is a confirmed fault; `RTK14NTRIP14` and `RTK21NTRIP14` are
|
||||
> **indeterminate**. §§3–4 below predate this and are superseded on the no-forward point.
|
||||
> The structural fix: make the **Blackpill log fix + packets + state** so health is
|
||||
> unambiguous — see `04-Source-Code/firmware/docs/LOGGING-SPEC.md`.
|
||||
|
||||
> **Baseline (RTK14NTRIP14_440), 3 views:** receiver `.ubx` = 341 epochs, 100% valid,
|
||||
> **12 sats, HDOP 0.75, SPS** (C/N0 ~20); server tcpdump = **984 pkts, steady 1 Hz, max
|
||||
> gap 0.86 s, zero dropouts** to `…:5010`; master console = registered only. Healthy
|
||||
> end-to-end **but SPS, not RTK** (corrections not applied). Use as the healthy-GNSS
|
||||
> baseline; raise the bar to a fix≥4 capture once corrections work.
|
||||
|
||||
> **Rev-2 note:** two captures were re-labelled by their swap pair —
|
||||
> `RTK04NTRIP24 → RTK24NTRIP04` and `RTK14NTRIP21 → RTK21NTRIP14` (byte-identical,
|
||||
> md5-verified). This **moved the no-forward fault from "board 14" to "master 14"**
|
||||
> (the element common to both no-forward captures). Earlier drafts said board 14.
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive summary
|
||||
|
||||
Two log streams were captured per unit on the Querétaro bench (20 Jun 2026):
|
||||
`*_GPS.ubx` (the ZED-F9P over USB, via u-center) and `*_NTRIP.log` (the 4G NTRIP
|
||||
Master console). Filenames `RTK<bb>NTRIP<mm>` = RTK board `bb` + NTRIP master `mm`,
|
||||
forming a **swap matrix** that isolates board vs master faults. Two config files
|
||||
were also analysed: the 4G master config and the F9P config dump.
|
||||
|
||||
**What the bench can / cannot show:** it validates the **integration/comms chain**
|
||||
(boot → GNSS position → cell registration → UDP forwarding). It does **not** validate
|
||||
RTK accuracy (needs open sky + live corrections) and **cannot** measure heat (v1 has
|
||||
no temperature channel; the bench runs cold).
|
||||
|
||||
| Outcome | Captures | Why |
|
||||
|---|---|---|
|
||||
| **Healthy (bench)** | RTK17NTRIP17, RTK24NTRIP04, RTK24NTRIP24, RTK25NTRIP25 | boot → GNSS alive → TELCEL registered → UDP forwarding |
|
||||
| **Failed — no forwarding** | RTK14NTRIP14, RTK21NTRIP14 | GNSS valid on USB, modem registered, but **0 UDP** and no state machine — **both use master 14** |
|
||||
| **Failed — modem** | RTK21NTRIP21 | modem **never registers** (`0xFFFFF`, **511× AT-parser errors**), stuck in init |
|
||||
|
||||
**Isolated faults:**
|
||||
|
||||
1. **NTRIP master 14 — GGA ingest.** With *two different* GPS boards (14 and 21),
|
||||
master 14 forwarded **nothing** while the F9P produced valid NMEA on USB. The
|
||||
common element is the master → **master 14's serial input / NMEA ingest**, not the
|
||||
board. (See config note: GGA *is* enabled on UART2, so it is not a message-rate
|
||||
error.)
|
||||
2. **NTRIP master 21 — LTE modem.** Will not register on Telcel; continuous
|
||||
AT-parser errors. The GPS feeding it (board 21) is fine.
|
||||
|
||||
Neither is the GNSS receiver; neither needs heat.
|
||||
|
||||
---
|
||||
|
||||
## 2. The heat question (headline for the deck)
|
||||
|
||||
**These logs do not prove the field failures are thermal — and that is useful.**
|
||||
They are room-temperature bench captures with no thermal instrumentation, yet they
|
||||
reproduce hard faults **cold** (master-14 ingest, master-21 modem). A fault you can
|
||||
reproduce cold is **not** caused by heat → the fleet problem is **not a single
|
||||
thermal cause**, matching the RCA's "don't collapse everything into heat."
|
||||
|
||||
This neither confirms nor refutes the **field** thermal hypothesis (18650 / boost / modem
|
||||
in a sealed cabinet in Veracruz sun, RCA H1/H7) — that needs a **thermocouple soak**
|
||||
of a representative cabinet and/or **v2 MCU-temp logging**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Per-device findings
|
||||
|
||||
Bench context is common: indoor/obstructed sky → **C/N0 ≈ 17–21 dB-Hz**, high HDOP,
|
||||
**zero RTK fixes** anywhere. Read the GNSS columns as "is the receiver alive and
|
||||
emitting valid NMEA," not as a positioning grade.
|
||||
|
||||
| Capture | Board / Master | Status | GNSS (USB) | Cell | UDP out | dBm | AT err | Headline |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| **RTK24NTRIP04** | 24 / 04 | 🟢 Healthy | alive, SPS, 98% valid | TELCEL | **1555 dg** | −51 | 0 | full chain OK (master 04 good) |
|
||||
| **RTK24NTRIP24** | 24 / 24 | 🟢 Healthy | alive, SPS, 85% valid | TELCEL | **1831 dg** | −51 | 0 | full chain OK |
|
||||
| **RTK25NTRIP25** | 25 / 25 | 🟢 Healthy | alive, SPS, 100% valid | TELCEL | **2413 dg** | −51 | 0 | best forwarder |
|
||||
| **RTK17NTRIP17** | 17 / 17 | 🟢 Healthy | alive, 12% valid (poor sky this run) | TELCEL | **2267 dg** | −63 | 0 | comms healthiest; GNSS had no sky |
|
||||
| **RTK14NTRIP14** | 14 / 14 | 🔴 Failed | **alive, SPS, 100% valid** | TELCEL | **0** | −75 | 0 | registered but forwards nothing |
|
||||
| **RTK21NTRIP14** | 21 / 14 | 🔴 Failed | **alive, SPS, 100% valid, 10 sats** | TELCEL | **0** | −83 | 0 | same master-14 symptom, other board |
|
||||
| **RTK21NTRIP21** | 21 / 21 | 🔴 Failed | alive, SPS, 97% valid | **NOT reg** | **0** | −113 | **511** | modem never attaches |
|
||||
|
||||
---
|
||||
|
||||
## 4. Swap-matrix analysis (isolation)
|
||||
|
||||
**By NTRIP master:**
|
||||
|
||||
| Master | Captures | Result |
|
||||
|---|---|---|
|
||||
| 04, 17, 24, 25 | one each | **healthy** — forwards position |
|
||||
| **14** | RTK14NTRIP14 (board 14) **and** RTK21NTRIP14 (board 21) | **both fail to forward** despite valid USB NMEA |
|
||||
| **21** | RTK21NTRIP21 (board 21) | modem **not registered**, AT errors |
|
||||
|
||||
→ Master 14 fails with two different boards → **fault is master 14's NMEA ingest**
|
||||
(serial input), confirmed common factor. Master 21 is a modem-registration fault.
|
||||
Masters 04/17/24/25 are good.
|
||||
|
||||
**By RTK board:**
|
||||
|
||||
| Board | Captures | Read |
|
||||
|---|---|---|
|
||||
| 24 | NTRIP04 (✓), NTRIP24 (✓) | **healthy with two masters** → board 24 GNSS + UART2 output good |
|
||||
| 17, 25 | one each (✓) | healthy |
|
||||
| 21 | NTRIP14 (master-14 fault), NTRIP21 (master-21 fault) | GNSS alive on USB; its failures belong to the masters it was paired with |
|
||||
| 14 | NTRIP14 only (master-14 fault) | GNSS alive on USB; **board-14 UART2 output unproven** — only ever paired with the bad master 14 |
|
||||
|
||||
**Open item:** test board 14 (and board 21) with a *known-good* master to clear/convict
|
||||
their UART2 output; test master 14 with a known-good board to confirm the ingest fault.
|
||||
|
||||
---
|
||||
|
||||
## 5. Configuration findings (from the two config files)
|
||||
|
||||
### 5.1 4G NTRIP Master (`4G_NTRIP_HelgenSRV-NMEA.txt`)
|
||||
|
||||
| Item | Value | Note |
|
||||
|---|---|---|
|
||||
| NTRIP caster | **rtk2go.com : 2101**, mountpoint **HelgenTestStation** | **free public community caster — best-effort, no SLA** |
|
||||
| APN | internet.itelcel.com (Telcel) | correct |
|
||||
| UART baud (to F9P) | 115200 | matches F9P UART2 baud |
|
||||
| NTRIP restart timer | **300 s** | matches "NTRIP DELAY n/300" in the logs |
|
||||
| Telemetry out | UDP → Helgen server :5010, device **H0014** | position forwarding path |
|
||||
| PointPerfect | configured but disabled | not in use |
|
||||
|
||||
> **Flag:** corrections from a **free public caster (rtk2go)** is a reliability risk
|
||||
> for a production fleet and a plausible reason the correction client rarely connected
|
||||
> and no unit reached RTK. Move production to a **dedicated/own caster (HelgenSRV)**.
|
||||
|
||||
### 5.2 ZED-F9P (`configuration_GNSS.txt`, UBX CFG-VALGET dump)
|
||||
|
||||
| Item | Value | Note |
|
||||
|---|---|---|
|
||||
| Model / FW | ZED-F9P, HPG 1.32 | rover-grade |
|
||||
| Mode | **Rover** (TMODE=0) | correct (not a base) |
|
||||
| Update rate | **1 Hz** (1000 ms) | correct |
|
||||
| **GGA on UART2** | **ENABLED (1/epoch)** | the feed the master needs — **correctly configured** |
|
||||
| RMC on UART2 | ENABLED (1/epoch) | not required by NTRIP, harmless |
|
||||
| UART2 | enabled · NMEA-out + RTCM3-in · 115200 | textbook 4G-NTRIP rover wiring |
|
||||
| USB | full NMEA (GGA/GLL/GSA/GSV/RMC/VTG) | matches the `.ubx` captures |
|
||||
| **Dump layer** | **RAM only** | **flash persistence NOT verified** |
|
||||
|
||||
> **Two implications:**
|
||||
> 1. **The message config is correct** — GGA *is* on UART2 @1 Hz. So master 14's dead
|
||||
> feed is **not** a missing-GGA setting → it's the master's ingest (or a per-unit
|
||||
> deviation), reinforcing §4.
|
||||
> 2. **The dump is RAM-layer only.** A RAM-only config is **lost on every power cycle**
|
||||
> (the classic "forgot Save Config to flash" trap). Confirm the **FLASH layer** on
|
||||
> each unit — a unit that boots without GGA-on-UART2 would feed the master nothing.
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommendations / next steps
|
||||
|
||||
1. **Master 14 — bench the ingest.** Feed master 14 a known-good GGA stream (or pair
|
||||
with a healthy board) and watch for `State main` + `UDP Bytes sent`. If still zero,
|
||||
the master's serial-in / firmware is the fault → swap/repair the master.
|
||||
2. **Master 21 — modem triage.** Re-test registration with a known-good SIM + antenna;
|
||||
511 AT-parser errors + `0xFFFFF` point at the LTE modem / its AT link / power. Swap
|
||||
the modem to confirm.
|
||||
3. **Persist & verify the F9P config to FLASH** on every unit (u-center → Save Config),
|
||||
then dump the **flash layer** to prove GGA-on-UART2 survives a power cycle. Check the
|
||||
simpleRTK2B Pro **XBEE↔UART2 switch** too (UART2 is off by default).
|
||||
4. **Move corrections off rtk2go** to a dedicated caster (HelgenSRV) with uptime, then
|
||||
**re-run outdoors** with open sky to actually grade RTK (quality 4/5).
|
||||
5. **Close the heat question separately** — thermocouple soak of a sealed cabinet in
|
||||
full sun and/or v2 MCU-temp logging.
|
||||
6. **Retranslator log (separate effort)** — pair this unit-side analysis with the
|
||||
backend (HOL-stall / anti-jump) evidence for the full RCA picture.
|
||||
|
||||
---
|
||||
|
||||
## 7. Method & reproducibility
|
||||
|
||||
- **Tool:** `log-analysis/analyze_logs.py` (Python stdlib). Parses NMEA (GGA/RMC/GSV) +
|
||||
UBX from `.ubx`, and the GPTXT diagnostic stream (`State main`, `NET`, `UDP Bytes
|
||||
sent`, `NTRIP DELAY`, `SM_REG`, AT errors) from `.log`. Emits per-device metrics,
|
||||
a swap-matrix with **common-factor isolation**, and per-device time-series.
|
||||
- **Dashboard:** `log-analysis/dashboard.html` — dark (GitHub Dark-Dimmed),
|
||||
self-contained: config summary, signal-flow diagram (marks the break + names the
|
||||
common master), zoomable time-series, status board, swap matrix.
|
||||
- **Config decode:** 4G master `$$$AS+` params; F9P UBX **CFG-VALGET** keys
|
||||
(CFG-MSGOUT-NMEA_ID_GGA_UART2, CFG-UART2*, CFG-RATE*, CFG-TMODE-MODE).
|
||||
- **Re-run:** `python3 analyze_logs.py --in raw_cur --out out && python3 build_dashboard.py`
|
||||
- **Validation:** classification spot-checked vs raw logs; the two relabels were
|
||||
md5-verified byte-identical to the originals.
|
||||
|
||||
**Caveats:** ~10-min captures; indoor bench sky; v1 (no temperature); F9P config dump
|
||||
is RAM-layer only; `ntr/tcp/pp` enum values inferred from behavior. None affect the
|
||||
pass/fail conclusions, which rest on presence/absence of UDP forwarding, cellular
|
||||
registration, and the swap-matrix common factor.
|
||||
|
||||
---
|
||||
|
||||
## 8. Glossary
|
||||
|
||||
- **GGA fix quality:** 0 no-fix · 1 SPS · 2 DGPS · **4 RTK-fixed** · **5 RTK-float**.
|
||||
- **C/N0 (dB-Hz):** satellite carrier-to-noise; ~35–50 open sky, ~20 indoors.
|
||||
- **NTRIP master state `main,ntr,tcp,pp,ntr_srv,udp`:** main=4 operational / 1 init;
|
||||
udp=1 forwarding position out; ntr = correction-client state (0 idle … 2 active).
|
||||
- **`NET,<op>,<dBm>,<MCCMNC>`:** TELCEL = registered; **`0xFFFFF` = not registered**.
|
||||
- **NTRIP DELAY n/300:** seconds since last correction vs the 300 s restart watchdog.
|
||||
- **GGA→UART2:** whether the F9P's GGA is reaching the 4G master over UART2 (XBee
|
||||
socket). Inferred from the master running its loop + forwarding UDP.
|
||||
143
docs/historia/bateria-alta-temperatura.md
Normal file
143
docs/historia/bateria-alta-temperatura.md
Normal file
@ -0,0 +1,143 @@
|
||||
---
|
||||
tags: [project-drogheda, hardware, battery, thermal, power]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO
|
||||
date: 2026-09-09
|
||||
status: Draft — decision input
|
||||
---
|
||||
|
||||
# Backup cell at 45 °C: why Li-ion is failing, and what replaces it
|
||||
|
||||
> **Bottom line:** LiPo is not an upgrade — a LiPo pouch *is* Li-ion, in a worse package.
|
||||
> And before changing any cell: **`RNTB` = 10 k makes the LTC4040 stop charging at
|
||||
> +40…+42 °C.** At a 45 °C port the pack essentially never charges. That is a resistor,
|
||||
> not a chemistry. The chemistry that then lets you *raise* that limit is
|
||||
> **high-temperature LiFePO4**, which the LTC4040 already supports — `JP2` re-strap, no respin.
|
||||
|
||||
---
|
||||
|
||||
## 1. LiPo is the wrong direction
|
||||
|
||||
A "LiPo" pouch is lithium-ion (LCO or NMC) in a soft laminate bag. Same 0–45 °C charge
|
||||
window, same calendar aging, and **worse** in a hot sealed box: no vent, no CID, and it
|
||||
swells. The 2000 mAh cells are bench hardware. Keep them on the bench.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🔴 The charger is refusing to charge — and it is our resistor
|
||||
|
||||
`RNTB` (10 k 1 %) biases the pack NTC (`RT1`, 10 k, on the cell via `P4.3`). The LTC4040
|
||||
pauses charging when the NTC falls to **53.6 % of `RBIAS`** and resumes below **325 %**.
|
||||
With `RBIAS` = 10 k that puts the charge window at:
|
||||
|
||||
| NTC β | Charge window today |
|
||||
|---|---|
|
||||
| B3380 | **−3.1 °C … +42.4 °C** |
|
||||
| B3950 | **+0.6 °C … +39.7 °C** |
|
||||
|
||||
Port operating temperature is ~45 °C. **The pack sits above the hot-pause threshold
|
||||
for most of the working day**, so it never reaches full charge and the backup is not
|
||||
there when the power drops. The MCU's own `CHRG_EN` hot-cut sits on top of this.
|
||||
|
||||
This is very likely most of the "poor performance" — and it is **not** the cell's fault.
|
||||
Note the earlier fleet finding blamed no-charge on the LTC4040 safety-timer latch; a
|
||||
pack that is repeatedly hot-paused mid-charge is exactly how you *reach* that latch.
|
||||
|
||||
### Re-scaling `RNTB`
|
||||
|
||||
You cannot simply raise this on the current cell — an NMC/LCO 18650 **must not** be
|
||||
charged at 55 °C. Raise it *together with* the chemistry change:
|
||||
|
||||
| Target hot pause | β | New `RNTB` | Resulting window |
|
||||
|---|---|---|---|
|
||||
| +55 °C | B3380 | **6.62 k** | +6.1 … +55 °C |
|
||||
| +55 °C | B3950 | 5.56 k | +12.3 … +55 °C |
|
||||
| **+60 °C** | **B3380** | **5.67 k → 5.6 k E24** | **+9.7 … +60 °C** |
|
||||
| +60 °C | B3950 | 4.64 k | +16.0 … +60 °C |
|
||||
|
||||
`RBIAS` sets both ends, so raising the hot limit raises the cold one. **Specify the
|
||||
B3380 NTC** (the delta doc already calls B3380 "ideal", B3950 "acceptable") — with B3950
|
||||
you would refuse to charge below +16 °C, which is a real winter night in Ensenada.
|
||||
|
||||
⚠ Computed from the datasheet's 53.6 % / 325 % ratios and a β model with R25 = 10 k.
|
||||
Check against the actual NTC part's R/T table before cutting the BOM.
|
||||
|
||||
---
|
||||
|
||||
## 3. The cell: high-temperature LiFePO4 26650
|
||||
|
||||
| | Today — NMC/LCO 18650 | **High-temp LFP 26650** |
|
||||
|---|---|---|
|
||||
| Charge temp | 0…45 °C | **−20…60 °C** (Topwell 3600 mAh) · **0…75 °C** (JGNE 4000 mAh) |
|
||||
| Discharge temp | −20…60 °C | −50…80 °C · −30…75 °C |
|
||||
| Nominal | 3.6 V | 3.2 V |
|
||||
| Cell energy | ~11 Wh (3 Ah) | 11.5 Wh (3.6 Ah) · **12.8 Wh (4 Ah)** |
|
||||
| Cycle life | 300–500 | **≥ 2000** to 80 % |
|
||||
| Thermal runaway onset | ~150–200 °C | ~270 °C (olivine) — does not propagate |
|
||||
| Calendar aging at 45 °C | fast | far slower |
|
||||
|
||||
**4P of the 3.6 Ah cell = 46 Wh; 4P of the 4 Ah = 51 Wh** — both at or above today's
|
||||
~43 Wh. Even 3P clears 38 Wh.
|
||||
|
||||
### The board already supports it — one jumper
|
||||
|
||||
LTC4040 `F2` selects chemistry: **high = Li-Ion, low = LiFePO4**. Float table:
|
||||
|
||||
| F1 | F0 | F2 = 1 (Li-Ion) | F2 = 0 (LiFePO4) |
|
||||
|---|---|---|---|
|
||||
| 0 | 0 | 3.95 V | 3.45 V |
|
||||
| **0** | **1** | **4.00 V ← we are here** | **3.50 V ← we land here** |
|
||||
| 1 | 0 | 4.05 V | 3.55 V |
|
||||
| 1 | 1 | 4.10 V | 3.60 V |
|
||||
|
||||
Current straps are F2=1 / F1=0 / F0=1. **Flip `JP2` (F2) only** → 3.50 V LFP float.
|
||||
The 2026-07-06 pre-fab audit already recorded this path as preserved. No schematic
|
||||
change, no respin. 3.50 V on LFP is ~90 % SoC — deliberately shy of 3.65 V, which is
|
||||
exactly what you want for calendar life at temperature. `JP3` to F1=1 gives 3.60 V if
|
||||
you later want the last few percent.
|
||||
|
||||
### What else must change
|
||||
|
||||
1. **`RNTB` → 5.6 k** (§2) — without this the swap changes nothing; the charger still
|
||||
pauses at 42 °C.
|
||||
2. **Firmware `BAT_CRITICAL` / `BAT_RECOVERY`.** LFP's discharge curve is flat at ~3.2 V
|
||||
from 90 % to 15 % SoC. Voltage-based SoC stops working. Re-derive the thresholds
|
||||
against the knee (~2.9 V), and expect the gauge to be near-useless mid-band —
|
||||
coulomb-count or accept a three-state indicator. The firmware review already flagged
|
||||
these thresholds as bench-tune items for the 4.00 V pack; this makes it mandatory.
|
||||
3. **MCU `CHRG_EN` hot-cut threshold** — raise in step with `RNTB`, or it becomes the
|
||||
new binding limit.
|
||||
4. **LTC4040 boost current.** 3.2 V nominal instead of 3.6 V ⇒ ~12 % more pack current
|
||||
for the same backup watts. Re-check against the boost limit and the `RS1` errata fix.
|
||||
5. **Mechanical.** 26650 is 26.5 mm vs 18.5 mm and ~85 g/cell. New ASA battery-case
|
||||
print, and re-check the 4P footprint inside the HBOX.
|
||||
6. **Sourcing.** The high-temp grades are Chinese-supplier parts (JGNE, Topwell class),
|
||||
not a shelf item in MX. Check MOQ, lead time and whether they will give a real R/T
|
||||
and cycle-at-temperature spec before this goes on the 30-unit BOM.
|
||||
|
||||
---
|
||||
|
||||
## 4. Alternatives, briefly
|
||||
|
||||
| Option | Verdict |
|
||||
|---|---|
|
||||
| **High-temp LFP 26650** | ✅ **Recommended.** Solves charge-at-temperature, board already supports it, cost/Wh comparable |
|
||||
| **Supercap / LIC hold-up** | −40…+65/85 °C, no charge-temperature window at all, effectively infinite cycles — but ~1/20 the energy. **The right answer if the real requirement is "report a power-loss alarm and shut down cleanly" rather than "run for hours".** Pin the requirement before dismissing it again |
|
||||
| **Smaller pack, better cell** | Nobody has pulled this lever. 43 Wh is a lot for a parked machine. Halve the requirement and the thermal problem halves with it |
|
||||
| **LTO** | Best temperature range and cycle life, but 2.4 V nominal breaks the LTC4040's 1S window entirely — new charger. Low energy density, expensive, hard to source. **Out** |
|
||||
| **High-temp-rated NMC 18650** | Keeps the form factor, but you are still fighting the same chemistry for a few °C. Marginal |
|
||||
| **NiMH** | High self-discharge at temperature, and trickle-charging hot is its own failure mode. **Out** |
|
||||
|
||||
---
|
||||
|
||||
## 5. Do this first — you already have the data
|
||||
|
||||
Do **not** buy cells on an assumption. `tbat` (TMP102 on the pack) and `tntc` are already
|
||||
in `$PHLG` and already landing in `htracker`. Pull the fleet histogram and answer:
|
||||
|
||||
1. What is the **actual** pack temperature distribution, by unit and by hour?
|
||||
2. What **fraction of the day** does it sit above the current ~40–42 °C hot pause?
|
||||
3. Does `cflt` / charge-fault correlate with those windows?
|
||||
|
||||
Question 2 is the number that decides whether this is a resistor change, a chemistry
|
||||
change, or both. It is one query against data we are already collecting.
|
||||
72
docs/historia/consulta-vectro-psm.md
Normal file
72
docs/historia/consulta-vectro-psm.md
Normal file
@ -0,0 +1,72 @@
|
||||
---
|
||||
tags: [project-drogheda, comms, vendor, telecom]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO
|
||||
date: 2026-09-10
|
||||
status: Draft para envío — cubre la tarea D2 del procedimiento de evaluación
|
||||
---
|
||||
|
||||
# Correo a Vectro — PSM / eDRX en las líneas Telcel
|
||||
|
||||
> Cubre **D2** de `2026-09-10_Drogheda_Backup-Power_Evaluation-Procedure_v1.0.md`.
|
||||
> Las preguntas 1–4 están redactadas para reenviarse **tal cual** a Telcel; las 5–7 sólo
|
||||
> las puede responder Vectro.
|
||||
|
||||
---
|
||||
|
||||
**Asunto:** Consulta técnica — modos de ahorro de energía PSM / eDRX en las líneas Telcel
|
||||
|
||||
Estimado [Nombre]:
|
||||
|
||||
Requerimos confirmar si las líneas Telcel que nos suministran soportan **PSM (Power Saving
|
||||
Mode)** y **eDRX (extended DRX)**, definidos en **3GPP Release 13**. La respuesta determina
|
||||
el diseño de nuestro modo de respaldo por batería, por lo que agradeceremos su apoyo para
|
||||
escalar la consulta con Telcel si no está a su alcance responderla directamente.
|
||||
|
||||
**Contexto:** operamos equipos de telemetría GPS/RTK en terminales portuarias. Ante un corte
|
||||
de energía, el equipo debe seguir reportando posición y estado durante varios días con
|
||||
batería. PSM permite que el módem duerma a ~10 µA **manteniendo el registro en la red**, en
|
||||
lugar de volver a registrarse en cada envío. En nuestros cálculos la diferencia en autonomía
|
||||
es de **días contra semanas**, con el mismo hardware.
|
||||
|
||||
## Preguntas técnicas (pueden reenviarse tal cual a Telcel)
|
||||
|
||||
1. ¿Está habilitado **PSM** en el APN y el perfil de SIM que tenemos contratado, para
|
||||
dispositivos **LTE Cat-1bis**?
|
||||
2. En caso afirmativo, ¿qué valores otorga la red para el **T3412 extendido (TAU)** y el
|
||||
**T3324 (Active Timer)**? Nuestra solicitud sería **TAU de 30 a 60 minutos** y
|
||||
**Active Timer de 0 a 60 segundos**.
|
||||
3. ¿Está habilitado **eDRX**? ¿Cuál es el **ciclo máximo** que otorga la red?
|
||||
4. Si PSM o eDRX no están disponibles en nuestro APN actual, ¿existe un **APN de IoT/M2M**
|
||||
donde sí lo estén, y cuál sería el procedimiento y el costo de migrar nuestras líneas?
|
||||
|
||||
## Preguntas comerciales y operativas (para Vectro)
|
||||
|
||||
5. ¿Existe alguna política de **suspensión o baja por inactividad**? Un equipo en PSM puede
|
||||
permanecer **varios días sin generar tráfico**, y necesitamos confirmar que la línea no se
|
||||
suspenda ni se dé de baja por ese motivo.
|
||||
6. ¿El plan factura por **sesión / attach**, o únicamente por **volumen de datos**?
|
||||
7. ¿Podemos habilitar **una línea de prueba** con PSM activo, para validarlo en banco antes
|
||||
de aplicarlo a la flota?
|
||||
|
||||
Agradeceremos su respuesta **a más tardar el viernes 19 de septiembre**, ya que esta
|
||||
definición bloquea la siguiente etapa de nuestro desarrollo.
|
||||
|
||||
Quedo atento a sus comentarios.
|
||||
|
||||
Saludos cordiales,
|
||||
|
||||
**Leo Rodríguez**
|
||||
Helgen Technologies
|
||||
|
||||
---
|
||||
|
||||
## Notas para Leo (no enviar)
|
||||
|
||||
- Ajustar **[Nombre]** y la fecha del compromiso.
|
||||
- La **pregunta 5 es la más importante para Vectro** y la que un revendedor suele pasar por
|
||||
alto: una política de inactividad que suspenda la SIM anularía todo el beneficio de PSM.
|
||||
- Si Vectro responde "no sé", el objetivo mínimo del correo es la **pregunta 7** — con una
|
||||
sola línea de prueba podemos medir en banco lo que la red realmente otorga
|
||||
(`AT+CPSMS?` y `AT+CEDRXRDP` devuelven los valores concedidos, que casi nunca son los
|
||||
solicitados) y dejar de depender de la respuesta comercial.
|
||||
259
docs/historia/einarboard-10-prefab-audit.md
Normal file
259
docs/historia/einarboard-10-prefab-audit.md
Normal file
@ -0,0 +1,259 @@
|
||||
---
|
||||
title: EINARBoard HBOX 1.0 — Pre-Fabrication Design Audit
|
||||
tags: [project-drogheda, hardware, watchdog, pcb, audit, einarboard]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO (Claude audit, reviewed by Leo)
|
||||
date: 2026-07-06
|
||||
status: CLOSED 2026-07-07 — all four HIGH findings fixed and re-verified; bare-board order released (see closure note)
|
||||
---
|
||||
|
||||
> **CLOSURE NOTE (2026-07-07):** H1 (Q7 S/D) fixed board-side by Leo + schematic redrawn (D=MAIN5V,
|
||||
> S=VIN_SW, verified visually and in file); H2 (Q5/Q6 BEC) fixed in schematic (Q_NPN_BEC) + board pads
|
||||
> swapped and rerouted; H3 (Rig 22k + dangling pin) fixed both sides — fab BOM now reads "RDFU1, RIG1 =
|
||||
> 22k"; H4 (SW_IN) in PWR_MID at 1.0 mm (19 segs). Re-verification: ERC 0 errors (172 cosmetic warnings) ·
|
||||
> DRC 0 errors / 0 unconnected (14 warnings: 10 lib-mismatch + 4 silk) · U2/U7 EP GND intact · refdes
|
||||
> converged on numbered names (annotator-proof) · fab package regenerated 11:59 after last board save.
|
||||
> **F8 is permanently retired on this board** — sync is manual (see memory/process note). Bare-board
|
||||
> order RELEASED conditional on the two physical gates (M3: D0/D1 on a physical Pro; M4: HBOX fit).
|
||||
> PCBA still blocked by M1 (LCSC coverage 18/77 lines).
|
||||
>
|
||||
> **M3 CLOSED (2026-07-07):** verified on a physical simpleRTK2B Pro — silk reads D0=TX1, D1=RX1,
|
||||
> D2=RX2 (shared XBee-TX), D3–D7 empty, D8=TX2 (shared XBee-RX). Carrier mapping confirmed
|
||||
> position-for-position.
|
||||
>
|
||||
> **H5 FINAL DISPOSITION (2026-07-07 evening, OWNER DECISION):** board released at J1↔J2 =
|
||||
> **15.03 mm** (J2 y=71.35). Leo measured the physical module repeatedly with a vernier and
|
||||
> stands on **15.0 mm** centers; the auditor's reading of the evidence remains **15.24 mm**
|
||||
> (600 mil: breadboard compatibility requires N×2.54; the KiCad-forum production footprint;
|
||||
> Leo's own outer-faces measurement 15.87 − 0.64 = 15.23). Dissent recorded, decision accepted —
|
||||
> under either value the module mates (≤0.1 mm/row elastic offset worst-case). **Mandatory
|
||||
> assembly mitigation for all units: solder the J1/J2 (and MOD1) female strips with the module
|
||||
> plugged in as an alignment jig** — hole clearance absorbs the delta, zero preload either way.
|
||||
>
|
||||
> **H5 — NEW HIGH (2026-07-07, caught by the M4 gate / CAD engineer): Blackpill socket rows one
|
||||
> grid step too wide.** Board J1↔J2 = 17.78 mm (0.7"); the physical WeAct Blackpill measures
|
||||
> **15.24 mm (600 mil)** row-to-row (calipers: 15.87 outer − 0.64 pin = 15.23 c-c; corroborated by
|
||||
> the KiCad forum footprint thread — "standard DIP", 600 mil — and a pixel re-measure of the WeAct
|
||||
> dimensions drawing: rows ≈15.5 mm, insets ≈2.7 mm on the 20.78 board). **Order RETRACTED until
|
||||
> fixed.** Fix: move **J2** from y=74.1031 → **y=71.5631** (Δ−2.54; the landing strip is
|
||||
> footprint- and via-free; moving J1 instead would collide with Q1/Q5), re-drag the 20 stubs,
|
||||
> refill, DRC 0, re-export, **then 1:1 paper test with the real module before ordering** (and same
|
||||
> paper test for MOD1 + J7/J8 + the physical modem while at it). Pin functions are unaffected —
|
||||
> layout-only fix; schematic untouched.
|
||||
>
|
||||
> **H6 — NEW HIGH (2026-07-07, surfaced by Leo's "silk on the back" question): MOD1 footprint is
|
||||
> FLIPPED to B.Cu** — has been since the Jul-3/4 re-placement (preCarrier backup shows F.Cu; preusb
|
||||
> onward B.Cu). The UNO socket pattern is x-mirrored: a shield plugged from the top cannot mate.
|
||||
> The back-side silkscreen was the symptom, not the problem. Only MOD1 is flipped (full 132-footprint
|
||||
> side sweep clean). Fix: select MOD1 → **F** (flip to F.Cu — silk returns to front automatically) →
|
||||
> reposition the envelope back over the shield field (check P3-notch + J8 clearance per the Jul-2
|
||||
> review notes) → reroute the 6 attached pads (2=3V3, 5=SW_5V @1.0 mm, 15/16=GNSS_RX/TX,
|
||||
> 17=DOUT_4G, 23=USART1_RX) → refill → DRC → re-STEP for the CAD engineer. Audit lesson recorded:
|
||||
> **footprint SIDE is now a standing checklist item; the 1:1 paper test with real modules is
|
||||
> mandatory before any socketed-carrier order.**
|
||||
>
|
||||
> **H6 RESOLVED 2026-07-07 — NOT A DEFECT (auditor false alarm). DO NOT FLIP MOD1.** The pad
|
||||
> pattern was physically validated by Leo: 1:1 print + the real simpleRTK2B Pro in the intended
|
||||
> orientation (antenna top-left, USB-C pair at bottom), including the discriminating count check —
|
||||
> 18-pin digital edge on the 18-hole row (y≈81) with the 10-pin subgroup at the correct (right)
|
||||
> end, 14-pin edge on the 14-hole row. Pads are correct as-placed; the B.Cu hosting is a
|
||||
> modeling/visual quirk only (outline on B.Fab, 3D model rendered underneath). Action taken:
|
||||
> move MOD1's B.Fab/B.CrtYd (and any B.Silk) graphics to the front layers — pads untouched.
|
||||
> **Auditor lesson:** the hand-derived back-side mirror transform produced a false positive;
|
||||
> for back-footprint geometry trust KiCad-computed outputs (IPC-D-356, gerbers, 3D/STEP) and the
|
||||
> physical 1:1 test — never a manually reconstructed flip. **Pro revisions differ:** older Pros have an "XBEE TO GPS UART" switch —
|
||||
> park it on UART1; newer Pros (like the verified sample) have NO switch — the XBee socket is
|
||||
> hardwired to UART2 (silk: "XBEE-TX/GPS RX2", "XBEE-RX/GPS TX2" on the rail pins). Assembly
|
||||
> checklist, both revisions: Pro's XBee socket stays EMPTY; if a switch exists → UART1; **never
|
||||
> plug the Pro's "USB XBee" port while the unit is in service** (its FTDI sits across UART2 on the
|
||||
> new rev = directly on the RTCM3 downlink). Only M4 (HBOX fit) gates the PO.
|
||||
|
||||
# EINARBoard HBOX 1.0 (`hbox-watchdog-v3-PROD`) — Pre-Fab Audit
|
||||
|
||||
**Scope:** read-only audit of the KiCad project as of 2026-07-06 17:14 (PCB saved 17:06, fab
|
||||
package `production/` regenerated 17:14). Board = 135 × 100 mm, 4-layer (F / In1-GND / In2 / B),
|
||||
132 components, 1317 tracks, 130 nets. Sources checked against: LTC4040 datasheet (ADI 4040fb),
|
||||
Omron G5V-1 datasheet, ArduSimple 4G NTRIP Master pinout diagram (footprint-R00), AOS AO4406A
|
||||
pinout, today's IPC-D-356 netlist, ERC/DRC runs, and the canonical repo docs.
|
||||
|
||||
**Verdict: the architecture is right and most of the execution is verified-correct, but there
|
||||
are 4 HIGH findings — 3 of them silent board-killers — that must be fixed before ordering.
|
||||
Estimated fix effort: one KiCad session (~2–4 h) + re-export.**
|
||||
|
||||
---
|
||||
|
||||
## 1. HIGH — fix before ordering
|
||||
|
||||
### H1. Q7 (LTC4040 input pass-FET) is installed backwards
|
||||
- **Board:** Q7 AO4406A source (pads 1–3) = `MAIN5V` (VSYS), drain (pads 5–8) = `VIN_SW` (CLN).
|
||||
- **Datasheet:** the mandatory input FET disconnects the system from the input during backup;
|
||||
with the OVP FET the pair is common-source at VIN (OVSNS text: the OVP FET's *drain* faces the
|
||||
input connector). Correct single-FET orientation = **source on the VIN/CLN side, drain on VSYS**
|
||||
(body diode conducts VIN→VSYS at startup, blocks VSYS→VIN in backup).
|
||||
- **As built:** body diode points MAIN5V→VIN_SW, so during a power-fail event the boost output
|
||||
back-feeds the dead vehicle rail through the body diode — the input can never be disconnected;
|
||||
backup collapses / pack drains into the SD-15B-05 output. **It will look fine on the bench in
|
||||
normal mode and fail exactly at the moment the battery backup is needed.**
|
||||
- **Fix:** swap S↔D on Q7 in the schematic (MAIN5V ↔ VIN_SW), resync, reroute locally.
|
||||
|
||||
### H2. Q5 / Q6 (MMBT3904) have collector/emitter swapped — BCE symbol on a BEC part
|
||||
- Symbols are `Device:Q_NPN_BCE` (1=B, 2=C, 3=E) on generic `SOT-23` footprints, but the
|
||||
MMBT3904 SOT-23 is physically **1=B, 2=E, 3=C**.
|
||||
- Result: Q5 (RTK load-switch driver, pad2=`GATE_LS`) and Q6 (K1 relay driver, pad2=`KCOIL`)
|
||||
operate in reverse-active mode (β ≈ 2–10). Q6 likely **cannot sink the 30 mA coil current →
|
||||
K1 never energizes → Modo B injection dead** (corrections survive via the NC bypass — the
|
||||
fail-safe does its job). Q5 may *appear* to work (10 k pull-up is easy to overcome) but with
|
||||
degraded VOL, unit-to-unit variation, and ~5 V reverse E-B stress (BVebo = 6 V abs max).
|
||||
- **Fix:** replace both with `Q_NPN_BEC` (or re-map pins), resync, reroute. SOT-23 rework is trivial.
|
||||
|
||||
### H3. RIG (ignition sense) — value drift + dangling schematic pin (the 1 ERC error)
|
||||
- **Schematic:** Rig = **22 k** — correct for the *current* topology (DFR1015 was removed; GX16
|
||||
pin 3 raw 12–24 V now feeds Rig 22k / Rigb 10k divider + D5 BAT54 clamp to 3V3 + C9 → PA5).
|
||||
- **PCB + both fab BOMs:** Rig = **100 R** (stale, from the old DFR1015→5 V design). If assembled
|
||||
as 100 R: ≈ 200 mA through the BAT54 clamp at 24 V ignition (~0.5 W in an 0603) → **D5 and Rig
|
||||
burn, ignition sense dead**, possible PA5 damage.
|
||||
- **Schematic wire bug:** Rig pin 2 (36.83, 74.93) does not terminate on the IGN_SENSE wire —
|
||||
the wire passes *through* the pin (73.66→76.2) without a junction. This is the single ERC
|
||||
error and means sch↔PCB sync is no longer provable; any future netlist push would also break.
|
||||
- **Fix:** terminate the wire on the pin, set the PCB value to 22k, regenerate BOM/CPL.
|
||||
|
||||
### H4. `SW_IN` routed entirely at 0.2 mm — full RTK-stack current on an 8-mil trace
|
||||
- `SW_IN` (F1 PPTC → Q1 source) is **missing from the netclass patterns**, so the USB autoroute
|
||||
pass laid it at the Default 0.2 mm — 18 segments, ~46 mm total, carrying ~1.2 A continuous
|
||||
(F9P + modem via U7) and up to ~3 A+ during PPTC fault ride-through. Expect large temperature
|
||||
rise / voltage drop right at the stack's supply.
|
||||
- Also audit `VEH5V`: several long 0.2 mm runs on B.Cu (20–30 mm). The main P2→RS1→U6 path is
|
||||
1.5 mm, and the thin runs appear to be the JP1 / sense branches — verify none of them is the
|
||||
only path for load current.
|
||||
- **Fix:** add `SW_IN` (pattern) to PWR_HI, reroute F1→Q1 at ≥1.0–1.5 mm, re-check VEH5V.
|
||||
|
||||
---
|
||||
|
||||
## 2. MEDIUM — decide/verify before or at ordering
|
||||
|
||||
- **M1. Fab BOM/CPL are not assembly-ready.** No LCSC part numbers anywhere; DNP parts (P3,
|
||||
P13, H3) carry "(DNP)" only in the Value text — no DNP attribute — and appear in BOM *and*
|
||||
CPL; CPL also lists THT sockets and the MOD1 pseudo-footprint. Fine for a **bare-board**
|
||||
order; blocks a JLCPCB **PCBA** order. (QFN-24 + DFN + SOT-563 + USB-C by hand is doable but
|
||||
unpleasant — decide which path you're buying.)
|
||||
- **M2. Your own fab gate is unmet.** The declared gate was "bench-validate the LTC4040 backup +
|
||||
hot-cell charge cutoff on the proto first" — the proto validates the *concept* with TP4056,
|
||||
not the LTC4040 circuit. Bare boards are cheap, so ordering after H1 is a defensible risk —
|
||||
but make that call consciously; don't discover LTC4040 issues on 30 assembled units.
|
||||
- **M3. D0/D1 (and D2/D8) direction on a physical simpleRTK2B Pro is still unverified** (your
|
||||
own open item since Jul-2). Board: MOD1 D1=`GNSS_TX` (PA2 MCU-TX), D0=`GNSS_RX` (PA3). If the
|
||||
Pro's D0/D1 are the opposite direction, the GNSS UART is dead with no socket-level rework.
|
||||
10-minute check against the Pro before ordering.
|
||||
- **M4. Mechanical fit not evidenced.** Board grew 120×100 → **135×100** on Jul-3; the
|
||||
"placement review vs HBOX/cabinet mechanicals" gate has no record. Verify HBOX interior,
|
||||
standoff positions (MH1–4: 3.2 mm, 4–5 mm edge margins), connector clearances (P1 terminal
|
||||
block + USB-C + JST heights under the shield) before committing.
|
||||
- **M5. USB routing is functional-grade, not compliant-grade.** DP/DM routed as independent
|
||||
0.2 mm traces on *different* layers (DP: F/In1, DM: B/In1), no diff-pair spacing, several
|
||||
segments ON the In1 GND plane (local plane cuts), no ESD array on the port. For a service
|
||||
console at Full-Speed it will almost certainly work; don't expect more. Next spin: route as a
|
||||
pair on one layer, add USBLC6-type ESD.
|
||||
- **M6. Two fab packages in the tree** — `fab-jlc-usb/` (14:41) is superseded by `production/`
|
||||
(17:14, includes the title-block change). Delete or archive the stale one so nobody orders it.
|
||||
|
||||
---
|
||||
|
||||
## 3. LOW — cosmetic / next spin
|
||||
|
||||
- L1. ERC: 95 warnings — ~90 off-grid pin/wire (your own TODO), plus lib-cache drift
|
||||
(`Q_NPN_BCE` not found, `L` and `AO4406A` mismatch vs library). Fixing H2 touches these anyway.
|
||||
- L2. DRC: 0 errors, 9 warnings — all lib_footprint mismatches (local overrides) on JST/Phoenix/
|
||||
SHT4x/MountingHole. Benign. Note `missing_courtyard` and `track_not_centered_on_via` are
|
||||
globally ignored (deliberate for the carrier); re-enable once per release to catch strays.
|
||||
- L3. Title block: rev "1.0", date **2026-06-23** — stale (board materially changed since);
|
||||
bump date/rev on release.
|
||||
- L4. MODEM LED: XBee pin 13 is a **1.8 V** output → Rmled 1 k + LED will be dim or dark.
|
||||
Accept, reduce R, or buffer next spin.
|
||||
- L5. L7 (3V3_MODEM buck) is 10 µH **2 A** vs module spec "2 A peak" — saturation margin is
|
||||
thin at worst case; consider a 3 A-rated part next spin. (Delta doc says "1 A/1.5 A pk" —
|
||||
understates the ArduSimple diagram's 2 A.)
|
||||
- L6. `SW3_NODE`/`SW_FAN` netclass patterns exist but `SW_IN` was the one that mattered (H4) —
|
||||
tidy the pattern list while in there.
|
||||
- L7. In2.Cu carries 336 signal segments and no pour — fine (In1 is solid GND), just wasted
|
||||
copper; consider a 5 V pour next spin.
|
||||
|
||||
---
|
||||
|
||||
## 4. Verified GOOD (evidence-backed, not vibes)
|
||||
|
||||
**Fail-safe invariants (the ones that matter for ICAVE):**
|
||||
- Load switch defaults OFF: R2 10 k pulls Q1 gate to rail; only PA1-driven Q5 can enable →
|
||||
invariant #1 holds with MCU absent. *(Weakened only by H2 — fix restores full margin.)*
|
||||
- **K1 Modo-B bypass verified against the Omron datasheet:** coil = pins 2/9 (SYS5V/Q6),
|
||||
COM = 5&6 (internally doubled — the double DIN_4G net is correct, not a short), **NC = 1 =
|
||||
F9P TX2** → corrections flow modem-ward with the MCU dead/absent; NO = 10 = PA9. Exactly the
|
||||
design intent of Block H. RTCM3 downlink (modem DOUT → MOD1 D2 → F9P RX2) is hardwired and
|
||||
never touches the MCU — invariant #3 in copper.
|
||||
- Cutoff chain MAIN5V→P11→SYS5V kills MCU *and* stack *and* modem (Blackpill 5V is on SYS5V) ✓.
|
||||
|
||||
**LTC4040 block (pin-by-pin vs datasheet):** VSYS 1/24 ✓ · PROG 825 Ω → 2.42 A ≈ C/5 ✓ ·
|
||||
CLN/RS1 8 mΩ → 3.1 A input limit ✓ · CHGOFF w/ 100 k pull-down = charge-enabled-if-MCU-absent,
|
||||
NTC still guards ✓ · BSTOFF grounded (backup enabled) ✓ · shared RSTFB/BSTFB divider 499k/100k →
|
||||
4.79 V backup (sharing explicitly datasheet-endorsed) ✓ · PFI 287k/100k → 4.60 V trip ✓ ·
|
||||
**F-straps decode to F2=1, F1=0, F0=1 = Li-Ion 4.00 V exactly as designed** (JP2/3/4 verified at
|
||||
net level; LiFePO4 = re-strap JP2 preserved) ✓ · NTC bias R55 from VIN + pack NTC on P4.3 —
|
||||
charger owns the battery NTC, per doctrine ✓ · OVSNS shorted to VIN (OVP unused, per datasheet) ✓ ·
|
||||
SW→L6 2.2 µH→BAT within spec ✓ · VSYS bulk 104 µF ≥ 100 µF ✓ · RST unused = legal ✓.
|
||||
|
||||
**XBee socket (verified against ArduSimple footprint-R00 diagram, incl. the mirrored row):**
|
||||
J7: VCC=3V3_MODEM (3.3–3.6 V ✓) · TX/RX=DOUT/DIN_4G ✓ · pin4+10 GND ✓ · RESET→SW4G (open =
|
||||
always-on) ✓ · **pin6 V_USB / 7 USB+ / 8 USB− = USB-C VBUS/DP/DM ✓** (USB enabled only when a
|
||||
laptop provides 5 V — elegant). J8 (pads mirror to XBee pins 20…11): ON/OFF→GND (always on) ✓ ·
|
||||
pin15/11 GND ✓ · **pin14 VREF→VCC ✓** · pin13 LED→Rmled/D4G ✓ (see L4) · CTS/RTS/DTR open ✓.
|
||||
Row spacing 22.35 mm = XBee spec ✓.
|
||||
|
||||
**Rest of the board:** USB-C UFP correct (5.1 k Rd on CC1/CC2; VBUS only to the modem — no rail
|
||||
backfeed) ✓ · Blackpill J1/J2: all 18 used pins match the documented map (A0 BAT_SENSE … B12–15
|
||||
gauge, A9/A10 USART1, B6/B7 I²C, NRST w/ SW1+P10) ✓ · 24 V front-end: SMBJ33A TVS, IRFR5305
|
||||
reverse-polarity FET correctly oriented w/ 15 V gate zener ✓ · fan: dedicated buck, UVLO enable
|
||||
divider, flyback SS34 across the fan, tach DNP provision ✓ · both MP1584 bucks correct (U7 FB
|
||||
100k/31.6k → 3.33 V) ✓ · sensors: SHT40 + TMP102 (ADD0→GND = 0x48) + 4.7 k pulls + external I²C
|
||||
probe port P5 ✓ · NTC-hotspot chain to PB1 ✓ · sense dividers ÷2 + filters ✓ · single GND net,
|
||||
solid In1 plane, closed 135×100 outline, 4×M3.2 holes with sane margins ✓ · netclasses PWR_HI
|
||||
1.5 mm / PWR_MID 1.0 mm exceed the kicad-guide minimums ✓ · DRC 0 errors / 0 unconnected;
|
||||
gerber+drill 4-layer set regenerated *after* the last save (17:14 > 17:06) ✓.
|
||||
|
||||
---
|
||||
|
||||
## 5. Documentation audit ("I need this to be perfect")
|
||||
|
||||
The doc *system* is genuinely good — canonical state doc + delta build-spec + rationale +
|
||||
ledgers is more than most teams have. But it is now **3–4 days behind the copper**:
|
||||
|
||||
| Doc claim | Board reality |
|
||||
|---|---|
|
||||
| DFR1015 on-board conditions ignition; Rig = 100 Ω series from 5 V | **DFR1015 removed**; raw 12–24 V → 22k/10k divider + clamp (and PCB value still says 100R — H3) |
|
||||
| Board 120 × 100 mm | **135 × 100 mm** (since Jul-3) |
|
||||
| "J8 all NC"; XBee = VCC/DOUT/DIN/GND only | J7.5–8 = RESET/V_USB/USB±; J8 = ON/OFF, VREF, LED, GNDs |
|
||||
| No USB-C block in design-state/delta | USB-C J9 + CC + VBUS-to-modem is a major new block (Jul-5) |
|
||||
| Delta: modem 1 A/1.5 A pk | ArduSimple diagram: **2 A peak** |
|
||||
| Name `hbox-watchdog-v3-PROD` everywhere | Title block says **EINARBoard HBOX 1.0** — name exists nowhere in the repo docs |
|
||||
| Title block date 2026-06-23, rev 1.0 | Board changed materially Jul 2–6 |
|
||||
|
||||
**Docs action list:** (1) update design-state §1/§3 (ignition topology, no DFR1015, 22k) ·
|
||||
(2) delta doc: add USB block + real J7/J8 table **including the J8 pad-k = XBee pin-(21−k)
|
||||
mirror** (that mapping is currently only in the copper — the single most dangerous piece of
|
||||
undocumented tribal knowledge on this board) · (3) board size + name: register "EINARBoard
|
||||
HBOX 1.0" in CLAUDE.md directory table + design-state header · (4) BOM: add LCSC column, DNP
|
||||
attributes on P3/P13/H3 · (5) delete/archive `fab-jlc-usb/` · (6) bump title-block date/rev on
|
||||
release · (7) log H1–H4 fixes in the delta doc changelog.
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommended order of operations
|
||||
|
||||
1. Fix H3 schematic wire + value → H2 symbols → H1 Q7 swap (all schematic) → resync to PCB.
|
||||
2. Add `SW_IN` to PWR_HI, reroute F1→Q1 + the three FET neighborhoods (H4/H1/H2 rework zones).
|
||||
3. ERC (target: 0 errors) → DRC (0 errors) → regenerate `production/` → **order bare boards.**
|
||||
4. In parallel: M3 (D0/D1 on a physical Pro) + M4 (mechanical fit) — both are no-cost checks.
|
||||
5. Docs pass (§5) — 1 h, do it while the fab quote is pending.
|
||||
6. PCBA later only after M1 (LCSC/DNP) and ideally proto bench results (M2).
|
||||
|
||||
*Audit artifacts: ERC 2026-07-06 (1 err / 95 warn) · DRC 17:09 (0 err / 9 lib warn) ·
|
||||
IPC-D-356 `production/netlist.ipc` 17:14 · sources: ADI 4040fb, Omron K048-E1-07,
|
||||
ArduSimple 4G-NTRIP-Master-footprint-R00.*
|
||||
181
docs/historia/einarboard-11-brief.md
Normal file
181
docs/historia/einarboard-11-brief.md
Normal file
@ -0,0 +1,181 @@
|
||||
---
|
||||
title: EinarBoard 1.1 — Design Brief & Execution Prompt
|
||||
tags: [project-drogheda, hardware, einarboard, respin, ethernet, rs485, design-brief, prompt]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO + Claude (decisions locked 2026-07-24)
|
||||
date: 2026-07-24
|
||||
status: Active — THE prompt for the 1.1 design sessions. Read this first, then execute the phases in §9.
|
||||
authority: supplements [[watchdog-electrical-design-state]] (canonical) + [[watchdog-production-schematic-delta]]; where they conflict on 1.1 scope, THIS file wins.
|
||||
---
|
||||
|
||||
# EinarBoard 1.1 — Design Brief ("the prompt")
|
||||
|
||||
> **How to use this doc.** This is the self-contained instruction set for turning
|
||||
> `hbox-watchdog-v3-PROD` (EinarBoard 1.0, as-fabbed, errata E-J1/E-CLR/E-PFI) into
|
||||
> **EinarBoard 1.1**. A design session (human or Claude) should be able to start from this
|
||||
> file alone. Standing rules: **file-level KiCad edits only** (the KiCad MCP backend
|
||||
> corrupted disk state on 2026-07-24 — see [[drogheda-respin-status]]); git checkpoint
|
||||
> after every phase; verify every structural edit with an independent parse; bench gates
|
||||
> unchanged (nothing fabs without the LTC4040 backup + charge-cutoff validation).
|
||||
|
||||
## 0. Decisions locked 2026-07-24 (Leo)
|
||||
|
||||
| Topic | Decision |
|
||||
|---|---|
|
||||
| Customer external comms | **RS-485/Modbus RTU AND RS-232/TTL** — one SC16IS752 dual-UART bridge serves both |
|
||||
| Ethernet | **On-board W5500 + MagJack**, hardware TCP/IP, SPI2 |
|
||||
| External indicators | **1 RGB status LED** (color-coded states), expander-driven |
|
||||
| Board outline | **Grow as needed**; ASA case/plate reprinted around the new outline |
|
||||
|
||||
Bench context driving this: board 1.0 is alive on the bench via the external J1 inverter
|
||||
adapter; battery-gauge LEDs dark = **firmware** (v2.3 never drives PB12–15 — review F3),
|
||||
fan off on a cool bench = **correct** (curve starts 45–48 °C). Neither is a 1.0 hardware bug.
|
||||
|
||||
## 1. The keystone architectural move (read this before touching anything)
|
||||
|
||||
The F411 has no third USART, no CAN, no Ethernet MAC, and as-built the board has ~6 free
|
||||
GPIO. Everything new hangs off two buses we already have:
|
||||
|
||||
**Move the battery gauge (PB12–15) to an I²C GPIO expander → SPI2 is freed → W5500
|
||||
Ethernet rides SPI2 → customer serial rides the I²C bus via SC16IS752 → RGB LED rides the
|
||||
expander.** Net result: all features land AND the free-pin count *increases*.
|
||||
|
||||
Pin plan after 1.1:
|
||||
|
||||
| Pin | 1.0 use | 1.1 use |
|
||||
|---|---|---|
|
||||
| PB12 | BATLED1 | **W5500 SPI2 CS** |
|
||||
| PB13/14/15 | BATLED2–4 | **W5500 SPI2 SCK/MISO/MOSI** |
|
||||
| PA15 (free) | — | **W5500 RSTn** |
|
||||
| PB2 (free) | — | **W5500 INTn** (BOOT1: irrelevant with BOOT0=0; do not strap) |
|
||||
| PB8 (free) | — | **SC16IS752 IRQn** |
|
||||
| PA7, PB9, PB10, PC14, PC15 | free/DNP | still free (reserve; PB10 keeps DNP tach provision) |
|
||||
| BATLED1–4, RGB | PB12–15 / n/a | **PCF8574 P0–P3 (gauge), P4–P6 (RGB), P7 spare** |
|
||||
|
||||
I²C1 (PB6/PB7, 4.7 k pull-ups) roster: SHT40 **0x44**, TMP102 **0x48**, PCF8574 **0x20**,
|
||||
SC16IS752 **strap A1/A0 AWAY from 0x48 — e.g. 0x4C/0x4D** (its address range 0x48–0x57
|
||||
collides with the TMP102; this WILL bite if left at default). 4 devices @100 kHz is fine;
|
||||
drop pull-ups to 2.2 k only if rise-time says so on the bench.
|
||||
|
||||
## 2. Carry-over fixes from 1.0 (state as of git `19c15e2`)
|
||||
|
||||
- **E-J1 (socket mirrored)** — pads renumbered end-for-end at file level, verified twice
|
||||
(module landing map 20/20; backend pad read). Footprint id deliberately renamed
|
||||
`Drogheda:PinSocket_1x20_P2.54mm_Vertical_RevNum_EJ1` (library-update can't revert;
|
||||
"not found in library" warning is expected — save it into a project `.pretty` in Phase A).
|
||||
**Pending:** re-route the 11 ripped signal nets (channel between the socket rows);
|
||||
**regenerate the J1 silkscreen net-name labels** — the current text row describes the OLD
|
||||
mapping (this is why the fix "looks not flipped" in the editor — trust pad numbers, not silk).
|
||||
- **E-CLR (24 V clearance, 23-Jul dead unit)** — netclass `HV24` (0.5 mm) assigned to
|
||||
`/24V_IN, 24V, IGN_IN, Q10_GATE`. **Pending:** DRC → re-route the flagged 24 V spots,
|
||||
prefer outer layers for 24 V copper. **Decision needed:** pre-existing `PWR_HI/PWR_MID`
|
||||
netclass patterns lack the `/` prefix and likely never matched (5 V nets have been on
|
||||
Default rules all along — plausibly related to how 1.0 shipped with 0.2 mm gaps). Fix the
|
||||
patterns in Phase A and absorb the new DRC flags, or consciously leave them.
|
||||
- **E-PFI** — R51 287k→270k DONE (trip 4.40 V falling; bench-proven need). Source 270k 1% 0603.
|
||||
- **Pending small:** 220 µF/10 V elco on VEH5V near P2/RS (modem-inrush bulk);
|
||||
silkscreen sweep (USB→ arrow + pin-1 dots on J1/J2, P11 `1=MAIN5V IN / 2=SYS5V OUT`,
|
||||
P2 `5V VEH IN`, J1 label row regenerated); P11 ships with jumper plug.
|
||||
- **Field/bench rules (no board change):** bench modem work at 5.2–5.4 V in or with pack
|
||||
connected; install SOP trims SD-15B-05 to ~5.2 V.
|
||||
|
||||
## 3. NEW Block M — customer serial (SC16IS752 + RS-485 + RS-232)
|
||||
|
||||
- **U10 SC16IS752** (I²C mode, addr per §1; XTAL 1.8432 MHz + 2×22 pF → standard bauds to
|
||||
115200; IRQn → PB8, 10 k pull-up).
|
||||
- **Channel A → RS-485 half-duplex:** SP3485/MAX3485-class transceiver, **auto-direction via
|
||||
the SC16IS752's RS-485 RTS mode** (no GPIO). A/B/GND on a 3-pos Phoenix; 120 Ω termination
|
||||
behind a jumper (DNP default); optional bias resistors (DNP). Add an **isolated-transceiver
|
||||
DNP alternative** (ISO3082-class footprint overlay) — decide once the customer says how far
|
||||
off-cabinet the bus runs.
|
||||
- **Channel B → RS-232:** MAX3232 + caps, TX/RX/GND on a 3-pos Phoenix; a 4-pin 2.54 mm TTL
|
||||
header (3V3 levels) in parallel behind solder jumpers — covers the "plain TTL UART" ask.
|
||||
- Firmware (v3.1): SC16IS752 driver + **Modbus RTU slave** exposing the telemetry registers
|
||||
(state, link, fix, sats, vbat, v5v, temps, charge status, fault flags, uptime, fw version —
|
||||
formal register map to be agreed with the customer, draft it in Phase A notes) + optional
|
||||
NMEA-out mode on either channel.
|
||||
|
||||
## 4. NEW Block N — Ethernet (W5500 + MagJack)
|
||||
|
||||
- **U11 W5500** (QFN48): SPI2 @ ≤25 MHz (PB13 SCK / PB14 MISO / PB15 MOSI / PB12 CS),
|
||||
RSTn → PA15, INTn → PB2. 25 MHz crystal + load caps, per-datasheet decoupling, 12.4 k RSET.
|
||||
- **J10 MagJack** HR911105A-class (integrated magnetics + LEDs) on a case-reachable edge.
|
||||
**Layout rules:** no copper pour under the magnetics area; keep the TX/RX pairs short,
|
||||
matched-ish, away from the switchers; W5500 + jack in their own corner, far from P3/GNSS RF
|
||||
and the SW nodes (existing §8 RF rule stands).
|
||||
- **Power: NEW 3V3_ETH rail** — reuse the proven Block-K MP1584 recipe (U12, FB 100k/31.6k),
|
||||
fed from **SYS5V** — explicitly NOT `3V3_MODEM` (Ethernet must survive stack power-cycles:
|
||||
it is the watchdog's reporting path during recovery) and NOT the Blackpill LDO (W5500
|
||||
~132 mA would cook the module regulator's budget). Budget 500 mA.
|
||||
- Firmware (v3.2): W5500 socket driver (8 hw sockets); transports in priority order —
|
||||
(1) $PHLG/telemetry + STREAM mirror to the htracker backend over TCP, (2) tracker-feed
|
||||
mirror using the existing Modo-B PA10 tap, (3) **NTRIP-over-Ethernet client** (fixed
|
||||
LAN-connected sites run RTK with NO 4G — a real product unlock for warehouse/port installs),
|
||||
(4) Modbus TCP for the customer if they land on Ethernet later.
|
||||
- Power/current sanity: +132 mA (W5500) +5 mA (M/O blocks) on 5 V side ≈ +0.1 A at SYS5V —
|
||||
SD-15B-05 (3 A) and LTC4040 SYSLIM (3.1 A) both have headroom; battery backup runtime with
|
||||
Ethernet up ≈ 12 Ah / ~0.5 A ≈ 24 h — still ample.
|
||||
|
||||
## 5. NEW Block O — expander + indicators
|
||||
|
||||
- **U13 PCF8574 @0x20** on I²C1. LEDs wired **common-anode to 3V3, expander SINKS through
|
||||
resistors** (PCF8574 sinks 25 mA/pin, sources almost nothing — active-LOW outputs).
|
||||
- P0–P3 → BATLED1–4 (existing on-board gauge LEDs, rewired from PB12–15).
|
||||
- P4–P6 → **RGB status LED** on a 4-pos JST (**P14**, replaces/absorbs single-LED P8 — decide
|
||||
in Phase A whether P8 stays for backward compatibility): common-anode RGB, one enclosure
|
||||
hole. State map (v3.1 firmware): GREEN solid RTK-OK · GREEN blink searching/SPS ·
|
||||
YELLOW solid low-battery · YELLOW blink degraded/limp · RED solid fault
|
||||
(CHRG_FAULT/thermal/fan) · RED blink recovery cycling · BLUE boot. Exact table goes in the
|
||||
operator manual (es-MX) when firmware lands.
|
||||
- P7 spare output (future buzzer/4th LED).
|
||||
- Existing PB0 EXT_LED path stays as the hardwired fallback indicator (works even if I²C dies).
|
||||
- Firmware: gauge refresh ≤1 Hz over I²C is trivial; **power-on self-test walks gauge + RGB +
|
||||
spins the fan 2 s** (assembly-line visual check; kills the "is the fan dead?" ambiguity).
|
||||
|
||||
## 6. Depopulation / cleanup (1.1)
|
||||
|
||||
REMOVE outright: P3, P13, H3 (harness alternatives obsoleted by the carrier + Modo-B),
|
||||
stale J1 label row. KEEP: J7 USB path (modem config access), PB10 tach DNP provision,
|
||||
P10 external reset, P11 (with jumper plug), P12 NTC. DECIDE in Phase A: H1 SWD DNP 4-pad
|
||||
(add it this time — it costs nothing and 1.0 bring-up would have used it), P8 vs P14.
|
||||
|
||||
## 7. Sourcing adds (LCSC pass in Phase E)
|
||||
|
||||
W5500 · HR911105A MagJack · 25 MHz + 1.8432 MHz crystals · SC16IS752IPW · SP3485EN ·
|
||||
MAX3232 · PCF8574T · RGB LED (plus JST 4-pos) · MP1584EN #3 + FB/L/C set (Block-K clone) ·
|
||||
220 µF/10 V elco · 270k 1% 0603 (E-PFI) · isolated-RS485 DNP footprint part TBD.
|
||||
Availability check is part of Phase E, not assumed.
|
||||
|
||||
## 8. Risks / open items
|
||||
|
||||
- SC16IS752 vs TMP102 **address collision if left at default** (§1) — strap check is a
|
||||
Phase-D ERC-notes item.
|
||||
- Magjack footprint + keepout correctness (first Ethernet on a Helgen board) — copy a proven
|
||||
reference layout; DRC can't catch magnetics mistakes.
|
||||
- Modbus register map needs customer sign-off before v3.1 firmware freezes.
|
||||
- Flash/RAM: F411 (512 K/128 K) comfortably fits W5500 driver + Modbus + existing v2.3;
|
||||
revisit only if an RTOS enters the picture.
|
||||
- Case redesign (RJ45 + RGB + serial glands) rides the outline growth — coordinate with the
|
||||
ASA reprint; connector edge placement must be agreed BEFORE routing (Phase B gate).
|
||||
- Existing 1.0 boards: bench/pilot use via Leo's J1 inverter adapter (consider panelizing it
|
||||
as a rework PCB); 1.1 is the production article.
|
||||
|
||||
## 9. Execution phases (each ends with git commit + parse-verify + snapshot)
|
||||
|
||||
- **A. Schematic** — blocks M/N/O; gauge nets moved to expander; bulk cap; depop §6;
|
||||
PWR_* pattern decision; save the RevNum_EJ1 footprint into a project `.pretty`;
|
||||
ERC = 0. *(file-level edits + `kicad-cli sch erc` if available, else parse checks)*
|
||||
- **B. Outline & placement** — grow the board (target set by connector-edge plan + case),
|
||||
move/place new blocks (Ethernet corner rule §4), mounting holes, keepouts. **Gate: Leo
|
||||
approves the connector-edge plan before routing.**
|
||||
- **C. Routing** — J1 11-net re-route (inter-socket channel) · 24 V re-clear per HV24 ·
|
||||
new blocks · zones refill.
|
||||
- **D. Verify** — DRC/ERC clean (courtyard-cosmetics excepted), silkscreen sweep done,
|
||||
strap/address notes, visual SVG/PDF review pack for Leo.
|
||||
- **E. Outputs & sync** — gerbers/drill/pos/BOM, LCSC pass, delta-doc §-updates, PINOUT.md
|
||||
rewrite, errata page (E-J1/E-CLR/E-PFI closed in 1.1), firmware v3.0/3.1/3.2 tickets into
|
||||
the Notion tracker.
|
||||
|
||||
*Firmware reminder (independent of the board): v3.0 scope in
|
||||
[[drogheda-firmware-v23-review]] §4 is unchanged and still blocks fleet flashing — the
|
||||
rollover fix and fan-tach fix don't wait for 1.1 silicon.*
|
||||
60
docs/historia/einarboard-30-capacitores.md
Normal file
60
docs/historia/einarboard-30-capacitores.md
Normal file
@ -0,0 +1,60 @@
|
||||
# EinarBoard 3.0 — Capacitor Placement Map (v1.0, 2026-08-14)
|
||||
|
||||
> Generated from the v5 netlist + v4 ownership. Rule: **every 100 nF lives ≤2 mm from the pin
|
||||
> it serves, same side of the board.** Crystal caps ≤2 mm of crystal AND chip. Bulk (≥1 µF)
|
||||
> gets 5–10 mm of slack. Place each cluster as a unit around its owner, then align/distribute.
|
||||
|
||||
## U16 — STM32F411 (the big cluster)
|
||||
| Cap | Value | Where |
|
||||
|---|---|---|
|
||||
| C105, C106, C107, C108 | 100 nF | one per **VDD pin** — four corners of the chip, ≤2 mm |
|
||||
| C111 | 4.7 µF | **VCAP1 (pin 30)** — ≤2 mm, short fat trace. Not decoupling; mandatory core cap |
|
||||
| C109 + C112 | 100 nF + 1 µF | **VDDA pin** — ≤3 mm, next to R104 (10R) |
|
||||
| C104 | 100 nF | **NRST pin** — ≤3 mm |
|
||||
| C110 | 4.7 µF | bulk — anywhere ≤5 mm from the chip |
|
||||
|
||||
## Y3 / Y4 — MCU crystals (tight sub-clusters beside U16)
|
||||
- **Y3 (25 MHz) + C113 + C114 (15 pF)** — crystal ≤5 mm from PH0/PH1 pins, caps ≤2 mm of the crystal, caps' GND short.
|
||||
- **Y4 (32.768 kHz) + C115 + C116 (12 pF)** — same treatment at PC14/PC15.
|
||||
|
||||
## U17 — AMS1117 LDO
|
||||
- **C117 (10 µF)** at the input pin, **C118 (22 µF) at the output — stability cap, the LDO oscillates without it.** Both ≤3 mm. D12/D13 (OR diodes) complete this cluster on the input side.
|
||||
|
||||
## U11 — W5500 (second-biggest cluster)
|
||||
| Cap | Value | Where |
|
||||
|---|---|---|
|
||||
| C92, C93, C98 | 100 nF | one per **VDD pin**, ≤2 mm (C98 belongs here — it only *sat* near Y1 in v4) |
|
||||
| C91 | 10 nF | **1V2O pin** — ≤2 mm |
|
||||
| C90 | 4.7 µF | **TOCAP pin** — ≤2 mm |
|
||||
| C95, C96 | 18 pF | **Y1 crystal caps** — with Y1, ≤2 mm |
|
||||
| C94 + C102 | 10 µF + 22 µF | rail bulk — ≤10 mm |
|
||||
- **Y1 (25 MHz)** itself: ≤5 mm from the W5500's XI/XO pins — this is the crystal that must NOT live far from U11.
|
||||
- **C97 (6.8 nF)** — MagJack RCT termination: between U11 and J10, near the magnetics side.
|
||||
|
||||
## Y2 — SC16IS752 crystal (with U10)
|
||||
- **Y2 (1.8432 MHz) + C82 + C83 (22 pF)** — ≤5 mm from U10's XTAL pins, caps ≤2 mm of Y2.
|
||||
|
||||
## U10 / U14 / U15 — serial bridge cluster
|
||||
- **C81, C84, C89 (100 nF)** — one each hugging U10, U14, U15 power pins.
|
||||
- **C85, C86, C87, C88 (100 nF)** — the **MAX3232 charge-pump caps**: functional, not decoupling — all four ≤3 mm around U15 exactly as its datasheet drawing shows.
|
||||
- **C30 (10 µF)** — cluster bulk, ≤10 mm.
|
||||
|
||||
## Sensors & expander
|
||||
- **Cu3 (100 nF)** → U3 SHT40. **Cu4 (100 nF)** → U4 TMP102. Each at its chip.
|
||||
- **C80 (100 nF)** → U13 PCF8574.
|
||||
- **C34 (10 µF), C35/C40 (22 µF), C103 (100 µF), C43 (100 µF)** — area bulk (sensor/modem/expander rails) — flexible, ≤10 mm of their load clusters.
|
||||
|
||||
## Bucks (each is a self-contained island — keep each group TIGHT)
|
||||
- **U2 fan buck:** Cbst1 (BST pin), Ccomp1 (COMP pin), C22 (output bulk), + its L/diode. The switch-node loop must be small.
|
||||
- **U7 modem buck:** Cbst2, C41, + L/diode — same rule.
|
||||
- **U12 ETH buck (if it survives the L8 decision):** C100 (SW snubber/BST), C99 + Ccomp2 (COMP network), C101 (output bulk), L8 — one tight island.
|
||||
|
||||
## ADC input filters (belong with their DIVIDERS, not the MCU)
|
||||
- **C5** → BAT_SENSE divider resistors · **C7** → 5V_SENSE divider · **C9** → IGN_SENSE (with Rig/D5 clamp) · **C24** → NTC divider (R42) — each cap sits with its resistor pair; the filtered node then runs to U16.
|
||||
|
||||
## Power entry / rail bulk (flexible placement, near the entry connectors)
|
||||
- **C10 (47 µF/50 V)** → 24 V input at Q10. **C3, C78 (220 µF!), C23, C31, C32, C33 (47 µF ×2)** → MAIN5V/VEH5V/SYS5V/LTC4040 rail bulk — near P2/P11/U6 power path; C78 is the modem-inrush bulk near P2/RS per the 1.1 fix.
|
||||
- **C11 (100 nF)** → K1 relay coil area.
|
||||
|
||||
## USB ports
|
||||
- J11 (DFU): R100/R101 + D14–D16 TVS right at the connector. J9 (modem): Rcc1/Rcc2 likewise.
|
||||
188
docs/historia/einarboard-30-mcu.md
Normal file
188
docs/historia/einarboard-30-mcu.md
Normal file
@ -0,0 +1,188 @@
|
||||
---
|
||||
title: EinarBoard 3.0 — On-Board MCU Integration — Design Brief & Execution Prompt
|
||||
tags: [project-drogheda, hardware, einarboard, mcu-integration, stm32f411, dfu, design-brief, prompt]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO + Claude (decisions locked 2026-08-12)
|
||||
date: 2026-08-12
|
||||
status: Active — THE prompt for the 3.0 design sessions. Read this first, then execute the phases in §10.
|
||||
authority: supplements [[watchdog-electrical-design-state]] (canonical) + the 1.1 brief (as executed in v4 = EinarBoard 2.0); where they conflict on 3.0 scope, THIS file wins.
|
||||
---
|
||||
|
||||
# EinarBoard 3.0 — Design Brief ("the prompt")
|
||||
|
||||
> **How to use this doc.** Self-contained instruction set for turning `hbox-watchdog-v4`
|
||||
> (EinarBoard 2.0, as sent to fab ~2026-08-01, git `b505cb3`) into **EinarBoard 3.0**
|
||||
> (`kicad/hbox-watchdog-v5`). Standing rules unchanged from the 1.1 brief: **file-level
|
||||
> KiCad edits only** (KiCad MCP backend clobbered disk state 2026-07-24 — read-only MCP
|
||||
> queries are fine); git checkpoint after every phase; verify every structural edit with an
|
||||
> independent parse; bench gates unchanged.
|
||||
|
||||
## 0. Decisions locked 2026-08-12 (Leo)
|
||||
|
||||
| Topic | Decision |
|
||||
|---|---|
|
||||
| MCU | **On-board STM32F411RET6 (LQFP64)** replaces the Blackpill module + J1/J2 sockets |
|
||||
| Flash path | **USB DFU on a board-mounted USB-C** (BOOT0 + NRST buttons on-board, Blackpill parity) + SWD recovery pads |
|
||||
| Base | `hbox-watchdog-v4` (EinarBoard 2.0: W5500 Ethernet, SC16IS752 serial, PCF8574 expander — all kept) |
|
||||
| Modem / GNSS | **Stay modular** — XBee socket + simpleRTK2B Pro shield (stock already purchased) |
|
||||
| Fixation | Shield **bolted through its UNO-pattern holes** into matching EinarBoard holes (M2.5); **2× M2.5 holes near the XBee** for a heatsink that also clamps the modem |
|
||||
| Fan | **2-pin fan only** — 4-pin header + tach conditioning removed, PB10 freed |
|
||||
| Naming | Product **EinarBoard 3.0** · KiCad project **`hbox-watchdog-v5`** |
|
||||
|
||||
## 1. Why the F411RET6 (and not something else)
|
||||
|
||||
Same silicon family as the Blackpill's F411CEU6 → **firmware v2.3/v3.x runs unchanged**.
|
||||
Only the build target moves: Arduino board = *Generic F411RE* (or a copied Blackpill
|
||||
variant) with `HSE_VALUE = 25 MHz` — a build-config change, not a port.
|
||||
|
||||
| | F411CEU6 (Blackpill chip) | **F411RET6 (chosen)** | G431CBT6 (rejected for now) |
|
||||
|---|---|---|---|
|
||||
| Package | UFQFPN48 (hot-air only) | **LQFP64 — hand-solder/rework OK** | LQFP48 |
|
||||
| JLC price/stock (2026-08-12) | $3.31 · 1,333 (C2054095) | **$2.47 · 5,342 (C94355)** | $2.96 · 13,744 (C529355) |
|
||||
| Extra pins | — | **PC0–PC12, PD2** (+ USART6 on PC6/PC7 = real 3rd UART; more ADC) | FDCAN (future CAN), crystal-less USB |
|
||||
| Firmware | zero change | build-target change only | **full port — wrong timing mid-fleet** |
|
||||
|
||||
The 1.1 brief's "F411 has no third USART" was a **48-pin-package** limitation (USART6 lives
|
||||
on PA11/PA12 there = USB). On LQFP64 it's on PC6/PC7 — reserve them. If vehicle CAN ever
|
||||
becomes a requirement: MCP2518FD on SPI, or a G4 port as its own project.
|
||||
Source **genuine ST via LCSC/JLC only** — the ROM bootloader behavior is the product feature.
|
||||
|
||||
## 2. ⚠ THE BOOT TRAP — read before touching pins
|
||||
|
||||
DFU entry on the F411 requires, at reset: **BOOT0 = 1 AND BOOT1 (PB2) = 0**
|
||||
(BOOT0=1 + PB2=1 boots SRAM = dead end). v4 has **`ETH_INT` (W5500 INTn) on PB2**, per the
|
||||
1.1 note "BOOT1: irrelevant with BOOT0=0; do not strap" — which was true only while the MCU
|
||||
was a removable module. W5500 INTn **idles HIGH** → with the MCU soldered down, DFU would
|
||||
be unreachable. Therefore in v5:
|
||||
|
||||
- **Move `ETH_INT` PB2 → PC0** (EXTI0 is free: PA0/PB0 don't use EXTI).
|
||||
- **Strap PB2 → 10 k → GND.** Nothing else on PB2, ever.
|
||||
- ROM bootloader also listens on **USART1 (PA9/PA10)** = the Modo-B pins. Flash with the
|
||||
RTK stack unpowered (current practice) so F9P TX2 traffic can't capture the bootloader;
|
||||
if field-reflash-with-stack-on is ever needed, add a series jumper on the PA10 tap then.
|
||||
|
||||
DFU bootloader clocking: the F411 has **no HSI48** — the ROM bootloader derives the 48 MHz
|
||||
USB clock from the **external HSE crystal (4–26 MHz, auto-detected)**. Our 25 MHz HSE is
|
||||
therefore **mandatory, not optional** (AN2606; same reason the Blackpill carries one).
|
||||
|
||||
## 3. NEW Block P — on-board MCU (Blackpill absorbed)
|
||||
|
||||
**U14 STM32F411RET6** (LQFP64, JLC C94355). Support circuitry, Blackpill-parity where noted:
|
||||
|
||||
- **HSE:** 25.000 MHz crystal (3225, CL 10–12 pF) + 2× load caps `C = 2(CL − C_stray)`
|
||||
(≈15 pF for CL=10 pF; finalize per chosen crystal). Short symmetric traces, GND guard.
|
||||
- **LSE:** 32.768 kHz (3215/2012, CL 12.5 pF, e.g. Q13FC-class) + caps per CL. Fit it
|
||||
(Blackpill parity; RTC available). Firmware note: F411 LSE wants low drive level.
|
||||
- **VCAP1:** 4.7 µF X5R ≥6.3 V at the pin (DS10314 — confirm value/pin in Phase A).
|
||||
- **Decoupling:** 100 nF at **every** VDD pin + 4.7 µF bulk; **VDDA:** ferrite bead (or
|
||||
10 Ω) from 3V3 + 1 µF + 100 nF (BAT_SENSE/VEH5V/NTC ADC quality — free upgrade vs the
|
||||
module's direct tie). **VBAT → 3V3** + 100 nF.
|
||||
- **BOOT0:** dedicated pin → **10 k pull-down** + tact switch to 3V3 (+ optional 2-pin jig
|
||||
pad). **PB2/BOOT1: 10 k pull-down (§2).**
|
||||
- **NRST:** joins the existing `NRST` net (SW1 on-board + P10 external — already designed)
|
||||
+ 100 nF at the pin.
|
||||
- **LEDs:** `LED_C13` **active-LOW** (3V3 → 1 k → LED → PC13, Blackpill polarity so v2.3+
|
||||
blink code is unchanged) + PWR LED on 3V3.
|
||||
- **USB-C (J11):** 16-pin USB 2.0 receptacle (TYPE-C-31-M-12 class) on a case-reachable
|
||||
edge. CC1/CC2 → 5.1 k to GND (UFP). D+ → PA12, D− → PA11 via **USBLC6-2SC6** ESD array.
|
||||
Shield → GND via RC (1 MΩ ∥ 4.7 nF) or direct — decide Phase A.
|
||||
- **SWD:** 4 pads/test-points (3V3, PA13 SWDIO, PA14 SWCLK, GND) — DNP header. With a
|
||||
soldered MCU this is the brick-recovery + factory path; the 1.1 brief already wanted it.
|
||||
- **PA0 note:** no KEY button (Blackpill's KEY is PA0 = our `BAT_SENSE`). Not a loss.
|
||||
|
||||
## 4. Power — who makes 3V3 now
|
||||
|
||||
The Blackpill's on-module LDO dies with the module, so:
|
||||
|
||||
- **U15 LDO 3V3:** AMS1117-3.3 (SOT-223, JLC Basic C6186) — or equal ≥500 mA. Budget:
|
||||
MCU ~50 mA + SHT40 + TMP102 + PCF8574 + SC16IS752 + MAX3232 + SP3485 TX bursts ≈
|
||||
150–250 mA worst → 1 A part with margin, ~0.3 W dissipation OK in SOT-223.
|
||||
- **Input OR:** `LDO_IN = MAIN5V (via D_or1 SS34) ∥ USB VBUS (via D_or2 SS34)`.
|
||||
**Bench flashing on USB alone powers the 3V3 domain only** (MCU, sensors, expander,
|
||||
serial bridge): W5500 (3V3_ETH from SYS5V) stays dark, K1 stays NC (corrections path
|
||||
untouched), no backfeed into the LTC4040/MAIN5V. This replaces the module's
|
||||
VBUS-to-5V tie with something that can't fight the backed-up rail.
|
||||
- **Phase-A sweep:** every net the module's 3V3 pin used to source (I²C pull-ups, sensor
|
||||
VDD, PCF8574, SC16IS752, IOREF row to MOD1, D5 clamp reference, NTC divider top…) moves
|
||||
to the new LDO output net — keep the net name `3V3`. Verify with a netlist diff.
|
||||
- 3V3_MODEM and 3V3_ETH bucks: **unchanged**.
|
||||
|
||||
## 5. Fan — 2-pin, done
|
||||
|
||||
- Fan connector → **2-pin** (`5V_FAN`, `FAN_RET`). Remove the 1×04 header, `FAN_TACH` /
|
||||
`FAN_TACH_RAW` conditioning (pull-up/divider). Driver chain (PA6 `FAN_PWM` → Q → low-side)
|
||||
and the U2 fan buck stay exactly as-is.
|
||||
- **PB10 freed** (tach DNP provision retired — fleet fans are 2-wire, decided 2026-06-24).
|
||||
- Firmware v3.0 already plans tach-less fan-fault inference (temp climbs while PWM high).
|
||||
|
||||
## 6. NEW Block Q — fixation (mechanical)
|
||||
|
||||
- **Shield bolts:** 4× holes in EinarBoard at **exactly MOD1's UNO-pattern hole coords**
|
||||
(read the NPTH positions out of the `Arduino_UNO_R3` footprint in the v5 PCB — do not
|
||||
re-derive from a drawing). Drill **Ø2.75 mm (M2.5)**; bolts pass shield → standoffs →
|
||||
EinarBoard (→ optionally the mounting plate). **Bench check before freezing drill:**
|
||||
caliper the real simpleRTK2B Pro holes — UNO-pattern boards are usually Ø3.2 (M3); if so
|
||||
either keep M2.5 hardware (washers, slop is fine) or open ours to Ø3.2 and use M3. Keepout
|
||||
annulus top/bottom, NPTH, no copper — and respect the HV24/inner-layer clearance rule.
|
||||
- **XBee heatsink:** **2× M2.5 holes near J7/J8** so a small heatsink/bracket bolts down and
|
||||
indirectly clamps the modem module. Position in Phase B with the case; keep clear of the
|
||||
RF side of the socket and the 24 V copper.
|
||||
- Case/plate: bolt pass-through and heatsink height ride the ASA reprint — coordinate
|
||||
connector-edge plan (USB-C now needs an opening too) **before routing** (Phase B gate,
|
||||
same as 1.1).
|
||||
|
||||
## 7. Pin map delta (v4 → v5)
|
||||
|
||||
| Pin | v4 (EinarBoard 2.0) | v5 (EinarBoard 3.0) |
|
||||
|---|---|---|
|
||||
| PB2 | `ETH_INT` (W5500 INTn) | **BOOT1 strap: 10 k → GND. Nothing else.** |
|
||||
| PC0 *(new pin)* | — (module had no PC0) | **`ETH_INT`** |
|
||||
| PB10 | tach DNP provision | **free** (fan is 2-pin) |
|
||||
| PA13/PA14 | not broken out (module USB only) | **SWD pads** (DNP header) |
|
||||
| PC6/PC7 *(new)* | — | **reserved: USART6** (future 3rd UART) |
|
||||
| PC1–PC5, PC8–PC12, PD2 *(new)* | — | free — do not spend without updating this table |
|
||||
| PA11/PA12 | module USB | **J11 USB-C** D−/D+ |
|
||||
| everything else | per [[watchdog-electrical-design-state]] §2 + 1.1 blocks M/N/O | **unchanged** |
|
||||
|
||||
## 8. Depopulation / cleanup (3.0)
|
||||
|
||||
REMOVE: J1/J2 2×20 sockets + Blackpill-L/R symbols and footprint (→ **errata E-J1 retired
|
||||
by construction** — no module, no mirroring, no "never power a seated module" hazard);
|
||||
1×04 fan header + tach parts; stale Blackpill silkscreen (module outline, header labels).
|
||||
KEEP: SW1 + P10 reset, P11 (jumper plug), P12 NTC, J7/J8 XBee, MOD1 shield sockets, J7 USB
|
||||
modem-config path. SILKSCREEN: USB-C "DFU: hold BOOT0" hint, SWD pad labels, hole callouts
|
||||
(M2.5), fan "2-pin 5V".
|
||||
|
||||
## 9. Risks / open items
|
||||
|
||||
- **PB2 strap is load-bearing** (§2) — make it a Phase-D ERC-notes item like the SC16IS
|
||||
address strap was in 1.1.
|
||||
- LQFP64 symbol/footprint: use KiCad lib `MCU_ST_STM32F4:STM32F411RETx` +
|
||||
`Package_QFP:LQFP-64_10x10mm_P0.5mm`; save copies into the project libs (same policy as
|
||||
the RevNum_EJ1 footprint).
|
||||
- Blackpill variant nuances: confirm `Generic F411RE` Arduino variant boots USB CDC as
|
||||
today's build does (bench item F-1 on first article; SWD pads are the net if it doesn't).
|
||||
- Shield hole diameter verify (§6). · LSE drive/caps verify on first article.
|
||||
- DFU with stack powered = USART1 capture risk (§2) — procedure note, not hardware.
|
||||
- v4 boards at fab: 3.0 starts now as design work; nothing in 3.0 blocks v4 bring-up, and
|
||||
v4 bench findings (modem rail, LTC4040 timer behavior) feed Phase D review.
|
||||
|
||||
## 10. Execution phases (each ends: git commit + parse-verify + snapshot)
|
||||
|
||||
- **A. Schematic** — copy v4 → `hbox-watchdog-v5` (done 2026-08-12 with this brief);
|
||||
Block P + LDO OR (§3/§4); ETH_INT move + PB2 strap (§2); 3V3 net sweep (§4); fan 2-pin
|
||||
(§5); depop §8; ERC = 0.
|
||||
- **B. Outline & placement** — reclaim the Blackpill socket area; USB-C edge + case plan;
|
||||
Block-Q holes (shield coords from MOD1 footprint; XBee heatsink pair); **Gate: Leo
|
||||
approves connector-edge + hole plan before routing.**
|
||||
- **C. Routing** — MCU fanout (crystal + USB first), re-route displaced nets, zones refill;
|
||||
HV24 rules stand.
|
||||
- **D. Verify** — ERC/DRC clean; strap/boot checklist (BOOT0 pull-down, PB2 low, HSE
|
||||
present = DFU truth table); netlist diff v4→v5 reviewed; SVG/PDF review pack for Leo.
|
||||
- **E. Outputs** — gerbers/BOM/CPL, LCSC pass (C94355 + new passives/USB-C/LDO), delta-doc
|
||||
§-update, PINOUT.md rewrite (RET6 + freed pins), errata page (E-J1 retired), firmware
|
||||
ticket: build target Generic F411RE + HSE flag + PC0 EXTI for ETH_INT.
|
||||
|
||||
*Fab gate unchanged: v5 doesn't go out before a v4-lineage bench validates the power path,
|
||||
and first-article v5 must prove **USB DFU enumeration + a full flash cycle** before any
|
||||
fleet quantity.*
|
||||
141
docs/historia/fuente-5v-costo.md
Normal file
141
docs/historia/fuente-5v-costo.md
Normal file
@ -0,0 +1,141 @@
|
||||
---
|
||||
tags: [project-drogheda, hardware, bom, cost, power]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO
|
||||
date: 2026-09-09
|
||||
status: Draft — first-pass estimate
|
||||
---
|
||||
|
||||
# 5 V source: on-board (V6 / U25) vs external Mean Well SD-15B-05 (V5)
|
||||
|
||||
!!! warning "Designators from before 22-Oct (T43)"
|
||||
This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the V5.6/V5.7 the LTC4040 is U14 (was U6); on the v6, U6 is the modem and the LTC4040 is U17. To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it.
|
||||
|
||||
> **Bottom line:** the on-board 24→5 V block already captured in V6 costs
|
||||
> **≈ US$2.2 / MX$37 per board** in parts. The external Mean Well it replaces costs
|
||||
> **≈ MX$330 per unit** all-in as currently purchased. **Net saving ≈ MX$290/unit**
|
||||
> (≈ MX$8,700 at 30 units, ≈ MX$29,000 at 100). The money is not the interesting part —
|
||||
> the isolation loss and the 3 A → 3.5 A headroom are.
|
||||
|
||||
Prices: LCSC/JLCPCB live database (2026-09-09) · FX **USD 1 = MXN 16.91**.
|
||||
|
||||
---
|
||||
|
||||
## 1. What each version actually does
|
||||
|
||||
| | **V5 (as built)** | **V6 (as captured in the schematic)** |
|
||||
|---|---|---|
|
||||
| 24→5 V | **External** Mean Well SD-15B-05 → `P2` (JST 2-pin) → `VEH5V` | **On-board** `U25` TPS54360B: `VIN_PROT` → `VEH5V` |
|
||||
| Rating | 5 V / **3 A** isolated (1.5 kVDC) | 5 V / **3.5 A**, non-isolated, 60 V input |
|
||||
| Entry chain | `Q10` (IRFR5305) + `D10` (SMBJ33A) → fan buck only | `F2 → Q23 → D32 (SMCJ33A) → C149/C150 → U25 → D34/L22 → C153/C154` |
|
||||
| Extra parts on board | `P2` connector | 15 fitted + 2 DNP (`R124`/`R125`) |
|
||||
|
||||
V6 keeps the fan buck (`U2`) and the 3V3_ETH buck (`U12`); `U7` was deleted. Neither
|
||||
ever made the main rail — this comparison is only about the `VEH5V` source.
|
||||
|
||||
---
|
||||
|
||||
## 2. On-board block — BOM cost per board
|
||||
|
||||
`U25` TPS54360B · `L22` SRP1265A-100M · `D34` B560C · `D32` SMCJ33A · `C149`/`C150`
|
||||
2.2 µF/100 V · `C151` 100 µF/50 V · `C152` 100 nF · `C153`/`C154` 47 µF/16 V ·
|
||||
`C155`/`C156` · `R120`–`R123` — plus the entry chain `F2` · `Q23` · `D33` · `R119`.
|
||||
|
||||
| Line | 30 boards | 100 boards | 500 boards |
|
||||
|---|---:|---:|---:|
|
||||
| `U25` TPS54360BDDAR (C524806) | $0.487 | $0.428 | $0.419 |
|
||||
| `L22` SRP1265A-100M 10 µH/10 A (C840531) | $0.639 | $0.554 | $0.504 |
|
||||
| `D34` B560C 5 A/60 V (C85100) | $0.220 | $0.179 | $0.158 |
|
||||
| `D32` SMCJ33A TVS (C699039) | $0.114 | $0.091 | $0.079 |
|
||||
| `C149`/`C150` 2.2 µF/100 V 1210 ×2 † | $0.180 | $0.180 | $0.160 |
|
||||
| `C151` 100 µF/50 V elco (C134502) | $0.149 | $0.149 | $0.149 |
|
||||
| `C153`/`C154` 47 µF/16 V 1210 ×2 (C5440143) | $0.185 | $0.185 | $0.127 |
|
||||
| `C152`, `C155`, `C156`, `R120`–`R123` | $0.022 | $0.022 | $0.022 |
|
||||
| **Buck subtotal** | **$1.996** | **$1.788** | **$1.618** |
|
||||
| `Q23` IRFR5305 (C2624) | $0.262 | $0.227 | $0.193 |
|
||||
| `F2` 2 A/1812 fuse †, `D33` BZX84C15, `R119` | $0.144 | $0.144 | $0.144 |
|
||||
| **Entry subtotal** | **$0.406** | **$0.371** | **$0.337** |
|
||||
| **Parts / board** | **$2.40** | **$2.16** | **$1.96** |
|
||||
| JLC extended-part setup (7 new uniques × $3, amortised) | $0.70 | $0.21 | $0.04 |
|
||||
| SMT placement (~50 joints) | $0.10 | $0.09 | $0.09 |
|
||||
| **All-in / board** | **$3.20 · MX$54** | **$2.46 · MX$42** | **$2.09 · MX$35** |
|
||||
|
||||
† `C149`/`C150` and `F2` have no LCSC part pinned yet — estimated at $0.09 and $0.12.
|
||||
Board outline stays 140×100, so **the block costs zero PCB area money**.
|
||||
|
||||
---
|
||||
|
||||
## 3. External Mean Well — cost per unit
|
||||
|
||||
| Item | MXN | Source |
|
||||
|---|---:|---|
|
||||
| Mean Well SD-15B-05 | **300** | `BOM-master.md` §1 item 12 (as purchased locally) |
|
||||
| — same part, direct import | *189–200* | Mouser $11.15 / Arrow $11.85 @100 |
|
||||
| DC PSU bracket (ASA print) | 20 | `BOM-master.md` §1 item 3 |
|
||||
| `P2` JST-XH + housing + crimps + wire | ~12 | est. |
|
||||
| **Total** | **≈ 332** *(≈ 221 if imported direct)* | |
|
||||
|
||||
Plus ~5 min/unit of mounting and wiring labour, and one more connector in the
|
||||
vibration path — `P2` is exactly the class of joint the RCA blamed.
|
||||
|
||||
---
|
||||
|
||||
## 4. Net
|
||||
|
||||
| Volume | External (local price) | On-board | **Saving** |
|
||||
|---|---:|---:|---:|
|
||||
| per unit | MX$332 | MX$42 | **MX$290** |
|
||||
| 30 units | MX$9,960 | MX$1,260 | **MX$8,700** |
|
||||
| 100 units | MX$33,200 | MX$4,200 | **MX$29,000** |
|
||||
|
||||
Against best-case direct import (MX$221) the saving is still **≈ MX$180/unit**.
|
||||
|
||||
---
|
||||
|
||||
## 5. What actually decides this (not the money)
|
||||
|
||||
**For on-board:**
|
||||
|
||||
- **3 A → 3.5 A.** The 10-Ago boot-loop was the SD-15B's 3 A ceiling. It also makes the
|
||||
standing `RS1` errata (3.12 A input limit sitting *above* a 3 A supply) go away by itself.
|
||||
- One fewer sub-assembly, one fewer connector, no import lead time on a PSU.
|
||||
|
||||
**Against on-board:**
|
||||
|
||||
- **Isolation is lost.** SD-15B-05 is 1.5 kVDC isolated; TPS54360B is not. HBOX ground
|
||||
becomes the cabinet 24 V return. The ignition already shares that return — worth a
|
||||
deliberate look at the ground topology before this is locked.
|
||||
- The EMC burden moves onto our board. The copper plan already calls `U25`'s input loop
|
||||
the loudest on the PCB; that stops being Mean Well's problem.
|
||||
- No field-replaceable part. Today a dead PSU is a 10-minute swap; after this it is a board.
|
||||
|
||||
---
|
||||
|
||||
## 6. 🔴 Side finding — the MP1584s cost more than this whole question
|
||||
|
||||
Genuine `MP1584EN-LF-Z` (C15051) is **$2.21 @100** on LCSC. V6 carries two (`U2`, `U12`)
|
||||
= **$4.41 / MX$75 per board**, against **$0.43** for the TPS54360B or **$0.58** for the
|
||||
AP64350 already qualified as `U20`.
|
||||
|
||||
Re-speccing those two rails onto a part already in the BOM saves **≈ $3.5 / MX$59 per
|
||||
board** — more than the entire 5 V-source saving — and it also retires EMC findings 3 and 4,
|
||||
which named the MP1584s directly. Worth a separate decision.
|
||||
|
||||
---
|
||||
|
||||
## 7. Battery — LiPo vs Li-ion, first pass
|
||||
|
||||
| | Energy | Price | **MXN / Wh** |
|
||||
|---|---:|---:|---:|
|
||||
| **Li-ion** Hyvel 18650 1S4P (current pack) | ~43 Wh | MX$204 | **≈ 4.7** |
|
||||
| **LiPo** 2000 mAh 1S, SparkFun @ Sandorobotics | 7.4 Wh | MX$270 | **≈ 36** |
|
||||
| **LiPo** 2000 mAh 1S, generic (indicative) | 7.4 Wh | ~MX$90–160 | **≈ 12–22** |
|
||||
|
||||
To reach today's 43 Wh with 2000 mAh pouches you need **6 in parallel**:
|
||||
MX$540–1,620 against MX$204. Li-ion 18650 is **3–8× cheaper per Wh**, and it is also the
|
||||
better cell for a box that runs hot — pouch cells swell, and heat is this product's
|
||||
standing failure mode.
|
||||
|
||||
LiPo buys thickness and form factor, nothing else. Treat the 2000 mAh cells as bench
|
||||
hardware. ⚠ The generic LiPo row is indicative — MercadoLibre/Amazon MX would not serve
|
||||
prices to an automated fetch; get one real quote before this line is used in a decision.
|
||||
44
docs/historia/fw-052-prueba-bateria.md
Normal file
44
docs/historia/fw-052-prueba-bateria.md
Normal file
@ -0,0 +1,44 @@
|
||||
# HBOX — Firmware 0.5.2: prueba de banco del reporte cada 10 min en batería
|
||||
|
||||
**Versión:** 1.0 · 24-sep-2026 · **Aplica a:** tarjetas V4 y V5 · **Duración:** ~30 min por unidad
|
||||
|
||||
> Hacer esta prueba completa en **una unidad V5 y una V4 antes de flashear el resto del lote**. Después,
|
||||
> en cada unidad basta con los pasos 1–4 (arranque + primera ventana en batería) dentro de su prueba final.
|
||||
|
||||
## Material
|
||||
- Unidad ensamblada con batería cargada (vbat ≥ 3.80 V), SIM activa, antenas conectadas y cielo abierto.
|
||||
- Fuente 24 V (o 5 V en P2) desconectable. Laptop con consola USB (PuTTY/screen, 115200).
|
||||
- Acceso al monitor (Salud) para ver la unidad.
|
||||
|
||||
## Binarios
|
||||
| Tarjeta | Archivo | SHA-256 |
|
||||
|---|---|---|
|
||||
| V5 | `hbox-v5-bringup_0.5.2.bin` | `a9b0d6c1c640ebc9a132236ca465cad04e8eed99962b7dc6024b6ee8b765d2d6` |
|
||||
| V4 | `hbox-tarjetaV4_0.5.2.bin` | `fa2b7554099b332c70ab18cde24b30a4f0a46ac01987466e491c69b308783a5a` |
|
||||
|
||||
Flasheo igual que 0.5.1 (DFU por J11, solo USB, sin 24 V):
|
||||
`dfu-util -a 0 -s 0x08000000:leave -D <archivo>.bin`
|
||||
|
||||
## Pasos
|
||||
| # | Acción | Resultado esperado | ✔ |
|
||||
|---|---|---|---|
|
||||
| 1 | Conectar consola USB tras flashear | Primera línea `hbox-v5-bringup 0.5.2`. Aparece la línea `bateria (0.5.2): reporte cada 10 min — ventana 90-180 s…` | ☐ |
|
||||
| 2 | Conectar 24 V | `STACK ON reason=auto` a los ~3 s; ~10 s después `MODOB ON`. En el monitor: fw `0.5.2-fm`, `pwr=VEH` | ☐ |
|
||||
| 3 | Esperar RTK FIX (LED verde). **Anotar la hora** y desconectar 24 V | `V5 PERDIDO`, `BACKUP inicio … modo reporte cada 10 min`. Monitor: `pwr=BATT` | ☐ |
|
||||
| 4 | Esperar ~90 s | `DUTY reposo: ventana 90s fix=RTK FIX phlg_aire≥2 … despierta en 510s`, luego `STACK OFF reason=duty_sleep`. LEDs apagados, parpadeo corto cada 5 s, ventilador apagado. STAT: `duty=REPOSO despierta_en=…` | ☐ |
|
||||
| 5 | Esperar hasta 10 min después del paso 3 | `DUTY despertar #…` y `STACK ON reason=duty_wake`. En el monitor aparece posición nueva y `DUTY_WAKE` | ☐ |
|
||||
| 6 | Esperar un segundo ciclo completo (~10 min) | Segundo `DUTY reposo` a ~90 s y segundo despertar a los 20 min del paso 3 | ☐ |
|
||||
| 7 | Con la unidad en reposo, reconectar 24 V | `DUTY fin`, `BACKUP fin`; `STACK ON reason=auto` a los ~3 s; reporte continuo | ☐ |
|
||||
| 8 | Repetir paso 3 y reconectar 24 V **durante** la ventana | `DUTY fin`, la etapa sigue encendida sin cortes | ☐ |
|
||||
| 9 | Pegar la consola completa (EVT + STAT) en el acta de **reflasheo** con fw 0.5.2 | Acta aprobada | ☐ |
|
||||
|
||||
**Criterio de aprobación:** dos ciclos completos con posición en el monitor cada 10 min, ventanas de
|
||||
90–180 s, sin `RECOVERY` ni `LIMP` en batería, y regreso a operación continua al reconectar.
|
||||
|
||||
**Medición opcional (si hay medidor de corriente en BAT+):** corriente promedio en reposo y durante la
|
||||
ventana. Es el dato que falta para calcular la autonomía real en batería.
|
||||
|
||||
## Si algo falla
|
||||
- No despierta a los 10 min → revisar si la consola muestra `DUTY despertar omitido (bateria baja / temperatura)`. Con vbat < 3.40 V o ≥ 60 °C es correcto.
|
||||
- Ventanas de 180 s siempre → no hay fix o no salen las `$PHLG` (revisar antena, SIM y `MODOB ON`).
|
||||
- Cualquier otro comportamiento: guardar la consola completa y reportar; volver a 0.5.1 si la unidad debe salir hoy.
|
||||
111
docs/historia/fw-31-modob-phlg.md
Normal file
111
docs/historia/fw-31-modob-phlg.md
Normal file
@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Firmware v3.1 — Modo B / $PHLG en banda — Design Brief & Execution Prompt
|
||||
tags: [project-drogheda, firmware, epct, modo-b, phlg, telemetria, design-brief, prompt]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO + Claude (decisión registrada 2026-08-08)
|
||||
date: 2026-08-08
|
||||
status: Active — EL prompt para la sesión de implementación de la telemetría en banda (v3.1 núcleo EPCT)
|
||||
authority: ejecuta la decisión A de `Informe-Tecnico-V3-V4` §4.7–4.8 (v1.2); hardware canónico = watchdog-electrical-design-state.md; hallazgos F1–F9 = firmware-review-v2.3-EinarBoard.md
|
||||
---
|
||||
|
||||
# Firmware v3.1 (núcleo EPCT) — encender la telemetría en banda
|
||||
|
||||
> **Cómo usar este doc.** Es la instrucción autocontenida para convertir la decisión
|
||||
> «**A = `$PHLG` tal cual, en banda por Modo B, es la palabra canónica de telemetría**»
|
||||
> (Leo, 2026-08-08) en software corriendo sobre V3 (EinarBoard 1.0, vía adaptador inversor)
|
||||
> y V4 (HBOX v4). Una sesión (humana o Claude) debe poder arrancar solo con este archivo.
|
||||
> Reglas vigentes: hostsim en verde antes de tocar hardware; **banco antes de campo**;
|
||||
> el NC de K1 es la garantía de hardware — ningún cambio puede debilitarla.
|
||||
|
||||
## 0. La decisión y su estado de partida
|
||||
|
||||
| Hecho | Estado hoy (v2.3 flota / v3.0 escrito) |
|
||||
|---|---|
|
||||
| Flujo NMEA al servidor | **Sin alterar** — posición pura; K1 desenergizado (PB5 sin manejar), `ENABLE_PHLG_RELAY=0` |
|
||||
| `$PHLG` | Se construye y emite **cada 30 s solo al log USB** (21 campos v2.3 / 25 v3.0, checksum XOR correcto) |
|
||||
| Camino Modo B en cobre | Listo y verificado en **ambas** tarjetas (PA10 tap · PA9→NO · PB5→Q6→bobina · NC fail-safe) |
|
||||
| Transparencia del módem | **Probada** 2026-06-22: `$PUBX` inyectada llegó byte-idéntica al servidor |
|
||||
| Palabra canónica | **DECIDIDA 2026-08-08: opción A** — la misma `$PHLG` del log USB, sin recortar ni re-empacar (una sola palabra en USB y aire; append-only se mantiene) |
|
||||
|
||||
Consecuencia inmediata útil: **el decodificador del backend puede desarrollarse y probarse
|
||||
HOY** contra capturas USB de banco — son los mismos bytes que llegarán por aire.
|
||||
|
||||
## 1. Fases (cada una con compuerta propia; 1 y 2 pueden correr en paralelo)
|
||||
|
||||
| Fase | Qué | Compuerta de salida |
|
||||
|---|---|---|
|
||||
| **1 — Servidor primero** (cero riesgo, sin tocar firmware) | `htracker`: extender `PHLG_FIELDS` a los 24 campos de datos v3.0; parser tolerante por longitud (20 = v2.3, 24 = v3.0+, extras = ignorar); validar checksum XOR; sentinelas −127/−1 → null; almacenar por unidad (llave `cell_id`) + panel/alertas (`limp=1`, `rec` subiendo, `cflt=1`, `nmea_age>60000`, `tmax≥75` o celda `≥55`) | Decodifica al 100 % un log USB real de banco (v2.3 **y** v3.0) |
|
||||
| **2 — Prerrequisitos F4** (firmware, sin encender nada) | SHT40 no bloqueante en dos fases + rings UART de 256 B (§3.1–3.2) | hostsim verde; TLM intacta; sin `delay()` en el lazo |
|
||||
| **3 — Relevo Modo B en banco V3** | `ENABLE_PHLG_RELAY=1` + driver PB5 (§3.3–3.5) en una 1.0 de banco (adaptador inversor) | Los 5 gates de banco de §4 en verde |
|
||||
| **4 — Piloto en vivo** | Una unidad transmitiendo; Fase 1 ya desplegada | `$PHLG` visible en el panel; RTK fix≥4 sostenido con relevo insertado; consumo medido |
|
||||
| **5 — V4** | Mismo código bajo `TARGET_HBOXV4` (PA9/PA10/PB5 = nets idénticas); después v3.2 = espejo de la MISMA palabra por W5500/TCP | bring-up V4 + gates §4 repetidos |
|
||||
|
||||
## 2. Diseño de la inserción (política de K1)
|
||||
|
||||
- **Insertar** solo en la transición BOOTING→RUNNING (después de «Boot complete»), **una vez
|
||||
por sesión de energía**: orden estricto `RELAY.begin(115200)` → `digitalWrite(PB5, HIGH)`.
|
||||
El transfer del G5V-1 (~ms) puede morder una sentencia en el switchover — aceptable una
|
||||
vez por sesión; jamás conmutar por-sentencia.
|
||||
- **Retirar** en TODA salida de RUNNING — dentro de `setPower(false)` (cubre
|
||||
`beginPowerCycle` y SLEEP): `digitalWrite(PB5, LOW)` (NC restituye al instante) →
|
||||
`RELAY.end()` → `pinMode(PA9, INPUT)` (flotar TX, espejo del patrón PA2 — no
|
||||
retroalimentar el riel).
|
||||
- **Fallas**: cuelgue del MCU → IWDG resetea → GPIO a default → PB5 cae → **el NC
|
||||
restituye el camino por hardware** (R41 10 k garantiza Q6 apagado durante el reset).
|
||||
Nada que programar: documentar como la historia de seguridad y probarla (gate G4).
|
||||
- **Presupuesto**: bobina ~30 mA de `SYS5V` mientras está insertado. En respaldo queremos
|
||||
la telemetría viva (¡ahí viaja `vbat`!) — se queda insertado; 30 mA contra 12 Ah es ruido.
|
||||
Datos: ~95 B/30 s ≈ **~0.3 MB/mes/unidad** — despreciable.
|
||||
|
||||
## 3. Cambios de código (archivo único `blackpillrtkcode_v3.ino` → v3.1)
|
||||
|
||||
1. **SHT40 en dos fases** (F4): sustituir `readSHT40()` bloqueante por máquina de dos
|
||||
estados dentro de `thermalService()`: pasada N dispara (`Wire` write `0xFD`, marca
|
||||
`sht40TrigMs`), pasada N+1 (≥10 ms después; THERMAL_PERIOD=2 s sobra) cosecha los 6
|
||||
bytes. Cero `delay()` en el lazo. TMP102 queda igual (free-running, lectura corta).
|
||||
2. **Rings UART de 256 B**: `build_opt.h` en la carpeta del sketch con
|
||||
`-DSERIAL_RX_BUFFER_SIZE=256 -DSERIAL_TX_BUFFER_SIZE=256` (vía STM32duino; equivalente
|
||||
`--build-property` en arduino-cli). Aplica a GNSS **y** RELAY.
|
||||
3. **Driver de inserción**: `#define PIN_BYP_CTRL PB5`; en `setup()` `OUTPUT` + `LOW`
|
||||
**antes** de cualquier otra cosa; helper `setModoB(bool)` con el orden de §2; llamada
|
||||
ON en la transición a RUNNING, OFF dentro de `setPower(false)`.
|
||||
4. **Flags**: `ENABLE_PHLG_RELAY 1` en el build EinarBoard; el pump (`pumpRelay()`) y la
|
||||
cola `pendingPhlg` **ya existen y son correctos** (reenvío byte a byte + inyección solo
|
||||
tras ≥8 ms de línea ociosa, solo en RUNNING) — no reescribir, solo encender.
|
||||
5. **Target V4**: introducir `TARGET_HBOXV4` (tercer target excluyente): hereda TODO el
|
||||
EPCT de `TARGET_EINARBOARD`; difiere en medidor/RGB→PCF8574 y en **no tocar PB12–15**
|
||||
(SPI2 del W5500). El Modo B es idéntico — cero cambios.
|
||||
6. **hostsim**: nuevo escenario `relay`: alimentar 10 min de GGA sintéticas a 1 Hz por el
|
||||
stub, verificar (a) salida byte-exacta salvo líneas `$PHLG` completas en fronteras de
|
||||
línea, (b) ninguna inyección con <8 ms de hueco, (c) `setModoB(false)` al salir de
|
||||
RUNNING. Correr toda la red existente (ladder/sps/rtkloss/gga/phlg/charge/gauge) sin
|
||||
regresión.
|
||||
7. **Docs**: CHANGELOG v3.1; LOGGING-SPEC §5b marcar «relay ON por default en EinarBoard»;
|
||||
PINOUT.md (la reescritura F7 pendiente absorbe esto).
|
||||
|
||||
## 4. Compuertas de banco (todas en una 1.0 con adaptador, antes de piloto)
|
||||
|
||||
- **G1 — Integridad byte-exacta**: capturar simultáneo F9P TX2 y módem DIN (o segunda
|
||||
USB-UART en el nodo DIN) 10 min @1 Hz → diff = solo líneas `$PHLG` íntegras, ninguna
|
||||
GGA partida ni byte alterado.
|
||||
- **G2 — Sin regresión RTK/VRS**: con relevo insertado y caster real, la unidad alcanza y
|
||||
sostiene fix ≥4 (la GGA de cortesía al caster sigue siendo válida).
|
||||
- **G3 — Llegada al servidor**: repetir la prueba `$PUBX` pero con `$PHLG` real → Fase 1
|
||||
la decodifica en el panel.
|
||||
- **G4 — Fail-safe**: forzar cuelgue (bucle infinito de prueba) → IWDG resetea → verificar
|
||||
con osciloscopio/continuidad que PB5 cae y el NC restituye ANTES del re-arranque; el
|
||||
flujo de posición nunca se detiene.
|
||||
- **G5 — Presupuesto de lazo**: con relevo activo, `TLM` sigue a 2 s, IWDG jamás dispara,
|
||||
sin `rxbad` creciente (los rings de 256 aguantan la ráfaga de época GNSS).
|
||||
|
||||
## 5. Riesgos / notas
|
||||
|
||||
- **F4 es prerrequisito duro** — encender el relevo sin él corrompe el uplink GGA del
|
||||
caster VRS (10 ms bloqueados ≈ 115 bytes vs ring de 64). El orden de fases no es opcional.
|
||||
- El switchover de K1 muerde ~1 sentencia una vez por sesión — el backend ya tolera
|
||||
pérdida de época (heartbeat 1 Hz); documentado, no mitigado.
|
||||
- v2.3 en flota **no cambia**: unidades sin re-flashear siguen transmitiendo posición pura
|
||||
por el NC — el backend debe seguir aceptando flujo sin `$PHLG` indefinidamente.
|
||||
- La opción C (Modbus, Bloque M de V4) queda como **espejo por consulta** para el cliente —
|
||||
no sustituye la palabra canónica; su mapa de registros sigue gateado por firma del cliente.
|
||||
- No tocar: la doctrina de recuperación, el formato `$PHLG` (append-only), ni el NC de K1.
|
||||
109
docs/historia/gnss-hallazgos.md
Normal file
109
docs/historia/gnss-hallazgos.md
Normal file
@ -0,0 +1,109 @@
|
||||
---
|
||||
title: "Project Drogheda — GNSS Findings Summary (Beamer source)"
|
||||
created: 2026-06-20
|
||||
owner: Leo Rodriguez (Helgen)
|
||||
type: findings-summary
|
||||
purpose: source material for a later LaTeX Beamer deck
|
||||
scope: 20 Jun 2026 bench captures + RTK14_440 healthy baseline + config files
|
||||
tags: [drogheda, hbox, gnss, rtk, ntrip, findings, presentation]
|
||||
---
|
||||
|
||||
# GNSS Findings Summary — Project Drogheda (20 Jun 2026)
|
||||
|
||||
> Presentation-ready. Each `##` is a candidate slide; bullets are speaker points.
|
||||
> Full detail: `2026-06-20_Drogheda_Bench-Log-Analysis_v1.0.md`. Tooling: `log-analysis/`.
|
||||
|
||||
## 1 · What we did
|
||||
|
||||
- Analyzed three independent log views per unit from the 20 Jun bench: u-blox receiver
|
||||
(`.ubx`), 4G-NTRIP-Master console (`.log`), and a server-side retranslator capture
|
||||
(tcpdump). Decoded the NTRIP-caster and ZED-F9P config files.
|
||||
- Built local tooling (in-browser **Log Explorer**, batch analyzer, dark dashboard) and
|
||||
established a **healthy GNSS baseline**.
|
||||
|
||||
## 2 · Headline: the GNSS receivers are fine — RTK is not being achieved
|
||||
|
||||
- **Every board produces valid NMEA** — good satellite counts (6–12) and HDOP (≈0.75–1.5).
|
||||
The **ZED-F9P is not the failure.**
|
||||
- **No unit reached RTK fix.** Every epoch is **SPS (quality 1)**; zero quality 4/5.
|
||||
Centimetre positioning was never delivered.
|
||||
- **Why:** corrections are not being applied — a combination of (a) corrections sourced
|
||||
from a **free public caster (rtk2go.com, no SLA)**, (b) the NTRIP correction client
|
||||
rarely reaching "connected," and (c) bench/indoor sky. The F9P config is actually
|
||||
**correct** (GGA on UART2 @1 Hz) — but the dump is **RAM-only; flash persistence is
|
||||
unverified** (a config lost on power-cycle would silently break the feed).
|
||||
|
||||
## 3 · The healthy baseline (RTK14_440) — what "good" looks like today
|
||||
|
||||
- Proven **end-to-end**: **984 packets, steady 1 Hz, zero dropouts**, valid Querétaro
|
||||
position delivered to the server — live in Wialon and the monitor.
|
||||
- **12 sats, HDOP 0.75** — excellent geometry. **But SPS, not RTK.**
|
||||
- So **"healthy delivery" ≠ "RTK."** The baseline is *healthy SPS + reliable delivery*;
|
||||
raise the bar to a fix-≥4 capture once corrections work.
|
||||
|
||||
## Update — 22 Jun 2026: live field capture + two ceilings on RTK
|
||||
|
||||
> A fresh live `udp/5010` server capture (not the 20-Jun bench) caught two units at once.
|
||||
|
||||
- **Telemetry forwarding CONFIRMED.** Enabling `$PUBX` (u-blox *proprietary* `$P…`, the
|
||||
same class as our planned `$PHLG`) on the F9P **UART2** → it reaches the server **intact**,
|
||||
next to GGA/RMC. The 4G NTRIP Master is a **transparent forwarder**, so **in-band unit
|
||||
health (`$PHLG`) to the server is viable** — proven with zero extra hardware.
|
||||
- **A natural A/B on RTK** (same capture):
|
||||
- **H0014 (Querétaro bench)** — RSSI **−93**, **SPS (quality 1)**. It sits in a **basement**.
|
||||
- **H0017 (Veracruz / ICAVE field)** — RSSI **−51**, **DGPS (quality 2, RMC mode `D`,
|
||||
base id 0133)** — i.e. it **is** applying corrections.
|
||||
- **Two independent ceilings on RTK** (refines §2's "RTK not achieved"):
|
||||
1. **Sky / antenna placement** — RTK fix is carrier-phase; a **basement caps it** no matter
|
||||
how good the corrections are. (Per Leo: bench antenna placement — not fixable there.)
|
||||
2. **Corrections pipeline** — H0017 has good sky + signal yet only reaches **DGPS, not RTK
|
||||
(4/5)**, pointing back at the **free rtk2go caster + RAM-only F9P config** as a *separate*
|
||||
limit present everywhere.
|
||||
- **Cleanest next test:** one **open-sky capture** (roof/outdoors) with a **proper caster +
|
||||
FLASH-saved config** — that isolates the two ceilings and shows whether a fix ≥4 is reachable.
|
||||
- **Watchdog implication:** a healthy unit can legitimately sit at SPS/DGPS (basement, or field
|
||||
on a weak caster) → **never power-cycle on no-RTK alone** (already enforced by the v2.1
|
||||
SPS-thrash guard; the v2.2 `$PHLG` will make this visible per-unit).
|
||||
|
||||
## 4 · The diagnostic trap (and a retraction)
|
||||
|
||||
- The **4G-NTRIP-Master console is an unreliable health signal.** The provably-healthy
|
||||
baseline logs the **same sparse console** (only cellular `NET` lines, no forwarding
|
||||
lines) as units that *looked* failed.
|
||||
- **Retraction:** the earlier "master-14 ingest fault" is **withdrawn** — master 14 is the
|
||||
baseline and demonstrably forwards. **Only master 21 is a confirmed fault** (modem never
|
||||
registers: `0xFFFFF` + 511 AT-parser errors).
|
||||
- **Lesson:** per-unit health must come from a component that *parses the fix itself* — the
|
||||
Blackpill — logged in a trustworthy form.
|
||||
|
||||
## 5 · Heat is not implicated by these logs
|
||||
|
||||
- Room-temperature bench captures, no thermal channel (v1). The faults reproduce **cold**,
|
||||
so heat is **not** the cause of what we see here. The **field** thermal hypothesis stays
|
||||
open — needs a thermocouple cabinet soak and/or v2 MCU-temp logging.
|
||||
|
||||
## 6 · Changes made this activity
|
||||
|
||||
- **Blackpill v2.1 logging spec** — replace the self-overwriting ANSI dashboard with a
|
||||
**structured, timestamped, parseable** stream (state, fix, packets, battery, MCU temp,
|
||||
recovery, **reset cause**). Makes per-unit health unambiguous from the unit's own log.
|
||||
- **Log tooling** — `hbox-log-tools` repo on `git.helgen.dev`: in-browser Log Explorer
|
||||
(parse + auto-plot + compare to baseline + PNG/SVG export), batch analyzer, dark dashboard.
|
||||
- **Corrected analysis + findings doc** (the master-14 retraction above).
|
||||
|
||||
## 7 · Next steps
|
||||
|
||||
1. **Persist the F9P config to FLASH and verify** (RAM-only is a power-cycle risk); check
|
||||
the simpleRTK2B Pro `XBEE↔UART2` switch.
|
||||
2. **Move corrections off rtk2go** to a dedicated caster (HelgenSRV) with uptime.
|
||||
3. **Open-sky RTK retest** with a verified caster → confirm fix ≥4/5.
|
||||
4. **Connect the Blackpill** with v2.1 logging → capture a true per-unit health log
|
||||
(drops into the Log Explorer as a fifth view).
|
||||
5. **Modem triage master 21** (known-good SIM/antenna; swap to confirm).
|
||||
6. **Thermal soak test** to settle the field heat hypothesis.
|
||||
|
||||
## One-line takeaway
|
||||
|
||||
> The radios work; we are delivering a **standalone-GPS** position reliably, **not RTK**.
|
||||
> The blockers are the **correction source/persistence** and **observability** — both now
|
||||
> addressed by config/caster changes and the new Blackpill logging + tooling.
|
||||
36
docs/historia/h0008-carga-nocturna.md
Normal file
36
docs/historia/h0008-carga-nocturna.md
Normal file
@ -0,0 +1,36 @@
|
||||
# H0008 — vigilancia de carga nocturna (fw 0.6.0, V5.5, SD-15B ~5.25 V)
|
||||
|
||||
Contexto: el LTC4040 reporta CHRG_STAT=1 (cargando) sin falla (cflt=0) desde up=157 s, pero el paquete sube muy despacio
|
||||
(3.55 V el 27-sep 14:00 → 3.83 V el 28-sep 19:08 con vbat_min 3.77 en la ventana de 30 s: oscila 60 mV = ~2.4 A conmutando).
|
||||
Riel 5 V plano (v5 = v5min = 5.25): el cargador NO está tirando ~2 A de forma continua. Hipótesis: (1) ciclado térmico del LTC4040
|
||||
(pista SW_BAT 0.3 mm, EP sin vías: OT interno), (2) corriente media baja por otra causa. Prueba definitiva en la mañana:
|
||||
pinza DC en el cable del paquete y temperatura de U6/L6 con el dedo o IR.
|
||||
|
||||
| Hora (UTC) | vbat | vbat_min | chg | cflt | v5 | v5min | tbat | tmcu/tmax | up_s | pwr | eventos nuevos |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 2026-09-28T01:08Z | 3.83 | 3.77 | 1 | 0 | 5.25 | 5.25 | 35 | 41/43 | 16367 | VEH | CHG_ON @157 s (v5=5.31 vbat=3.57); CHG_SHED @548 s v5=4.89 → V5_LOST (prueba); MODEM_BACK/MODEM_DARK (sag 160→6 mV, diagnóstico) |
|
||||
| 2026-09-28T02:42Z | 3.94 | 3.87 | 1 | 0 | 5.25 | 5.25 | 34 | 41/43 | 21977 | VEH | ninguno nuevo (sin CHG_SHED, sin fallas). +0.11 V en 94 min → ~1.3 A medios: SÍ está cargando. Oscilación 70 mV en 30 s = corriente pulsante. |
|
||||
| 2026-09-28T04:15Z | 4.01 | 3.96 | 1 | 0 | 5.27 | 5.25 | 34 | 40/43 | 27557 | VEH | ninguno nuevo. +0.07 V en 93 min (3.94→4.01): entra en zona CV (flotación 4.00 V) — la corriente empieza a bajar como debe. |
|
||||
| 2026-09-28T05:51Z | 4.02 | 4.01 | 0 | 0 | 5.30 | 5.26 | 33 | 39/43 | 33317 | VEH | **CHRG_IDLE @30443 s (vbat=4.01)**: el LTC4040 terminó la carga (C/8 en flotación 4.00 V) — paquete LLENO, sin falla. Modem watch oscila MODEM_DARK/BACK cada 2–5 min con sag 38–43 mV (justo en el umbral de 40 mV): ruido, necesita histéresis en 0.5.10. |
|
||||
| 2026-09-28T07:24Z | 4.02 | 4.02 | 0 | 0 | 5.30 | 5.27 | 32 | 38/43 | 38927 | VEH | ninguno nuevo. Paquete lleno y estable (4.02 V, sin oscilación: sin corriente de carga). Cargador en reposo tras terminación; recarga automática al bajar de VRECHRG (~3.9 V). |
|
||||
| 2026-09-28T08:55Z | 4.01 | 4.00 | 0 | 0 | 5.33 | 5.28 | 32 | 38/43 | 44387 | VEH | ninguno nuevo. Reposo tras carga completa (4.01 V). Fin de la vigilancia nocturna: 5/5 chequeos sin alarma. |
|
||||
|
||||
## Conclusión (28-sep 13:35 UTC, del historial de telemetría `telemetry_history.db`, muestras cada 5 min)
|
||||
|
||||
**El cargador funciona. La observación "atascado en 3.80 V" no era una falla.**
|
||||
|
||||
- Arranque con 0.6.0: 27-sep ~20:36 UTC (up_s 61187 a las 13:35Z). CHG_ON @157 s con vbat 3.57 V.
|
||||
- Cargando (`chg=Sí`) de forma continua desde 20:30Z hasta 05:00Z = **102 muestras × 5 min ≈ 8.5 h**, ninguna con `cflt` (0/242 muestras).
|
||||
- Curva: 3.67 (20:30) → 3.73 (21:30) → 3.78 (22:30) → **3.77–3.80 entre 22:00 y 00:30 (la meseta que se vio en vivo)** → 3.85 (01:00) → 3.90 (02:00) → 3.95 (03:00) → 4.00 (04:00) → 4.01 (04:30–05:00).
|
||||
- **Terminación: CHRG_IDLE @ up 30443 s ≈ 05:03Z** (C/8 en flotación 4.00 V, ~8.5 h después del CHG_ON). Después: 4.01–4.03 V en reposo hasta las 13:35Z, sin corriente (sin oscilación 30 s).
|
||||
- Riel 5 V: 5.25 V constante durante la carga (v5min 5.27 según firmware), 5.29–5.33 V en reposo; un solo mínimo 5.10 V en la muestra del arranque. **Ningún CHG_SHED después del boot** (el único, @548 s, fue la prueba de desconexión de Leo: V5_LOST v5=2.14).
|
||||
- Temperaturas durante la carga: batería P4 32–35 °C, MCU 38–42 °C (máx 43). Sin pausa NTC.
|
||||
- Ahora (13:35Z): vbat 4.00, chg=0, cflt=0, v5 5.31, pwr=VEH, bkp_n=0, tbat 29 °C.
|
||||
|
||||
Lectura: la zona 3.75–3.82 V es la parte plana de la curva OCV del Li-ion (la mayor parte de la carga entra ahí con poca variación de tensión) y la oscilación ±60–70 mV en la ventana de 30 s es la caída IR de una corriente de carga pulsante (el LTC4040 comparte L6 entre buck de carga y boost de respaldo). Con +0.11 V/94 min en la meseta la corriente media fue del orden de 1.3 A, no los 2.4 A de R50=825 Ω: el paquete tardó 8.5 h en lugar de ~2–3 h. Eso NO es una falla, pero sí es el margen que hay que medir.
|
||||
|
||||
Pendientes:
|
||||
1. **Pinza DC en el cable del paquete** durante la próxima carga (descargar hasta ~3.6 V y volver a conectar 24 V): esperado ~1.3 A medios; si la pinza marca ~2.4 A a pulsos, es limitación térmica/de entrada del LTC4040; si marca ~1.3 A continuos, revisar RS1/R50 en el ensamble.
|
||||
2. **Temperatura de U6 / L6** con IR o dedo a los 15–20 min de carga (hipótesis de ciclado por OT: EP sin vías, SW_BAT 0.3 mm).
|
||||
3. Firmware 0.5.10 / 0.6.1: **histéresis en el modem watch** (sag5 osciló 38–43 mV alrededor del umbral de 40 mV → MODEM_DARK/BACK cada 2–5 min toda la noche; propuesta: DARK < 30 mV durante 4 ventanas, BACK > 50 mV en 1 ventana).
|
||||
4. Monitor: desplegar HISTORIAL (local, sin subir) para ver esta curva sin exportar CSV.
|
||||
144
docs/historia/halt-rig-compras.md
Normal file
144
docs/historia/halt-rig-compras.md
Normal file
@ -0,0 +1,144 @@
|
||||
---
|
||||
tags: [project-drogheda, testing, halt, compras, rig]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO
|
||||
date: 2026-09-17
|
||||
status: Para compra
|
||||
version: 1.0
|
||||
---
|
||||
|
||||
# Lista de compras — Banco de pruebas ambiental HBOX
|
||||
|
||||
**Diseño de referencia:** `2026-09-17_Drogheda_HALT-Rig_Thermal-Vibration-Impact_v1.1.md`
|
||||
**Presupuesto objetivo:** ≈ $30,100 MXN con 15 % de contingencia.
|
||||
**Configuración aprobada:** cámara capaz a **75 °C** (se opera a 60 °C) ·
|
||||
accionamiento de vibración **completo** (motovibrador + variador).
|
||||
|
||||
> **Compra en dos oleadas.** La Oleada 1 (etapa térmica) se puede pedir hoy y es
|
||||
> útil por sí sola. La Oleada 2 (mecánica) se pide cuando la cámara pase la
|
||||
> compuerta G1. Si el presupuesto aprieta, la Oleada 2 se difiere a octubre sin
|
||||
> perder nada de la Oleada 1.
|
||||
|
||||
---
|
||||
|
||||
## OLEADA 1 — Etapa térmica (≈ $9,800 MXN + $1,100 de seguridad)
|
||||
|
||||
Pedir esta semana. Todo se consigue en MercadoLibre / Amazon MX / Steren /
|
||||
ferretería industrial local.
|
||||
|
||||
| # | Artículo | Especificación crítica | Cant. | Est. MXN |
|
||||
|---|---|---|---|---|
|
||||
| 1.1 | Lámina de aluminio 3 mm | Para forro interior y puerta, cubo de 400 mm. ~1.2 m² + mermas | — | 2,000 |
|
||||
| 1.2 | Panel de lana mineral (rockwool) **75 mm** | **NO usar EPS/XPS/poliuretano** — el EPS está clasificado a ~75 °C, justo en nuestro punto de diseño. Densidad ~100 kg/m³. ~2.5 m² | — | 1,000 |
|
||||
| 1.3 | Piel exterior: triplay 18 mm o lámina 1.2 mm + bastidor | Cubo exterior 550 mm | — | 900 |
|
||||
| 1.4 | **Calentador PTC con ventilador, 220 V 300 W** | Buscar "calentador PTC ventilador 220V 300W". Auto-limitante. **3 piezas** = 900 W total | 3 | 1,050 |
|
||||
| 1.5 | Ventilador de recirculación, **aspas metálicas**, alta temperatura | Aspas de plástico se deforman. 120 mm, 220 V | 1 | 400 |
|
||||
| 1.6 | Kit PID: **REX-C100 + SSR-40DA + termopar tipo K** | Kit común, ~$450. Verificar que el SSR sea 40 A DA | 1 | 450 |
|
||||
| 1.7 | 🔴 **Termostato KSD301 NC 100 °C** | **Corte independiente. No sustituir por 85 °C** (nuestro setpoint máximo es 75 °C) | 2 | 120 |
|
||||
| 1.8 | 🔴 **Contactor mecánico** 220 V, ≥10 A, bobina 220 V | Va en serie con el KSD301. **Es la única protección contra un SSR pegado.** No omitir | 1 | 350 |
|
||||
| 1.9 | Interruptor termomagnético 10 A + caja de control metálica | | 1 | 400 |
|
||||
| 1.10 | Termopares tipo K adicionales + **registrador 4 canales SD/USB** | Para la traza medida del informe. Va FUERA de la caja | 1 | 700 |
|
||||
| 1.11 | 🔴 **Cable con aislamiento de silicón** (interior de la cámara) | **NO PVC.** El PVC a 75 °C junto a una resistencia se vuelve quebradizo y luego conductivo. AWG 14 y AWG 18, ~10 m c/u | — | 400 |
|
||||
| 1.12 | Prensaestopas de silicón + placa de pasamuros (cara trasera) | | — | 200 |
|
||||
| 1.13 | **Empaque de silicón para horno** (200 °C+), bisagra piano, 2 pestillos de palanca | No usar EPDM ni cinta de espuma | — | 500 |
|
||||
| 1.14 | 🔴 **Fuelle/bota de silicón, barreno 50 mm** + brida de piso | Paso de la varilla motriz. Silicón 200 °C | 1 | 450 |
|
||||
| 1.15 | Entrada IEC, indicadores, **paro de emergencia** | | — | 400 |
|
||||
| 1.16 | Consumibles: remaches, ángulo, tornillería, sellador de silicón alta temp | | — | 480 |
|
||||
| | | | **Subtotal** | **9,800** |
|
||||
|
||||
### Seguridad — comprar junto con la Oleada 1, no después
|
||||
|
||||
| # | Artículo | Por qué |
|
||||
|---|---|---|
|
||||
| S.1 | **Cubeta de arena seca 20 kg** — $200 | Pack de litio en la cámara. **CO₂ no sirve** para litio |
|
||||
| S.2 | Manta ignífuga (welding blanket) — $600 | |
|
||||
| S.3 | Guantes térmicos ×2 — $300 | Superficies a 75 °C |
|
||||
| S.4 | *Opcional:* extintor Clase D — ~$3,500 | La arena + manta cubre el caso realista. En el BOM va con cantidad 0 |
|
||||
|
||||
**Subtotal seguridad: $1,100 MXN** (sin el extintor Clase D).
|
||||
|
||||
---
|
||||
|
||||
## OLEADA 2 — Etapa mecánica (≈ $16,800 MXN)
|
||||
|
||||
Pedir al pasar la compuerta G1.
|
||||
|
||||
### Vibración
|
||||
|
||||
| # | Artículo | Especificación crítica | Cant. | Est. MXN |
|
||||
|---|---|---|---|---|
|
||||
| 2.1 | **Motovibrador trifásico 0.18–0.25 kW** | 🔴 **Debe traer contrapesos ajustables** (medias lunas). Sin eso se pierde el plan de barridos por bandas. Pedir la hoja de datos: **fuerza centrífuga máx. y velocidad máx. de baleros** — decide si la Banda C llega a 80 Hz | 1 | 3,500 |
|
||||
| 2.2 | **Variador de frecuencia (VFD) 0.75 kW** | 220 V monofásico entrada → trifásico salida. Debe permitir rampas programables y salida > 60 Hz | 1 | 1,900 |
|
||||
| 2.3 | Placa de aluminio **15 mm, 300 × 300 mm** | Placa de fixture. **Aluminio, no acero** — el acero añade 6 kg de masa móvil | 1 | 1,400 |
|
||||
| 2.4 | Varilla motriz acero 20 mm × 350 mm + coples + rótula (rod-end) | | — | 900 |
|
||||
| 2.5 | **Resortes de compresión 2.5 N/mm** | fn ≈ 5 Hz con 10.3 kg, hundimiento estático ~10 mm. Pedir la constante, no "resortes medianos" | 4 | 500 |
|
||||
| 2.6 | Bastidor base: PTR 2", soldado, anclado a piso | | 1 | 1,600 |
|
||||
| 2.7 | 🔴 **Guarda: malla de acero + ventana de policarbonato + paro de emergencia** | Masa excéntrica a 4800 rpm. **No omitir** | 1 | 1,200 |
|
||||
| 2.8 | Tornillería, **Loctite 243**, soportes DUT | 🔴 Los soportes deben ser **idénticos a los de la grúa** — si no, estamos probando el fixture | — | 700 |
|
||||
| | | | **Subtotal** | **11,700** |
|
||||
|
||||
### Impacto
|
||||
|
||||
| # | Artículo | Especificación | Cant. | Est. MXN |
|
||||
|---|---|---|---|---|
|
||||
| 3.1 | Eje lineal 16 mm × 300 mm + rodamientos SC16UU | Guías de caída | 2 + 4 | 900 |
|
||||
| 3.2 | Palanca de izaje, retén, pestillo de liberación, soportes de eje | Fabricación en taller | 1 | 900 |
|
||||
| 3.3 | **Surtido de tacones de hule, Shore 60A y 70A**, Ø 20/25/30/40/50 mm, 25 mm de espesor | Se sintonizan contra el acelerómetro. Comprar surtido: el cálculo E·A/L **subestima** la rigidez real | — | 600 |
|
||||
| | | | **Subtotal** | **2,400** |
|
||||
|
||||
### Instrumentación
|
||||
|
||||
| # | Artículo | Especificación | Cant. | Est. MXN |
|
||||
|---|---|---|---|---|
|
||||
| 4.1 | Módulo **ADXL355** | ±8 g, 20 bits, SPI. Acelerómetro de referencia del banco | 1 | 1,100 |
|
||||
| 4.2 | Módulo **ADXL1002** | ±50 g, 11 kHz BW, analógico. 🔴 Para choque — el ADXL355 satura y hace aliasing en un pulso de 15 g / 11 ms | 1 | 700 |
|
||||
| 4.3 | Teensy 4.0 + SD + protoboard + cableado de silicón | ≥2 kHz en el canal de choque. Va fuera de la caja caliente | 1 | 900 |
|
||||
| | | | **Subtotal** | **2,700** |
|
||||
|
||||
---
|
||||
|
||||
## Resumen
|
||||
|
||||
| Bloque | MXN |
|
||||
|---|---|
|
||||
| Oleada 1 — térmica | 9,800 |
|
||||
| Oleada 1 — seguridad | 1,100 |
|
||||
| Oleada 2 — vibración | 11,700 |
|
||||
| Oleada 2 — impacto | 2,400 |
|
||||
| Oleada 2 — instrumentación | 2,700 |
|
||||
| **Subtotal** | **27,700** |
|
||||
| Contingencia 15 % | 4,155 |
|
||||
| **TOTAL** | **≈ 31,855** |
|
||||
|
||||
> 🔴 **El archivo de control de compras es el BOM en Excel**, no esta tabla:
|
||||
> `2026-09-17_Drogheda_HALT-Rig_BOM_v1.0.xlsx`. Ahí se capturan proveedor,
|
||||
> estado y precio real, y los totales se recalculan solos.
|
||||
|
||||
---
|
||||
|
||||
## Los seis puntos que NO se recortan
|
||||
|
||||
1. **KSD301 100 °C + contactor mecánico** (1.7, 1.8). El modo de falla dominante
|
||||
de un SSR es quedarse pegado en conducción, y el PID entonces no tiene
|
||||
ninguna autoridad. Es lo único que separa un SSR pegado de un incendio
|
||||
durante un soak nocturno.
|
||||
2. **Cable de silicón, no PVC** (1.11).
|
||||
3. **Lana mineral, no EPS** (1.2).
|
||||
4. **Guarda + paro de emergencia** en la masa rotatoria (2.7, 1.15).
|
||||
5. **ADXL1002 para el canal de choque** (4.2). Es la medición que no se puede
|
||||
fingir.
|
||||
6. **Contrapesos ajustables en el motovibrador** (2.1). Sin ellos no hay
|
||||
barridos por bandas y el banco deja de ser repetible — que es el punto entero.
|
||||
|
||||
## Antes de comprar el motovibrador (2.1)
|
||||
|
||||
Pedir al proveedor, por escrito:
|
||||
|
||||
- Fuerza centrífuga máxima (N o kgf) y a qué rpm
|
||||
- Velocidad máxima admisible de baleros
|
||||
- Rango de ajuste de los contrapesos (m_e·e mínimo y máximo)
|
||||
- Si admite operación continua a 80 Hz con variador
|
||||
|
||||
Necesitamos **m_e·e ajustable entre ~1.4 y ~12.5 g·m** (equivalente a 28 g y
|
||||
250 g a 50 mm de radio). Si el motovibrador no baja de ~1.4 g·m, la Banda C no
|
||||
se puede ajustar y hay que mecanizar contrapesos más ligeros.
|
||||
529
docs/historia/halt-rig-impacto-v10.md
Normal file
529
docs/historia/halt-rig-impacto-v10.md
Normal file
@ -0,0 +1,529 @@
|
||||
---
|
||||
tags: [project-drogheda, testing, halt, environmental, hbox, rig]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO
|
||||
date: 2026-09-17
|
||||
status: SUPERSEDED by v1.1 (2026-09-17) — architecture changed after the 75 °C decision; do not build from this file
|
||||
supersedes: none
|
||||
---
|
||||
|
||||
# DIY Environmental Test Rig — 60 °C + Vibration + Impact for HBOX
|
||||
|
||||
!!! warning "Designators from before 22-Oct (T43)"
|
||||
This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the v6, U6 is now the A7670SA modem (was U19) and the LTC4040 is U17 (was U6). To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it.
|
||||
|
||||
**Question asked (Leo, 17-Sep):** can we build a cheap, off-the-shelf heat chamber
|
||||
with vibration and impact that reproduces crane service conditions for the HBOX?
|
||||
|
||||
**Answer: yes.** Target scope agreed: 1–2 units in the chamber, self-declared
|
||||
profile-shaped results (measured and logged, not calibration-traceable),
|
||||
budget band **$10k–$30k MXN**. The design below lands at **~$22k MXN**
|
||||
with a **~$14k MXN** stripped variant, buildable in the Querétaro shop in
|
||||
**2–3 weeks** of part-time work.
|
||||
|
||||
The single most important design decision is in §2: **measure the real crane
|
||||
environment before building anything**, using the accelerometer that is already
|
||||
on the v6 board. That step costs nothing and is what turns this from a
|
||||
"shake it and see" box into something we can put in a report and show ICAVE.
|
||||
|
||||
---
|
||||
|
||||
## 1. What we are actually trying to catch
|
||||
|
||||
We are not trying to certify the HBOX. We are trying to reproduce, in a week on
|
||||
a bench, the failure modes that took months to appear at ICAVE, TIMSA and LCT:
|
||||
|
||||
| Failure mode | Provoked by | Evidence we already have |
|
||||
|---|---|---|
|
||||
| Solder joint / pad cracking on heavy parts (L22, U25, electrolytics, the GX16 entry) | Vibration + thermal cycling | Generic, but v5.5 has tall inductors and a 100 µF/50 V can |
|
||||
| Connector backout / intermittent (P1 Phoenix, U.FL pigtails, GX16) | Vibration + impact | U.FL rated ~30 mating cycles; field units were hand-wired |
|
||||
| Screw / standoff loosening, enclosure resonance | Vibration | ASA case in a sealed ABS cabinet |
|
||||
| Battery charge stall and hot-pause at temperature | 60 °C soak | **Confirmed**: `RNTB` = 10 k puts the LTC4040 hot-pause at ≈ +42 °C (B3380). Port ambient ~45 °C. See `Battery-Chemistry-HighTemp v1.0` |
|
||||
| Modem brownout / boot-loop under load at temperature | 60 °C soak + LTE TX burst | 10-Ago RCA, LTC4040 PFI 4.60 V trip |
|
||||
| Firmware thermal policy bugs (fan, `BAT_CRITICAL`, watchdog) | 60 °C soak | fan-policy v3 doc, and v6 still has open firmware items |
|
||||
| GNSS TCXO drift / fix loss hot | 60 °C soak | not yet characterised |
|
||||
|
||||
Note the asymmetry: **most of our known failures are thermal, not mechanical.**
|
||||
The heat chamber is the part that earns its money immediately. Vibration and
|
||||
impact are the insurance policy for the 250-unit rollout, where a 1 % mechanical
|
||||
failure rate is 2–3 truck rolls to Veracruz.
|
||||
|
||||
That argues for building the thermal chamber first (§4) and the mechanical stage
|
||||
second (§5–6). They are separable and the chamber is useful on day one.
|
||||
|
||||
---
|
||||
|
||||
## 2. Step zero: measure the crane, don't guess it
|
||||
|
||||
**Do this before buying a single part.**
|
||||
|
||||
The published standards a crane-mounted controller would normally be judged
|
||||
against are all borrowed from adjacent industries and none of them is
|
||||
"port crane":
|
||||
|
||||
- **IEC 60068-2-6** (Test Fc, sinusoidal sweep) — the generic industrial
|
||||
severity is a constant-displacement leg then a constant-acceleration leg,
|
||||
e.g. 10–150 Hz, 0.35 mm / 5 m·s⁻², 1 oct/min, 10–20 sweep cycles per axis.
|
||||
Toradex, for example, runs 10–55 Hz at 0.54 mm and 10–300 Hz at 39 m·s⁻².
|
||||
- **IEC 61373 Cat 1 Class B** (railway, equipment inside a case on the car body)
|
||||
— the closest analogue in spirit: low-level random 5–150 Hz for function,
|
||||
plus a 5 h/axis simulated-long-life run.
|
||||
- **IEC 60068-2-27** (Test Ea, shock) — half-sine severities of
|
||||
15 g / 11 ms, 30 g / 18 ms, 30 g / 11 ms, 50 g / 20 ms, typically
|
||||
200–1000 shocks per direction over 3 axes.
|
||||
|
||||
Picking one of these off the shelf would be guessing. We don't have to guess:
|
||||
|
||||
> **EinarBoard v6 carries U26 = SC7A20TR (LIS3DH-compatible, I²C 0x18,
|
||||
> INT1 → PB9).** Firmware work for `$PHLG` vibration fields is already an open
|
||||
> item in the v6 brief. Finish that item first, flash one unit, mount it on a
|
||||
> live crane at ICAVE for 5–7 working days, and log raw XYZ at 400 Hz ODR in
|
||||
> bursts (e.g. 10 s every 5 min, plus a triggered burst on INT1 over threshold).
|
||||
|
||||
From that log we get, for free:
|
||||
|
||||
1. the real **PSD** — where the energy actually sits (expect < 50 Hz: gantry
|
||||
travel over rail joints, trolley accel/decel, hoist, structural modes);
|
||||
2. the real **shock spectrum** — container landing and twistlock events, which
|
||||
is what "impact on a crane" actually means;
|
||||
3. the **duty cycle** — how many shock events per shift, which sets how many
|
||||
shocks the rig has to deliver to represent a year of service;
|
||||
4. a defensible sentence for the report: *"the rig reproduces the 95th-percentile
|
||||
measured field environment at ICAVE, ×N acceleration factor."*
|
||||
|
||||
That sentence is worth more to a client than "we ran IEC 60068-2-6."
|
||||
|
||||
**Fallback if the field log slips:** build to **5–60 Hz, 0.5 g rms broadband,
|
||||
peaks to 2 g**, plus **15 g / 11 ms half-sine shock**. Those are the numbers
|
||||
sized throughout this document and they are defensible as a conservative
|
||||
industrial-vehicle envelope.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
Three subsystems on one frame. The key trick is that **the hot box rides on the
|
||||
shaker** rather than the shaker living inside the heat — a 3600 rpm motor,
|
||||
its bearings and a VFD do not belong in a 60 °C oven.
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ HOT BOX (40×40×40 useful) │
|
||||
│ ┌───────┐ ┌───────┐ │ ← 50 mm rockwool,
|
||||
│ │ HBOX │ │ HBOX │ │ 3 mm alu liner
|
||||
│ │ DUT1 │ │ DUT2 │ │
|
||||
│ └───────┘ └───────┘ │ ← DUTs on the SAME
|
||||
│ PTC heater + circ. fan │ bracket the crane uses
|
||||
└──────────────┬──────────────┘
|
||||
══════════════╪══════════════ ← 12 mm alu platen
|
||||
║ │ ║
|
||||
[spring] [ECC] [spring] ← VIBRATION MODE:
|
||||
║ motor ║ 4 soft springs, fn ≈ 5 Hz
|
||||
══════╩═══════════════╩══════ ← steel base frame, floor-bolted
|
||||
▲ SHOCK MODE: springs swapped
|
||||
│ 4× linear shaft + SC16UU for Shore-60A pucks, platen
|
||||
│ (guided drop, 25–55 mm) lifted and released
|
||||
```
|
||||
|
||||
Service loop for mains + thermocouples + DUT harness enters the moving platen
|
||||
through a slack silicone-jacketed loop with a strain-relief anchor on both ends —
|
||||
this is the #1 thing that breaks on rigs like this.
|
||||
|
||||
**Mode switching is a 20-minute job** (unbolt 4 springs, bolt in 4 pucks). Do not
|
||||
try to do vibration and shock simultaneously; the pad stiffness requirements are
|
||||
in direct conflict (see §6).
|
||||
|
||||
---
|
||||
|
||||
## 4. Thermal stage — 60 °C
|
||||
|
||||
### Sizing
|
||||
|
||||
Steady-state loss through 50 mm rockwool (k ≈ 0.040 W·m⁻¹·K⁻¹) on a 0.50 m cube,
|
||||
ΔT = 45 K worst case (15 °C shop morning → 60 °C):
|
||||
|
||||
```
|
||||
Q_cond = k·A·ΔT/t = 0.040 × 1.22 m² × 45 / 0.050 ≈ 44 W
|
||||
× 3 for leakage, cable penetrations, door seal, thermal bridges ≈ 130 W
|
||||
```
|
||||
|
||||
Pull-up dominates the sizing, not steady state. A 10 kg aluminium platen +
|
||||
5 kg of DUT + fixture raised 35 K is **455 kJ**:
|
||||
|
||||
| Pull-up time | Power for the thermal mass alone |
|
||||
|---|---|
|
||||
| 15 min | 506 W |
|
||||
| 20 min | 379 W |
|
||||
| 30 min | 253 W |
|
||||
|
||||
**Specify 600 W** (2 × 300 W PTC fan-heater modules). That gives ~25 min to
|
||||
setpoint with margin, and the PID will spend most of the soak at 20–25 % duty.
|
||||
|
||||
PTC elements are the right choice over nichrome coils or cartridge heaters: they
|
||||
self-limit, they come as a sealed module with the fan integrated, and they cannot
|
||||
run away if the fan stops.
|
||||
|
||||
### Control and — more important — safety
|
||||
|
||||
Two independent layers. This is not optional.
|
||||
|
||||
1. **Control loop:** REX-C100 PID → SSR-40DA → heaters. Type-K thermocouple in
|
||||
the return air stream, *not* touching a DUT.
|
||||
2. **Independent over-temperature cutout:** a **KSD301 85 °C normally-closed
|
||||
thermostat** wired in series with the coil of a **mechanical contactor** that
|
||||
feeds the heaters. The dominant failure mode of an SSR is **shorted on**, and
|
||||
when that happens the PID has no authority at all. The contactor is the only
|
||||
thing between a stuck SSR and a fire in an unattended overnight soak.
|
||||
3. Plus: a 10 A breaker, a metal control enclosure, and PE bonded to the alu liner.
|
||||
|
||||
Log independently of the PID: 2 spare type-K probes (one at each DUT surface,
|
||||
one in the air) into a cheap 2–4 channel USB/SD logger, so the report has a
|
||||
measured temperature trace rather than "the display said 60."
|
||||
|
||||
### Materials
|
||||
|
||||
- **Insulation: rockwool board, not EPS/XPS.** EPS is rated ~75 °C and softens
|
||||
and outgasses long before that near a heater; rockwool is non-combustible and
|
||||
costs about the same. Polyiso is also fine but rockwool is safer next to a
|
||||
600 W element.
|
||||
- **Liner: 3 mm aluminium or a stainless steamer-pan shell.** Needed both as a
|
||||
fire barrier and to stop the insulation from shedding fibres onto the DUTs.
|
||||
- Door: piano hinge + silicone oven gasket + two over-centre latches.
|
||||
|
||||
### 🔴 Battery hazard — read before the first soak
|
||||
|
||||
The HBOX contains a lithium pack. A 60 °C ambient plus internal self-heating plus
|
||||
LTE TX bursts puts cells above their charge window, and per
|
||||
`Battery-Chemistry-HighTemp v1.0` the LTC4040 already hot-pauses at ≈ +42 °C with
|
||||
the current `RNTB` = 10 k.
|
||||
|
||||
- Run the **first** soaks with the pack fitted but the charger inhibited, and a
|
||||
thermocouple taped to the pack.
|
||||
- **Never leave the first soak of a new build configuration unattended.**
|
||||
- Keep a Class-D extinguisher or a bucket of dry sand and a welding blanket at
|
||||
the rig. Not a CO₂ extinguisher.
|
||||
- If we move to high-temp LFP 26650, re-baseline everything — LFP is the whole
|
||||
reason 60 °C becomes a *reasonable* soak rather than a destructive one.
|
||||
|
||||
Useful side effect: **this rig is the cheapest way to prove the RNTB → 5.6 k +
|
||||
LFP decision** before committing it to the 30-unit BOM.
|
||||
|
||||
---
|
||||
|
||||
## 5. Vibration stage
|
||||
|
||||
### Mechanism choice
|
||||
|
||||
**Rotating eccentric mass on soft springs.** It is the only mechanism that gives
|
||||
useful force at 5–60 Hz for four figures of pesos. An electrodynamic shaker
|
||||
(§5.4) is better but is a Phase 2 upgrade.
|
||||
|
||||
Moving assembly budget: alu platen 12 mm (~5 kg) + hot box shell and insulation
|
||||
(~4 kg) + heaters and fixture (~2 kg) + 2 DUTs (~5 kg) ≈ **16 kg**.
|
||||
|
||||
### The honest limitation — read this before believing any number below
|
||||
|
||||
A fixed eccentric on a soft-mounted mass produces **constant displacement**,
|
||||
so acceleration rises as **f²**:
|
||||
|
||||
| m_e·e | peak displacement | a @ 10 Hz | a @ 30 Hz | a @ 60 Hz |
|
||||
|---|---|---|---|---|
|
||||
| 2 g·m | 0.12 mm | 0.05 g | 0.45 g | 1.81 g |
|
||||
| 5 g·m | 0.31 mm | 0.13 g | 1.13 g | 4.53 g |
|
||||
| 10 g·m | 0.62 mm | 0.25 g | 2.26 g | 9.05 g |
|
||||
| 20 g·m | 1.25 mm | 0.50 g | 4.53 g | 18.1 g |
|
||||
|
||||
You cannot hold a flat 0.5 g across 5–60 Hz with one weight setting. Anyone who
|
||||
tells you their eccentric rig "runs IEC 60068-2-6" either has closed-loop
|
||||
amplitude control or is not measuring.
|
||||
|
||||
**What we do instead — banded sweeps with a weight change between bands:**
|
||||
|
||||
| Band | Frequency | Motor RPM | Eccentric setting m_e·e | Achieved a |
|
||||
|---|---|---|---|---|
|
||||
| A | 5–15 Hz | 300–900 | ~20 g·m (400 g @ 50 mm) | 0.1 → 1.1 g |
|
||||
| B | 15–40 Hz | 900–2400 | ~5 g·m (100 g @ 50 mm) | 0.3 → 2.0 g |
|
||||
| C | 40–80 Hz | 2400–4800 | ~1.5 g·m (30 g @ 50 mm) | 0.9 → 3.8 g |
|
||||
|
||||
> ⚠ Band C overspeeds a 60 Hz motor to 80 Hz on the VFD. Check the motovibrador's
|
||||
> rated maximum centrifugal force and bearing speed before doing this, and
|
||||
> de-rate the weights accordingly — the force the bearings see scales as f² too.
|
||||
> If the datasheet won't allow it, stop band C at 60 Hz and use Option V3 (BLDC)
|
||||
> for anything above.
|
||||
|
||||
Each band is swept at 1 octave/min, several cycles, and **the accelerometer logs
|
||||
what actually happened**. The report states measured levels per band, which is
|
||||
honest and still perfectly good at finding cracked joints.
|
||||
|
||||
### Suspension
|
||||
|
||||
Soft springs so the platen floats above its own suspension resonance and the
|
||||
bench does not get shaken:
|
||||
|
||||
| Target fn | k total | per spring (×4) | static deflection |
|
||||
|---|---|---|---|
|
||||
| 4 Hz | 10.1 kN/m | 2.5 kN/m | 15.5 mm |
|
||||
| **5 Hz** | **15.8 kN/m** | **4.0 kN/m** | **9.9 mm** |
|
||||
| 6 Hz | 22.7 kN/m | 5.7 kN/m | 6.9 mm |
|
||||
|
||||
Pick **fn ≈ 5 Hz** (4 N/mm springs). Note this means band A starts near the
|
||||
suspension resonance — expect amplification there and **do not dwell at 5–7 Hz**
|
||||
until you have watched the accelerometer trace.
|
||||
|
||||
### Drive options
|
||||
|
||||
- **Option V1 (recommended):** 3-phase motovibrador 0.18–0.25 kW with adjustable
|
||||
half-moon counterweights + **VFD 0.75 kW, 220 V 1-ph in / 3-ph out**. Smooth
|
||||
0–80 Hz, ramps programmable, weights adjustable by design. ~$5,400 MXN.
|
||||
- **Option V2 (cheap):** 110 V 100 W single-phase vibrating motor
|
||||
(VEVOR-class, 3600 rpm) on a phase-angle dimmer. Works, but speed regulation
|
||||
under load is poor and the eccentric is often not adjustable. ~$2,000 MXN.
|
||||
Acceptable for the stripped build.
|
||||
- **Option V3 (reach 80–200 Hz):** RC outrunner BLDC + 80 A ESC + servo tester,
|
||||
eccentric disc on the shaft. Spins to 10 000+ rpm for ~$1,200 MXN. Crude but
|
||||
it is the only cheap way to excite PCB-level resonances (typically 150–300 Hz).
|
||||
|
||||
### 5.4 Phase 2 upgrade (optional, ~$6k MXN) — the woofer shaker
|
||||
|
||||
A 15" pro-audio woofer, its voice coil coupled to a light fixture, driven by a
|
||||
class-D amp from a PC sound card, gives genuine **closed-loop swept sine and
|
||||
random 20–200 Hz** with the accelerometer as the control sensor. Force is
|
||||
modest (~100 N, so ~2 g on a 5 kg light fixture — the hot box is too heavy for
|
||||
it), but it is the only sub-$10k way to run a real controlled profile.
|
||||
Worth doing later for bare-PCBA resonance search; not needed for v1.
|
||||
|
||||
---
|
||||
|
||||
## 6. Impact / shock stage
|
||||
|
||||
A guided drop table. The platen is lifted a set height on four linear shafts and
|
||||
released onto elastomer pucks. Pulse shape is a clean half-sine; peak g and
|
||||
duration are set by drop height and puck stiffness, and both are *measured*, not
|
||||
assumed.
|
||||
|
||||
For the **15 g / 11 ms** target on the 16 kg platen:
|
||||
|
||||
```
|
||||
Δv = (2/π)·a·T = 0.6366 × 147.2 m/s² × 0.011 s = 1.03 m/s
|
||||
```
|
||||
|
||||
| Rebound | Impact velocity | **Drop height** |
|
||||
|---|---|---|
|
||||
| 0 % | 1.03 m/s | 54 mm |
|
||||
| 30 % | 0.79 m/s | 32 mm |
|
||||
| 50 % | 0.69 m/s | 24 mm |
|
||||
|
||||
**Pad stiffness required: k = m·π²/T² = 1.31 MN/m**, i.e. 326 kN/m per puck
|
||||
across four. With Shore 60A rubber (E ≈ 4 MPa) 25 mm thick that is a **51 mm
|
||||
diameter puck**; Shore 70A (E ≈ 6 MPa) gives 42 mm. Buy an assortment of
|
||||
30/40/50 mm pucks in 60A and 70A and tune empirically against the accelerometer
|
||||
trace — this takes an afternoon.
|
||||
|
||||
> ⚠ The puck diameters above use k = E·A/L, which **under-estimates** real rubber
|
||||
> in compression: a bonded or friction-constrained puck is stiffened by its shape
|
||||
> factor, often by 2–4× for a squat geometry. Expect the pucks you actually need
|
||||
> to be **smaller or softer** than the table says. Treat those numbers as the
|
||||
> starting point of the tuning, not the answer.
|
||||
|
||||
Other severities for reference, same rig:
|
||||
|
||||
| Severity | Δv | Drop height (30 % rebound) | Pad k |
|
||||
|---|---|---|---|
|
||||
| 3 g / 30 ms (gentle, ≈ IEC 61373 Cat 1) | 0.56 m/s | 10 mm | 0.18 MN/m |
|
||||
| 15 g / 11 ms | 1.03 m/s | 32 mm | 1.31 MN/m |
|
||||
| 30 g / 11 ms | 2.06 m/s | 128 mm | 1.31 MN/m |
|
||||
| 30 g / 18 ms | 3.37 m/s | 343 mm | 0.49 MN/m |
|
||||
|
||||
Everything up to 30 g / 11 ms is comfortably inside a 300 mm shaft travel.
|
||||
Lift by hand with a lever and a detent for v1; motorise with a cam and a
|
||||
geared motor later if we want 1000-shock endurance runs.
|
||||
|
||||
**Axes:** rotate the DUT in its bracket rather than tipping the table. Three
|
||||
orthogonal orientations, N shocks each, per §2's measured duty cycle
|
||||
(fallback: 100 per direction).
|
||||
|
||||
---
|
||||
|
||||
## 7. Instrumentation
|
||||
|
||||
| What | Sensor | Why |
|
||||
|---|---|---|
|
||||
| Rig control/reference vibration | **ADXL355** (±8 g, 20-bit, SPI, low noise) on the platen | This is the sensor the report quotes. Mount it rigidly to the platen, not the box |
|
||||
| Shock peaks | **ADXL1002** (±50 g, 11 kHz BW, analog) | The ADXL355 will clip and alias on a 15 g / 11 ms pulse |
|
||||
| DUT-internal vibration | **U26 SC7A20** already on the v6 board | Free, and it cross-checks the field log from §2 with the rig — that correlation *is* the validation of the rig |
|
||||
| Temperature | 3 × type-K + the DUT's own `tair` (SHT40), `tntc`, `tbat` (TMP102) | Already in `$PHLG` |
|
||||
| DUT health | `$PHLG` telemetry over RS-232/RS-485 out of the chamber, logged continuously | Pass/fail evidence |
|
||||
|
||||
Logger: Teensy 4.0 or Pi Pico → SD at ≥2 kHz for the shock channel. Keep the
|
||||
logger *outside* the hot box.
|
||||
|
||||
**Pass/fail is defined on `$PHLG`, not on "it still turns on":** no GNSS fix
|
||||
loss > N s, no modem re-registration, no watchdog reset, no `cflt` charge fault
|
||||
that does not clear, no drift in `C/N₀` beyond X dB. Define the thresholds
|
||||
before the first run, not after.
|
||||
|
||||
---
|
||||
|
||||
## 8. BOM and budget (MXN, Sep-2026 street prices, ±25 %)
|
||||
|
||||
### Thermal stage
|
||||
|
||||
| Item | Qty | Est. |
|
||||
|---|---|---|
|
||||
| Aluminium sheet 3 mm + angle frame (liner + door) | — | 1,800 |
|
||||
| Rockwool board 50 mm, ~2 m² | — | 700 |
|
||||
| PTC fan-heater module 220 V 300 W | 2 | 700 |
|
||||
| High-temp circulation fan, metal blade | 1 | 400 |
|
||||
| PID kit: REX-C100 + SSR-40DA + type-K probe | 1 | 450 |
|
||||
| KSD301 85 °C cutout + mechanical contactor + 10 A breaker | 1 | 500 |
|
||||
| Spare type-K probes + 4-ch SD/USB temp logger | 1 | 600 |
|
||||
| Mains wiring, control enclosure, IEC inlet, silicone glands, oven gasket, latches | — | 900 |
|
||||
| **Subtotal** | | **6,050** |
|
||||
|
||||
### Vibration stage
|
||||
|
||||
| Item | Qty | Est. |
|
||||
|---|---|---|
|
||||
| Motovibrador 3-ph 0.18–0.25 kW, adjustable weights | 1 | 3,500 |
|
||||
| VFD 0.75 kW 220 V 1-ph → 3-ph | 1 | 1,900 |
|
||||
| Aluminium platen 12 mm, 400×400 mm | 1 | 2,200 |
|
||||
| Compression springs ~4 N/mm + guide cups | 4 | 600 |
|
||||
| Steel base frame (PTR 2", welded, floor-anchored) | 1 | 1,400 |
|
||||
| Guarding: steel mesh + polycarbonate window, E-stop | 1 | 1,200 |
|
||||
| Fasteners, Loctite 243, service-loop cable + strain relief | — | 500 |
|
||||
| **Subtotal** | | **11,300** |
|
||||
|
||||
### Impact stage
|
||||
|
||||
| Item | Qty | Est. |
|
||||
|---|---|---|
|
||||
| Linear shaft 16 mm × 300 mm + SC16UU bearings | 4 + 8 | 1,600 |
|
||||
| Lift lever, detent, release latch, shaft supports | 1 | 900 |
|
||||
| Rubber puck assortment, Shore 60A/70A, 30/40/50 mm | — | 500 |
|
||||
| **Subtotal** | | **3,000** |
|
||||
|
||||
### Instrumentation
|
||||
|
||||
| Item | Qty | Est. |
|
||||
|---|---|---|
|
||||
| ADXL355 breakout | 1 | 1,100 |
|
||||
| ADXL1002 breakout (shock channel) | 1 | 700 |
|
||||
| Teensy 4.0 + SD + protoboard + wiring | 1 | 800 |
|
||||
| **Subtotal** | | **2,600** |
|
||||
|
||||
### Total
|
||||
|
||||
| | MXN |
|
||||
|---|---|
|
||||
| Thermal | 6,050 |
|
||||
| Vibration | 11,300 |
|
||||
| Impact | 3,000 |
|
||||
| Instrumentation | 2,600 |
|
||||
| Subtotal | **22,950** |
|
||||
| Contingency 15 % | 3,450 |
|
||||
| **Total** | **≈ 26,400** |
|
||||
|
||||
### Stripped variant — ≈ $14,000 MXN
|
||||
|
||||
Drop to: 110 V 100 W vibrating motor on a dimmer instead of motovibrador + VFD
|
||||
(−3,500), hand-built wooden outer box with rockwool and alu liner instead of a
|
||||
folded alu shell (−1,000), 10 mm steel plate instead of alu tooling plate
|
||||
(−1,200), ADXL345 only, no ADXL1002 (−1,400), no formal guarding beyond a
|
||||
polycarbonate lid and E-stop (−700), reduced contingency.
|
||||
|
||||
**Do not strip:** the independent thermal cutout + contactor, the E-stop, the
|
||||
reference accelerometer, or the guarding around the rotating mass. Those four
|
||||
are the difference between a test rig and an incident.
|
||||
|
||||
---
|
||||
|
||||
## 9. Build sequence
|
||||
|
||||
| Week | Deliverable | Gate |
|
||||
|---|---|---|
|
||||
| 0 | Finish the `$PHLG` vibration fields + SC7A20 driver (already an open v6 item); flash one unit; ship to ICAVE | Firmware logs XYZ bursts to SD/telemetry |
|
||||
| 1 | Field log running on a live crane, 5–7 days. **In parallel:** build the thermal chamber | Chamber holds 60.0 ± 1.0 °C for 4 h with the door shut and the cutout proven by test |
|
||||
| 2 | Derive the real PSD + shock spectrum from the field log → freeze the rig target profile. Build platen, base frame, suspension, guarding | Bare platen sweeps 5–80 Hz, accelerometer trace logged and sane |
|
||||
| 3 | Drop-table conversion; tune pucks to hit 15 g / 11 ms ± 20 % | Three consecutive pulses within tolerance |
|
||||
| 4 | First full campaign: 2 units, one control | Report |
|
||||
|
||||
**Do not skip the "bare platen" and "empty chamber" commissioning runs.** Half
|
||||
the problems in a rig like this are the rig's own resonances and hot spots, and
|
||||
you want them characterised before a DUT is in there confusing the picture.
|
||||
|
||||
---
|
||||
|
||||
## 10. Test campaign v1 (proposed)
|
||||
|
||||
Two units. **DUT-A gets everything; DUT-B is the control and only gets the
|
||||
thermal soak** — so that if something breaks, we know whether it was heat or
|
||||
mechanics. Both fully instrumented, both powered and transmitting throughout.
|
||||
|
||||
1. **Baseline** — 1 h at 25 °C, full `$PHLG` logging, GNSS fix + NTRIP + LTE.
|
||||
Record C/N₀, fix type, modem RSSI, `tbat`, `cflt`. This is the reference.
|
||||
2. **Thermal soak** — ramp to 60 °C over 30 min, hold 8 h, with LTE TX active.
|
||||
Then off, cool to ambient, repeat ×3 (3 cycles). Watch for: charge stall,
|
||||
modem re-registration storms, fan policy behaviour, watchdog resets,
|
||||
GNSS C/N₀ drift.
|
||||
3. **Hot vibration** — at 60 °C, bands A/B/C per §5, 10 sweep cycles each,
|
||||
in the Z axis first (crane deck is dominantly vertical), then X, then Y.
|
||||
4. **Hot shock** — still at 60 °C, 100 half-sine pulses per direction ×
|
||||
3 orientations at the measured field severity (fallback 15 g / 11 ms).
|
||||
5. **Post-test** — repeat step 1 verbatim and diff against the baseline.
|
||||
Then open both units: visual + X-ray-by-eye of L22, U25, the 100 µF can,
|
||||
the GX16 entry and every connector; torque-check every screw; re-seat and
|
||||
re-measure the U.FL pigtails.
|
||||
|
||||
The diff in step 5 is the deliverable. "Nothing visibly broke" is not a result;
|
||||
"C/N₀ fell 2.1 dB and never recovered" is.
|
||||
|
||||
---
|
||||
|
||||
## 11. What this rig will NOT give you
|
||||
|
||||
State this plainly in any client-facing use of the results:
|
||||
|
||||
- **No traceability.** No calibrated accelerometer, no accredited lab, no
|
||||
certificate. Measurements are self-declared with a commercial MEMS reference.
|
||||
- **No closed-loop profile control.** The eccentric produces a measured
|
||||
displacement-limited sweep, not a controlled constant-g or PSD-shaped profile.
|
||||
- **Single-axis at a time**, not simultaneous triaxial.
|
||||
- **No cold, no humidity, no salt fog.** Ports are humid and saline; that is a
|
||||
separate (and for us, probably more relevant) test than cold.
|
||||
- **Not a substitute for certification** if a client ever demands IEC/EN
|
||||
compliance. It *is* a legitimate pre-screen that makes a paid lab run far more
|
||||
likely to pass first time — which is where it saves real money.
|
||||
|
||||
---
|
||||
|
||||
## 12. Open decisions for Leo
|
||||
|
||||
1. **Green-light step zero?** The field-log firmware is already on the v6
|
||||
open-items list; this just prioritises it. Nothing else should be bought
|
||||
until that log exists.
|
||||
2. **60 °C or 70 °C?** 60 °C is the stated target and matches port ambient +
|
||||
solar gain in the cabinet. But the nested ASA-in-ABS heat trap from the June
|
||||
RCA is what actually cooks the unit — if we have a measured internal cabinet
|
||||
temperature above 60 °C, the chamber should be sized for that instead. The
|
||||
design above has headroom to ~75 °C; the only change is insulation thickness.
|
||||
3. **Battery in or out for the first campaign?** Recommend: in, charger
|
||||
inhibited, pack instrumented. Revisit once the LFP decision lands.
|
||||
4. **Who builds it?** This is ~2 weeks of shop time competing with the
|
||||
ICAVE/Lázaro assembly finishing end of September. It may be worth waiting
|
||||
until after the first-week-of-October shipment.
|
||||
5. **Stripped ($14k) or full ($26k)?** Recommend full — the VFD and the
|
||||
adjustable-weight motovibrador are what make the vibration stage
|
||||
*repeatable*, and repeatability is the whole point.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- IEC 60068-2-6 — Test Fc, sinusoidal vibration
|
||||
- IEC 60068-2-27 — Test Ea, mechanical shock (half-sine severities)
|
||||
- IEC 60068-2-64 — Test Fh, broadband random vibration
|
||||
- IEC 61373 — Railway rolling stock, shock and vibration, Cat 1 Class B
|
||||
- Internal: `03-Architecture-Hardware/2026-09-09_Drogheda_Battery-Chemistry-HighTemp_v1.0.md`
|
||||
- Internal: `03-Architecture-Hardware/2026-08-24_Drogheda_EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`
|
||||
- Internal: `06-Testing-Validation/RCA-2026-06-field-failures.md`
|
||||
- Internal: `06-Testing-Validation/2026-08-18_Drogheda_FW-v3.0.3_Fan-Policy_Bench-Test_v1.0.md`
|
||||
528
docs/historia/halt-rig-impacto.md
Normal file
528
docs/historia/halt-rig-impacto.md
Normal file
@ -0,0 +1,528 @@
|
||||
---
|
||||
tags: [project-drogheda, testing, halt, environmental, hbox, rig]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO
|
||||
date: 2026-09-17
|
||||
status: APPROVED FOR BUILD
|
||||
version: 1.1
|
||||
supersedes: 2026-09-17_Drogheda_HALT-Rig_Thermal-Vibration-Impact_v1.0.md
|
||||
---
|
||||
|
||||
# DIY Environmental Test Rig — Thermal + Vibration + Impact for HBOX
|
||||
|
||||
!!! warning "Designators from before 22-Oct (T43)"
|
||||
This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the v6, U6 is now the A7670SA modem (was U19) and the LTC4040 is U17 (was U6). To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it.
|
||||
|
||||
## 🔒 Decisions locked (Leo, 17-Sep)
|
||||
|
||||
| # | Decision | Consequence |
|
||||
|---|---|---|
|
||||
| D1 | **Chamber designed for 75 °C, campaigns run at 60 °C** | 75 mm rockwool (not 50), 900 W heaters (not 600), 100 °C cutout (not 85), silicone-insulated internal wiring throughout |
|
||||
| D2 | **Full vibration drive: motovibrador + VFD** | 3-ph 0.18–0.25 kW with adjustable counterweights, 0.75 kW VFD 1-ph→3-ph. Banded sweeps are repeatable run-to-run |
|
||||
| D3 | **Shop time is available** | Build starts now, in parallel with the field-log firmware. 4-week plan in §9 |
|
||||
| D4 | **Architecture changed from v1.0 — see §3** | D1 forced it. The hot box is now **fixed**; a light fixture moves *inside* it |
|
||||
|
||||
---
|
||||
|
||||
## ⚠ What changed from v1.0, and why it matters
|
||||
|
||||
v1.0 had the whole hot box riding on the shaker. Running the 75 mm insulation
|
||||
required by **D1** through that geometry gives:
|
||||
|
||||
```
|
||||
rockwool shell (0.55³ − 0.40³ = 102 L @ 100 kg/m³) 10.2 kg
|
||||
3 mm alu liner, 0.4 m cube 7.8 kg
|
||||
12 mm alu platen 400×400 5.2 kg
|
||||
outer skin 8.5 kg
|
||||
2 DUTs + heaters + fixture 8.0 kg
|
||||
TOTAL 39.7 kg ← was 16 kg
|
||||
```
|
||||
|
||||
**40 kg of moving mass breaks the mechanical stage.** The shock pads would need
|
||||
3.24 MN/m (810 kN/m each across four), and the eccentric would need ~2.5× the
|
||||
counterweight to reach the same g. We would be spending the entire vibration
|
||||
budget shaking insulation.
|
||||
|
||||
**The fix — decouple.** Hot box fixed and heavy; a **light aluminium fixture
|
||||
moves inside it**, driven from below by a rod through a silicone bellows in the
|
||||
chamber floor. The motovibrador, the VFD and every bearing stay outside in cool
|
||||
air, which is where they belong anyway.
|
||||
|
||||
```
|
||||
new moving mass
|
||||
15 mm alu fixture plate 300×300 3.6 kg
|
||||
20 mm steel drive rod, 350 mm 0.9 kg
|
||||
2 HBOX DUTs + brackets + accel 5.8 kg
|
||||
TOTAL 10.3 kg ← better than v1.0's 16 kg
|
||||
```
|
||||
|
||||
Every mechanical number in §5 and §6 is smaller and cheaper as a result. This
|
||||
change makes the rig *more* capable, not less. It also removes the v1.0 problems
|
||||
of mains heaters and a PID on a shaking platform.
|
||||
|
||||
---
|
||||
|
||||
## 1. What we are actually trying to catch
|
||||
|
||||
Unchanged from v1.0. Summary: most of our known HBOX failures are **thermal**,
|
||||
not mechanical (LTC4040 hot-pause at ≈ +42 °C with `RNTB` = 10 k; the 10-Ago
|
||||
modem boot-loop; the June nested-cabinet heat trap). Vibration and impact are
|
||||
insurance for the 250-unit rollout, where a 1 % mechanical failure rate is
|
||||
two or three truck rolls to Veracruz.
|
||||
|
||||
Consequence for the build order: **the chamber is useful on day one and is built
|
||||
first.** The mechanical stage follows in week 2–3.
|
||||
|
||||
---
|
||||
|
||||
## 2. Step zero: measure the crane, don't guess it
|
||||
|
||||
**Still the highest-value action in this document, and it costs nothing.**
|
||||
|
||||
EinarBoard v6 carries **U26 = SC7A20TR** (LIS3DH-compatible, I²C 0x18,
|
||||
INT1 → PB9). The `$PHLG` vibration fields are already an open firmware item in
|
||||
the v6 brief. Finish that item, flash one unit, mount it on a live crane at
|
||||
ICAVE for 5–7 working days, log raw XYZ at 400 Hz ODR in bursts (10 s every
|
||||
5 min, plus a triggered burst on INT1 over threshold).
|
||||
|
||||
That gives us the real PSD, the real shock spectrum, the real event rate per
|
||||
shift, and one defensible sentence for the client report:
|
||||
|
||||
> *"The rig reproduces the 95th-percentile measured field environment at ICAVE,
|
||||
> with an acceleration factor of N."*
|
||||
|
||||
That is worth more to Hutchison than "we ran IEC 60068-2-6."
|
||||
|
||||
**Reference severities, for calibration of expectations only:**
|
||||
|
||||
- **IEC 60068-2-6** (Fc, sine sweep) — generic industrial: a constant-displacement
|
||||
leg then a constant-acceleration leg, 1 oct/min. Toradex, e.g., runs
|
||||
10–55 Hz @ 0.54 mm and 10–300 Hz @ 39 m·s⁻².
|
||||
- **IEC 61373 Cat 1 Class B** — railway, equipment in a case on the car body.
|
||||
Closest analogue in spirit: low-level random 5–150 Hz + 5 h/axis long-life.
|
||||
- **IEC 60068-2-27** (Ea, shock) — half-sine 15 g/11 ms, 30 g/18 ms,
|
||||
30 g/11 ms, 50 g/20 ms; 200–1000 shocks per direction, 3 axes.
|
||||
|
||||
**Fallback if the field log slips:** build to **5–80 Hz, peaks to 2 g**, plus
|
||||
**15 g / 11 ms half-sine**. Those are the numbers sized below.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture (revised — D4)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ FIXED HOT BOX — 400×400×400 useful │
|
||||
│ 75 mm rockwool · 3 mm alu liner · 900 W PTC │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ HBOX A │ │ HBOX B │ │ DUTs on the SAME
|
||||
│ └────┬─────┘ └────┬─────┘ │ bracket the crane uses
|
||||
│ ╔═════╧═════════════════╧═════╗ │
|
||||
│ ║ 15 mm alu FIXTURE 300×300 ║ ← ADXL355│ MOVING: 10.3 kg
|
||||
│ ╚═══════════╤═════════════════╝ │
|
||||
│ [spring] │ [spring] │ 4× 2.5 N/mm, fn ≈ 5 Hz
|
||||
│═════════════════╪══════════════════════════════│ chamber floor
|
||||
│ ▓ ← silicone bellows, 50 mm bore
|
||||
└─────────────────╪────────────────────────────┘
|
||||
│ 20 mm drive rod
|
||||
┌──────┴──────┐
|
||||
│ ECCENTRIC │ motovibrador + VFD, OUTSIDE, in cool air
|
||||
│ 0.18 kW │ adjustable counterweights
|
||||
└─────────────┘
|
||||
steel base frame, floor-anchored
|
||||
```
|
||||
|
||||
**Vibration mode:** fixture floats on 4 soft springs (fn ≈ 5 Hz); the rod
|
||||
transmits the eccentric's force. Everything hot is stationary; everything
|
||||
rotating is cool.
|
||||
|
||||
**Shock mode:** springs removed; the rod lifts the fixture a set height on two
|
||||
guide shafts and releases it onto elastomer pucks bolted to the chamber floor.
|
||||
Same fixture, same DUT mounting, same accelerometer. **Mode change ≈ 20 min.**
|
||||
|
||||
**Axes:** rotate the DUT in its bracket, not the rig. Three orthogonal
|
||||
orientations.
|
||||
|
||||
**Do not** attempt vibration and shock simultaneously — the pad stiffness
|
||||
requirements are in direct conflict (2.5 N/mm vs 210 kN/m, a factor of ~80).
|
||||
|
||||
### Service penetrations
|
||||
|
||||
- Drive rod: 50 mm bore, **silicone bellows boot** (200 °C rated, trivially
|
||||
available). Conduction through the bellows wall is <0.1 W; budget 5–10 W for
|
||||
annulus convection. Irrelevant against 900 W.
|
||||
- DUT harness (24 V, RS-232/RS-485, thermocouples, accelerometer): silicone
|
||||
gland plate on the **rear** face, with a slack service loop inside so the
|
||||
moving fixture doesn't tension the cables. **This is the #1 thing that breaks
|
||||
on rigs like this** — give it 150 mm of slack and anchor both ends.
|
||||
|
||||
---
|
||||
|
||||
## 4. Thermal stage — 75 °C capable, 60 °C operating
|
||||
|
||||
### Sizing (D1)
|
||||
|
||||
Steady-state conduction, 75 mm rockwool (k ≈ 0.040 W·m⁻¹·K⁻¹), 0.40 m inner /
|
||||
0.55 m outer, mean area 1.354 m²:
|
||||
|
||||
| Setpoint | Ambient | Conduction | ×3 for leakage, bridges, penetrations |
|
||||
|---|---|---|---|
|
||||
| 60 °C | 15 °C | 32.5 W | **97 W** |
|
||||
| 75 °C | 15 °C | 43.3 W | **130 W** |
|
||||
| 75 °C | 25 °C | 36.1 W | 108 W |
|
||||
|
||||
Note the elegance of D1: 75 mm of insulation at 75 °C loses the same as 50 mm at
|
||||
60 °C. The extra insulation pays for the extra ΔT exactly.
|
||||
|
||||
**Pull-up dominates the sizing.** 10 kg alu fixture + hardware + 5 kg DUT:
|
||||
|
||||
| Target | Energy | @ 600 W | @ **900 W** | @ 1200 W |
|
||||
|---|---|---|---|---|
|
||||
| 60 °C (ΔT 35 K) | 455 kJ | 14 min | **9 min** | 7 min |
|
||||
| 75 °C (ΔT 50 K) | 650 kJ | 20 min | **13 min** | 10 min |
|
||||
|
||||
**Specify 900 W** = 3 × 300 W PTC fan-heater modules, PID-controlled together.
|
||||
The air charge is 3.2 kJ — negligible against 650 kJ of metal, so don't bother
|
||||
sizing for it.
|
||||
|
||||
PTC elements remain the right choice: self-limiting, sealed modules with the fan
|
||||
integrated, cannot run away if the fan stops.
|
||||
|
||||
### Control and safety — two independent layers, non-negotiable
|
||||
|
||||
1. **Control:** REX-C100 PID → SSR-40DA → the three heaters. Type-K thermocouple
|
||||
in the **return air stream**, not touching a DUT.
|
||||
2. **Independent over-temperature cutout: KSD301 100 °C NC thermostat in series
|
||||
with the coil of a mechanical contactor** feeding the heaters.
|
||||
The dominant SSR failure mode is **shorted on**, and the PID then has zero
|
||||
authority. The contactor is the only thing between a stuck SSR and a fire
|
||||
during an overnight soak. **Prove it by test at commissioning** — jumper the
|
||||
SSR closed deliberately and confirm the contactor drops out.
|
||||
3. 10 A breaker, metal control enclosure, PE bonded to the alu liner and to the
|
||||
steel frame.
|
||||
|
||||
### D1 material consequences
|
||||
|
||||
- **All internal wiring silicone-insulated**, not PVC. PVC at 75 °C in still air
|
||||
next to a 300 W element will go brittle and then conductive.
|
||||
- Door seal: **silicone** oven gasket (rated 200 °C+), not EPDM or foam tape.
|
||||
- **Rockwool, not EPS/XPS/polyiso.** EPS is rated ~75 °C — i.e. exactly at our
|
||||
design point — and will soften and outgas. Rockwool is non-combustible and
|
||||
costs the same.
|
||||
- 3 mm aluminium liner as both fire barrier and fibre containment.
|
||||
- The HBOX ASA case softens ~95–100 °C, so 75 °C is safe for the enclosure
|
||||
itself. The cells are the constraint, not the plastic.
|
||||
|
||||
### Independent temperature logging
|
||||
|
||||
Three spare type-K probes (one per DUT surface, one in air) → 4-channel SD/USB
|
||||
logger, **outside** the box. The report needs a measured trace, not "the PID
|
||||
display said 60."
|
||||
|
||||
### 🔴 Battery hazard
|
||||
|
||||
The HBOX contains a lithium pack. 60 °C ambient + self-heating + LTE TX bursts
|
||||
puts cells above their charge window, and per `Battery-Chemistry-HighTemp v1.0`
|
||||
the LTC4040 already hot-pauses at ≈ +42 °C with `RNTB` = 10 k.
|
||||
|
||||
- First soaks: pack fitted, **charger inhibited**, thermocouple on the pack.
|
||||
- **Never leave the first soak of a new configuration unattended.**
|
||||
- Class-D extinguisher or dry sand + welding blanket at the rig. **Not CO₂.**
|
||||
- **Do not run the 75 °C capability with a Li-ion pack installed.** 75 °C is for
|
||||
bare-board and enclosure work, or for LFP once that decision lands.
|
||||
- Useful side effect: this chamber is the cheapest way to prove the
|
||||
`RNTB` → 5.6 k + high-temp LFP decision before it enters the 30-unit BOM.
|
||||
|
||||
---
|
||||
|
||||
## 5. Vibration stage (re-sized for M = 10.3 kg)
|
||||
|
||||
### Suspension
|
||||
|
||||
| fn | k total | per spring (×4) | static deflection |
|
||||
|---|---|---|---|
|
||||
| 4 Hz | 6.51 kN/m | 1.63 N/mm | 15.5 mm |
|
||||
| **5 Hz** | **10.17 kN/m** | **2.54 N/mm** | **9.9 mm** |
|
||||
| 6 Hz | 14.65 kN/m | 3.66 N/mm | 6.9 mm |
|
||||
|
||||
**Specify 4 × 2.5 N/mm compression springs**, fn ≈ 5 Hz, ~10 mm static sag.
|
||||
Band A starts near this resonance — **do not dwell at 5–7 Hz** until the
|
||||
accelerometer trace has been watched.
|
||||
|
||||
### The honest limitation (unchanged in principle, better in magnitude)
|
||||
|
||||
A fixed eccentric on a soft-mounted mass gives **constant displacement**, so
|
||||
acceleration rises as **f²**:
|
||||
|
||||
| m_e·e | x peak | 5 Hz | 10 Hz | 20 Hz | 30 Hz | 45 Hz | 60 Hz | 80 Hz |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 1 g·m | 0.10 mm | 0.01 | 0.04 | 0.16 | 0.35 | 0.79 | 1.41 | 2.50 |
|
||||
| 2 g·m | 0.19 mm | 0.02 | 0.08 | 0.31 | 0.70 | 1.58 | 2.81 | 5.00 |
|
||||
| 5 g·m | 0.49 mm | 0.05 | 0.20 | 0.78 | 1.76 | 3.95 | 7.03 | 12.5 |
|
||||
| 10 g·m | 0.97 mm | 0.10 | 0.39 | 1.56 | 3.51 | 7.91 | 14.1 | 25.0 |
|
||||
| 20 g·m | 1.94 mm | 0.20 | 0.78 | 3.12 | 7.03 | 15.8 | 28.1 | 50.0 |
|
||||
|
||||
(values in g)
|
||||
|
||||
You cannot hold a flat level across 5–80 Hz with one weight setting. Anyone
|
||||
claiming an eccentric rig "runs IEC 60068-2-6" either has closed-loop amplitude
|
||||
control or is not measuring.
|
||||
|
||||
### Banded sweep plan — **this is the operating procedure**
|
||||
|
||||
| Band | Freq | Motor RPM | Counterweight m_e·e | as mass @ 50 mm | x peak | a at bottom → top |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **A** | 5–15 Hz | 300–900 | 12.5 g·m | 250 g | 1.22 mm | 0.12 → 1.10 g |
|
||||
| **B** | 15–40 Hz | 900–2400 | 3.2 g·m | 64 g | 0.31 mm | 0.28 → 2.00 g |
|
||||
| **C** | 40–80 Hz | 2400–4800 | 1.4 g·m | 28 g | 0.14 mm | 0.88 → 3.50 g |
|
||||
|
||||
Sweep each band at 1 octave/min, N cycles, in Z first (crane deck is dominantly
|
||||
vertical), then X, then Y. **The accelerometer logs what actually happened** and
|
||||
the report states measured levels per band. That is honest and still perfectly
|
||||
good at finding cracked joints.
|
||||
|
||||
> ⚠ **Band C overspeeds a 60 Hz motor to 80 Hz on the VFD.** Check the
|
||||
> motovibrador's rated maximum centrifugal force and bearing speed first, and
|
||||
> de-rate the weights accordingly — bearing load scales as f² too. If the
|
||||
> datasheet forbids it, stop band C at 60 Hz and add the BLDC option (§5.3).
|
||||
|
||||
### 5.3 Optional reach above 80 Hz (~$1,200 MXN)
|
||||
|
||||
RC outrunner BLDC + 80 A ESC + servo tester, eccentric disc on the shaft, spins
|
||||
past 10 000 rpm. Crude, but it is the only cheap way to excite **PCB-level
|
||||
resonances (typically 150–300 Hz)** — which is where L22, U25 and the 100 µF/50 V
|
||||
can would crack if they are going to. Worth adding if the field log shows
|
||||
meaningful energy above 80 Hz, or if a post-test teardown finds joint damage.
|
||||
|
||||
### 5.4 Phase 2 (~$6k MXN) — the woofer shaker
|
||||
|
||||
15" pro woofer, voice coil coupled to a light fixture, class-D amp from a PC
|
||||
sound card, accelerometer as the control sensor → genuine **closed-loop swept
|
||||
sine and random, 20–200 Hz**. ~100 N of force, so ~2 g on a 5 kg bare-PCBA
|
||||
fixture. The only sub-$10k route to a truly controlled profile. Not needed for
|
||||
v1; revisit after the first campaign.
|
||||
|
||||
---
|
||||
|
||||
## 6. Impact / shock stage (re-sized for M = 10.3 kg)
|
||||
|
||||
Guided drop inside the chamber. The fixture is lifted on the drive rod plus two
|
||||
guide shafts and released onto elastomer pucks on the chamber floor. Peak g and
|
||||
duration are set by drop height and puck stiffness, and **both are measured**.
|
||||
|
||||
| Severity | Δv | Drop, 30 % rebound | Drop, 0 % rebound | Pad k total | per pad (×4) |
|
||||
|---|---|---|---|---|---|
|
||||
| 3 g / 30 ms | 0.56 m/s | 10 mm | 16 mm | 0.113 MN/m | 28 kN/m |
|
||||
| **15 g / 11 ms** | **1.03 m/s** | **32 mm** | **54 mm** | **0.841 MN/m** | **210 kN/m** |
|
||||
| 30 g / 11 ms | 2.06 m/s | 128 mm | 216 mm | 0.841 MN/m | 210 kN/m |
|
||||
| 30 g / 18 ms | 3.37 m/s | 343 mm | 580 mm | 0.314 MN/m | 79 kN/m |
|
||||
|
||||
Everything to 30 g / 11 ms fits inside 250 mm of shaft travel, which fits inside
|
||||
the 400 mm chamber. 30 g / 18 ms does not — that one needs the fixture outside
|
||||
the box, cold. Accept that limit.
|
||||
|
||||
**Puck starting sizes** for 15 g / 11 ms, from k = E·A/L, 25 mm thick:
|
||||
Shore 60A ≈ 41 mm dia, Shore 70A ≈ 33 mm dia.
|
||||
|
||||
> ⚠ k = E·A/L **under-estimates** real rubber in compression: a constrained
|
||||
> squat puck is stiffened by its shape factor, often 2–4×. The pucks you
|
||||
> actually need will be **smaller or softer** than the table says. Buy an
|
||||
> assortment (Shore 60A and 70A, 20/25/30/40/50 mm) and tune against the
|
||||
> accelerometer trace. Budget one afternoon.
|
||||
|
||||
Lift by hand with a lever and a detent for v1. Motorise with a cam later only if
|
||||
we want 1000-shock endurance runs.
|
||||
|
||||
---
|
||||
|
||||
## 7. Instrumentation
|
||||
|
||||
| What | Sensor | Notes |
|
||||
|---|---|---|
|
||||
| Rig reference vibration | **ADXL355** (±8 g, 20-bit, SPI) | Bolted to the fixture plate. This is the sensor the report quotes |
|
||||
| Shock peaks | **ADXL1002** (±50 g, 11 kHz BW, analog) | The ADXL355 will clip and alias on a 15 g / 11 ms pulse |
|
||||
| DUT-internal vibration | **U26 SC7A20**, already on the v6 board | Free. Correlating it against the field log **is** the validation of the rig |
|
||||
| Temperature | 3 × type-K + the DUT's own `tair` (SHT40), `tntc`, `tbat` (TMP102) | Already in `$PHLG` |
|
||||
| DUT health | `$PHLG` over RS-232/RS-485, logged continuously | Pass/fail evidence |
|
||||
|
||||
Logger: Teensy 4.0 or Pi Pico → SD, ≥2 kHz on the shock channel, **outside the
|
||||
hot box**. Sensor cables through the rear gland plate with the service loop.
|
||||
|
||||
**Define pass/fail on `$PHLG` before the first run, not after.** Suggested:
|
||||
no GNSS fix loss > N s · no modem re-registration · no watchdog reset ·
|
||||
no `cflt` that fails to clear · C/N₀ drift within X dB.
|
||||
|
||||
---
|
||||
|
||||
## 8. BOM (MXN, Sep-2026 street prices, ±25 %) — D1 + D2 configuration
|
||||
|
||||
### Thermal stage — 900 W, 75 °C capable
|
||||
|
||||
| Item | Qty | Est. |
|
||||
|---|---|---|
|
||||
| Aluminium sheet 3 mm, liner + door (0.4 m cube) | — | 2,000 |
|
||||
| Rockwool board **75 mm**, ~2.5 m² | — | 1,000 |
|
||||
| Outer skin: 18 mm plywood or 1.2 mm steel, frame | — | 900 |
|
||||
| PTC fan-heater module 220 V **300 W** | **3** | 1,050 |
|
||||
| High-temp circulation fan, metal blade | 1 | 400 |
|
||||
| PID kit: REX-C100 + SSR-40DA + type-K probe | 1 | 450 |
|
||||
| **KSD301 100 °C NC cutout + mechanical contactor** + 10 A breaker | 1 | 550 |
|
||||
| 3 × spare type-K probes + 4-ch SD/USB temp logger | 1 | 700 |
|
||||
| **Silicone-insulated wire** (internal), silicone glands, gland plate | — | 600 |
|
||||
| **Silicone oven gasket**, piano hinge, over-centre latches | — | 500 |
|
||||
| **Silicone bellows boot, 50 mm bore** + floor flange | 1 | 450 |
|
||||
| Control enclosure, IEC inlet, indicators, E-stop | — | 800 |
|
||||
| **Subtotal** | | **9,800** |
|
||||
|
||||
### Vibration stage (D2 — full)
|
||||
|
||||
| Item | Qty | Est. |
|
||||
|---|---|---|
|
||||
| **Motovibrador 3-ph 0.18–0.25 kW, adjustable counterweights** | 1 | 3,500 |
|
||||
| **VFD 0.75 kW, 220 V 1-ph in → 3-ph out** | 1 | 1,900 |
|
||||
| Aluminium fixture plate **15 mm, 300×300** | 1 | 1,400 |
|
||||
| Drive rod 20 mm steel + couplings + rod-end bearing | — | 900 |
|
||||
| Compression springs **2.5 N/mm** + guide cups | 4 | 500 |
|
||||
| Steel base frame (2" PTR, welded, floor-anchored) | 1 | 1,600 |
|
||||
| Guarding: steel mesh + polycarbonate window, E-stop | 1 | 1,200 |
|
||||
| Fasteners, Loctite 243, DUT brackets (crane-identical) | — | 700 |
|
||||
| **Subtotal** | | **11,700** |
|
||||
|
||||
### Impact stage
|
||||
|
||||
| Item | Qty | Est. |
|
||||
|---|---|---|
|
||||
| Guide shaft 16 mm × 300 mm + SC16UU bearings | 2 + 4 | 900 |
|
||||
| Lift lever, detent, release latch, shaft supports | 1 | 900 |
|
||||
| Rubber puck assortment, Shore 60A/70A, 20/25/30/40/50 mm | — | 600 |
|
||||
| **Subtotal** | | **2,400** |
|
||||
|
||||
### Instrumentation
|
||||
|
||||
| Item | Qty | Est. |
|
||||
|---|---|---|
|
||||
| ADXL355 breakout | 1 | 1,100 |
|
||||
| ADXL1002 breakout (shock channel) | 1 | 700 |
|
||||
| Teensy 4.0 + SD + protoboard + silicone wiring | 1 | 900 |
|
||||
| **Subtotal** | | **2,700** |
|
||||
|
||||
### Total
|
||||
|
||||
| | MXN |
|
||||
|---|---|
|
||||
| Thermal | 9,800 |
|
||||
| **Safety (was missing a cost line in v1.0 — corrected)** | **1,100** |
|
||||
| Vibration | 11,700 |
|
||||
| Impact | 2,400 |
|
||||
| Instrumentation | 2,700 |
|
||||
| Subtotal | **27,700** |
|
||||
| Contingency 15 % | 4,155 |
|
||||
| **TOTAL** | **≈ 31,855** |
|
||||
|
||||
**Authoritative costing is now the BOM workbook**, not this table:
|
||||
`2026-09-17_Drogheda_HALT-Rig_BOM_v1.0.xlsx`. The v1.0 draft understated the
|
||||
thermal stage by $400 (arithmetic) and carried the safety kit at zero.
|
||||
|
||||
**$1,855 above the $30k top of the agreed band.** Three ways to come back inside:
|
||||
|
||||
- Drop the ADXL1002 and accept clipped shock peaks on the ADXL355 (−700). Not
|
||||
recommended; the shock channel is the one measurement we cannot fake.
|
||||
- Steel fixture plate instead of aluminium (−700). Adds 6 kg of moving mass and
|
||||
undoes part of §3. **Not recommended.**
|
||||
- Build the thermal stage now and the mechanical stage in October from the next
|
||||
budget cycle. **This is the recommended way to stage it** — the chamber is
|
||||
useful alone and is the part that addresses our actual known failures.
|
||||
|
||||
---
|
||||
|
||||
## 9. Build sequence (shop time available — D3)
|
||||
|
||||
| Week | Shop | Firmware / field | Gate |
|
||||
|---|---|---|---|
|
||||
| **1** (22–26 Sep) | Cut and fold the liner, build the shell, mount heaters + circulation fan, wire PID + SSR + **contactor and cutout** | Finish `$PHLG` vibration fields + SC7A20 driver; flash one unit | **G1:** empty chamber holds 60.0 ± 1.0 °C for 4 h; **cutout proven by deliberate SSR-short test** |
|
||||
| **2** (29 Sep–3 Oct) | Base frame, fixture plate, drive rod, bellows, springs, guarding. *(Note: ICAVE/Lázaro shipment lands this week — expect contention)* | Unit mounted on a live crane at ICAVE; logging | **G2:** bare fixture sweeps 5–80 Hz, accelerometer trace logged and sane; no frame resonance in band |
|
||||
| **3** (6–10 Oct) | Drop conversion: guide shafts, lift lever, puck tuning | Pull the field log; derive PSD + shock spectrum; **freeze the rig target profile** | **G3:** three consecutive pulses within ±20 % of target; rig profile signed off |
|
||||
| **4** (13–17 Oct) | Run campaign v1 (§10) | — | **G4:** report |
|
||||
|
||||
**Commissioning runs that must not be skipped:**
|
||||
|
||||
- **Empty chamber, 4 h at 60 °C** — map the hot spots with the three spare
|
||||
probes before a DUT confuses the picture. Then repeat once at 75 °C.
|
||||
- **Bare fixture sweep, all three bands** — characterise the rig's own
|
||||
resonances. If the base frame or the guarding rings at 30 Hz, you will spend
|
||||
the campaign chasing it.
|
||||
- **Deliberate SSR-short test** — see G1. Do it with nothing in the box.
|
||||
|
||||
---
|
||||
|
||||
## 10. Campaign v1
|
||||
|
||||
**DUT-A gets everything. DUT-B is the control and gets only the thermal soak** —
|
||||
so a failure tells us heat or mechanics, not both. Both fully instrumented,
|
||||
powered and transmitting throughout.
|
||||
|
||||
1. **Baseline** — 1 h at 25 °C, full `$PHLG`, GNSS + NTRIP + LTE. Record C/N₀,
|
||||
fix type, modem RSSI, `tbat`, `cflt`. This is the reference.
|
||||
2. **Thermal soak** — ramp to 60 °C over 30 min, hold 8 h with LTE TX active;
|
||||
cool to ambient; ×3 cycles. Watch: charge stall, modem re-registration
|
||||
storms, fan policy, watchdog resets, C/N₀ drift.
|
||||
3. **Hot vibration** — at 60 °C, bands A/B/C, 10 sweep cycles each, Z then X
|
||||
then Y.
|
||||
4. **Hot shock** — still at 60 °C, 100 half-sine pulses per direction ×
|
||||
3 orientations, at the measured field severity (fallback 15 g / 11 ms).
|
||||
5. **Post-test** — repeat step 1 verbatim and **diff against the baseline**.
|
||||
Then open both units: visual inspection of L22, U25, the 100 µF/50 V can,
|
||||
the GX16 entry and every connector; torque-check every screw; re-seat and
|
||||
re-measure both U.FL pigtails.
|
||||
|
||||
The diff in step 5 is the deliverable. *"Nothing visibly broke"* is not a result.
|
||||
*"C/N₀ fell 2.1 dB and never recovered"* is.
|
||||
|
||||
---
|
||||
|
||||
## 11. What this rig will NOT give you
|
||||
|
||||
State this plainly in any client-facing use of the results.
|
||||
|
||||
- **No traceability** — no calibrated accelerometer, no accredited lab, no
|
||||
certificate. Self-declared measurements with a commercial MEMS reference.
|
||||
- **No closed-loop profile control** — a measured, displacement-limited banded
|
||||
sweep, not a controlled constant-g or PSD-shaped profile.
|
||||
- **Single-axis at a time**, not simultaneous triaxial.
|
||||
- **No cold, no humidity, no salt fog.** Ports are humid and saline; for us that
|
||||
is probably a *more* relevant missing test than cold. Consider a salt-fog
|
||||
add-on after this rig proves out.
|
||||
- **Not a substitute for certification** if a client demands IEC/EN compliance.
|
||||
It *is* a legitimate pre-screen that makes a paid lab run far more likely to
|
||||
pass first time — which is where it saves real money.
|
||||
|
||||
---
|
||||
|
||||
## 12. Remaining open items
|
||||
|
||||
1. **Battery-in vs battery-out for campaign v1.** Recommendation: in, charger
|
||||
inhibited, pack instrumented. Revisit when the LFP decision lands.
|
||||
**Hard rule: no Li-ion pack in the box above 60 °C.**
|
||||
2. **Who owns the rig?** It needs a named owner for calibration checks (monthly
|
||||
accelerometer cross-check, thermocouple ice-point check) or it will drift and
|
||||
quietly stop being trustworthy.
|
||||
3. **Week-2 shop contention** with the ICAVE/Lázaro shipment. Flagged, not
|
||||
resolved.
|
||||
4. **BLDC option (§5.3)** — decide after the field log shows whether there is
|
||||
energy above 80 Hz.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- IEC 60068-2-6 — Test Fc, sinusoidal vibration
|
||||
- IEC 60068-2-27 — Test Ea, mechanical shock (half-sine severities)
|
||||
- IEC 60068-2-64 — Test Fh, broadband random vibration
|
||||
- IEC 61373 — Railway rolling stock, shock and vibration, Cat 1 Class B
|
||||
- `03-Architecture-Hardware/2026-09-09_Drogheda_Battery-Chemistry-HighTemp_v1.0.md`
|
||||
- `03-Architecture-Hardware/2026-08-24_Drogheda_EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`
|
||||
- `06-Testing-Validation/RCA-2026-06-field-failures.md`
|
||||
- `06-Testing-Validation/2026-08-18_Drogheda_FW-v3.0.3_Fan-Policy_Bench-Test_v1.0.md`
|
||||
- Companion: `2026-09-17_Drogheda_HALT-Rig_BOM_v1.0.xlsx` (**authoritative BOM** — buy from this)
|
||||
- Companion: `2026-09-17_Drogheda_HALT-Rig_Lista-Compras_ES_v1.0.md` (narrative purchasing notes)
|
||||
65
docs/historia/htracker-revision-500.md
Normal file
65
docs/historia/htracker-revision-500.md
Normal file
@ -0,0 +1,65 @@
|
||||
# htracker — revisión de código, rama `feature/test500unidades` @ `2cdcb5e` (18-sep-2026)
|
||||
|
||||
Archivos revisados: `web_monitor.py` (6 286 líneas), `retranslator.py` (1 466), `device_metadata.py`, `web_monitor_ctl.sh`,
|
||||
`device_config.json`. No revisados (no estaban en el set): `auth.py`, `gps_logger.py` — ver «Pendiente de verificar».
|
||||
Todos los hallazgos están verificados en el código (archivo:línea). Dueño del backend: Gibran.
|
||||
|
||||
## Resumen ejecutivo (ES)
|
||||
|
||||
**No es seguro seguir desplegando tal cual.** Tres cosas primero, en este orden:
|
||||
|
||||
1. **XSS almacenado sin autenticación por UDP (C1) y entre clientes (C2).** `cell_id` llega del `$GPTXT` sin validar y se
|
||||
inyecta con `innerHTML` en el panel de desconectadas, el mapa y los reportes; los nombres de unidad y de geocerca tampoco
|
||||
se escapan. Cualquiera en internet puede ejecutar JavaScript en el navegador del administrador con un datagrama a UDP 5010.
|
||||
Arreglo: `re.fullmatch(r'H\d{4}', cell_id)` en el retranslator + `esc()` en todos los sitios de inyección (una tarde).
|
||||
2. **Secretos y datos en git (H1)** — `.session_secret`, `users.json`, `web_monitor.py.bak`, `telemetry_history.db`:
|
||||
`git rm --cached`, `.gitignore`, rotar `.session_secret` (invalida sesiones), `git filter-repo`. Y limitar la edición de
|
||||
unidades/geocercas a las `cell_id` permitidas del usuario (H4).
|
||||
3. **El reenvío a Wialon bloquea por paquete (H2) y reporta «ok» sin leer la respuesta (H3).** Con 500 unidades a 1 Hz se
|
||||
descartan posiciones de forma sostenida mientras el tablero dice sano. Rehacer el forwarder (no esperar ACK síncrono,
|
||||
validar `#ASD#1`) **antes** del despliegue de 500.
|
||||
|
||||
Para las 15 unidades v5 de esta semana: C1 es el riesgo real hoy; H2/H3 no muerden a 30 unidades.
|
||||
|
||||
---
|
||||
## Detailed findings (EN, ranked)
|
||||
|
||||
### CRITICAL
|
||||
**C1. Unauthenticated stored XSS via UDP: `cell_id` is never validated and is rendered unescaped.** `retranslator.py:691-695` returns `parts[2]` of any `$GPTXT` datagram verbatim; it becomes a key in every state dict and is emitted in `/tmp/htracker_status.json`. `web_monitor.py:3780` (disconnected panel), `5555`, `5607` (map InfoWindow) interpolate it into `innerHTML` without `esc()`. After `DEVICE_TTL_S` (300 s) the unit lands in `disconnected` and every open dashboard renders it; persists in `htracker_device_state.json` across restarts. Fix: validate (`H\d{4}`), drop and count otherwise; `esc()` every interpolated field.
|
||||
|
||||
**C2. Authenticated cross-tenant stored XSS through unit names and geofence names.** `web_monitor.py:785-809` lets any logged-in user set `unit_name` for any cell_id (only `.strip()`); `952-960` same for geofence `name`. Rendered unescaped at `4963`, `5069`, `5155`, `5141`, `6115`, `6128`, `5556`, `5608`, `5653`. Fix: escape at every sink; restrict `unit_name` to `[\w \-\.]{1,40}`; gate `POST /api/units/{cell_id}` on `_user_allowed_cell_ids`.
|
||||
|
||||
### HIGH
|
||||
**H1. Secrets/data committed** (`.session_secret`, `users.json`, `web_monitor.py.bak`, `telemetry_history.db`). Session cookies forgeable, hashes crackable offline. `web_monitor_ctl.sh:38,68,69,98` hard-codes the public IP.
|
||||
**H2. Wialon forwarder head-of-line blocks per worker.** `retranslator.py:511-516`: `sendall` then blocking `recv` (15 s) per packet. Throughput per worker = 1/RTT; 64 workers ≈ 320–640 pkt/s; 500 units at 1 Hz = 500 pkt/s → sustained oldest-drop (`386-402`). One stalled socket freezes ~7 other units on that worker. Fix: write then drain ACKs non-blocking (`select` across the worker's sockets); add `queue_depth` stat.
|
||||
**H3. "acked" counted without checking the response.** `515-516`: `b''`, `#ASD#0`, `#ASD#-1` all count as ack → `wialon_state='ok'` while Wialon rejects. Fix: parse; only `#ASD#1`/`#AD#1` = ok; `b''` = disconnect.
|
||||
**H4. Cross-tenant writes by design** (`769-824`, "decisión de producto 2026-08-17") — with C2 this is privilege escalation. Gate on allowed cell_ids; vehicle-type icons full-access only.
|
||||
**H5. SVG uploads served same-origin from `/static` unauthenticated** (`372`, `611`) = stored XSS. Drop SVG or serve with `Content-Disposition: attachment` + CSP sandbox.
|
||||
|
||||
### MEDIUM
|
||||
**M1.** Units with no fix are invisible: `_parse_gga` returns `None` on quality 0 (`647`), `_RMC_RE` needs `A` (`217`); `NoFix` chip unreachable. Mark seen + `fix_mode='NoFix'`, skip only forward/odometer.
|
||||
**M2.** Hard speed gate ≥ 57.6 km/h (`1169-1173`) drops from Wialon *and* the local DB, not toggleable, not counted — the "healthy unit locked out" case of invariant #5. Make it `GW_MAX_SPEED_KMH`, count `speed_rejects`, keep `_log_to_sql`.
|
||||
**M3.** Unbounded state growth: `_cleanup_thread` (`1033-1071`) never prunes `device_first_connected`, `device_total_online_s`, `device_last_seen_ever`, `device_km_*`, `device_disconnected`; written every 5 s, reloaded on boot. Prune by `last_seen_ever` age (+ C1 validation).
|
||||
**M4.** `km_24h` resets at 00:00 UTC (`570, 612, 1197` use `utcnow()`); everything else uses `_TZ_CDMX`.
|
||||
**M5.** KM reset offset (`web_monitor.py:2596-2599`, `140-142`) corrupts `km_24h` on following days; store `reset_at` and apply only same local day.
|
||||
**M6.** Availability/disconnection reports bucket by UTC day (`1090-1093`); trips/CO2/track by local day → tabs disagree, "Hoy" misses 6 h.
|
||||
**M7.** SSE parses the whole status file per client every 2 s on the event loop (`2629-2640`); ~450 KB at 500 units × 20 dashboards = 10 parses/s stalling all handlers. One cached background reader; slim payload.
|
||||
**M8.** `web_monitor_ctl.sh restart` hangs while any dashboard is open (`/events` never completes; single SIGTERM at `79`; port-in-use at `43-48`). Add `--timeout-graceful-shutdown 5`, poll then `kill -9`.
|
||||
**M9.** Web monitor not under systemd, binds 0.0.0.0, cookie without `secure` (`2755-2758`). systemd unit, `--host 127.0.0.1`, `secure=True` behind TLS.
|
||||
**M10.** No `logging` in web_monitor; `except … pass`/`return []` at `298-301`, `313-314`, `1106-1107`, `1539-1540`, `338-339`, `2508-2509` → a DB permission/WAL problem shows as "0 % / no data" with HTTP 200.
|
||||
**M11.** CGNAT port reuse mis-attributes positions: addr→cell_id kept 300 s (`1026-1029`, `1353-1360`). Expire at ~2× the `$GPTXT` period; refuse GGA if the addr's last `$GPTXT` is older.
|
||||
|
||||
### LOW
|
||||
L1 reset link `?email=` unencoded (`2800`). L2 `/logout` is GET (`2766`). L3 `Content-Disposition` filename from unvalidated ids (`2264, 2304, 2354`). L4 Wialon packet timestamp = receipt time, not fix time (`332`). L5 CWD-relative paths (`133, 149`; `web_monitor.py:57, 78`) vs `__file__`-anchored ones; `PrivateTmp=yes` would hide `/tmp/htracker_status.json`. L6 one missed 5-min sample = "gap" (`1371, 1405`). L7 `telemetry_history.db` ≈ 30 MB/day at 500 units, no retention. L8 per-device INFO/ERROR lines (`1335, 449, 499, 519`) flood the collector at scale.
|
||||
|
||||
### Verified OK
|
||||
All SQL parametrized. Upload paths safe. Server-side f-string HTML escapes. CSRF mitigated by `samesite=lax`. Status JSON write atomic (tmp + rename). Per-cell_id **read** authorization enforced on every read endpoint. Anti-jump filter re-anchors after 3 rejects and forgets anchors on TTL — no permanent lockout from that filter. `INSERT OR IGNORE` + `UNIQUE(cell_id, captured_at)` dedupes.
|
||||
|
||||
### Must verify in `auth.py` / `gps_logger.py`
|
||||
HMAC session token compared with `hmac.compare_digest`; bcrypt/argon2/scrypt (not sha256); constant-time on unknown emails; reset tokens `secrets.token_urlsafe(32)`, single-use, ≤ 1 h, stored hashed; lockout persistence. Index on `gps_positions(device_cell_id, received_at)` — without it fleet CO2/trips views do 60 full scans per request.
|
||||
|
||||
### Improvements (not bugs)
|
||||
1. Cached SSE reader + delta payloads. 2. `/run/htracker/` for the status file, passed explicitly to both processes. 3. `web_monitor` under systemd (`Requires=htracker.service`), 127.0.0.1, `--proxy-headers`, graceful timeout. 4. `/healthz` (status-file age, DB readable, forwarder drop rate). 5. Retention for `telemetry_history.db`, pruning of `htracker_device_state.json`. 6. 429/503 + `Retry-After` on heavy report ranges. 7. Structured logs; ship only `[SUMMARY]` + WARN. 8. UDP replay test harness with a fake Wialon returning `#ASD#0` / closing / stalling.
|
||||
|
||||
### Related
|
||||
Patch already prepared for the 0.5.1 firmware fields and `$PHEV` events: `02-Architecture-Software/htracker-patches/`.
|
||||
84
docs/historia/icave-confiabilidad-backend.md
Normal file
84
docs/historia/icave-confiabilidad-backend.md
Normal file
@ -0,0 +1,84 @@
|
||||
# Actualización de Confiabilidad — Plataforma de Telemetría HBOX
|
||||
|
||||
**Proyecto Drogheda (GPS-RTK) · Backend / Servidor**
|
||||
Helgen Technologies · 20 de junio de 2026 · Preparado para ICAVE — Hutchison Ports
|
||||
|
||||
---
|
||||
|
||||
## Resumen ejecutivo
|
||||
|
||||
A raíz de las desconexiones reportadas, realizamos un **análisis de causa raíz del
|
||||
backend** (el servidor que recibe la telemetría de las unidades y la reenvía a Wialon).
|
||||
Identificamos y **corregimos tres causas de raíz en el software del servidor** que estaban
|
||||
sacando unidades de Wialon — incluyendo casos en los que **la unidad transmitía
|
||||
correctamente pero la plataforma la descartaba**.
|
||||
|
||||
**Las tres correcciones ya están desplegadas en producción y verificadas.** La
|
||||
ruggedización de la electrónica de campo, el PCB de producción y el manejo térmico del
|
||||
gabinete avanzan **en paralelo**, como frentes independientes.
|
||||
|
||||
## Lo que se observó en campo
|
||||
|
||||
- La flota completa se desconectaba prácticamente **al mismo tiempo**.
|
||||
- Unidades que "morían" y **no regresaban** a Wialon, aun estando encendidas y transmitiendo.
|
||||
|
||||
## Causa raíz (en el backend, no en el radio)
|
||||
|
||||
1. **Bloqueo en cadena.** El servidor enviaba a Wialon y esperaba la confirmación dentro del
|
||||
mismo hilo en que recibía los datos de toda la flota. Un solo envío lento, o un enlace
|
||||
superior intermitente, **congelaba la recepción de todas las unidades a la vez** → explica
|
||||
la caída simultánea de la flota.
|
||||
2. **Filtro "anti-salto" demasiado rígido.** Descartaba saltos de posición grandes, pero **sin
|
||||
actualizar su punto de referencia**: una unidad que se movía mientras estaba desconectada
|
||||
(o con un primer dato atípico) quedaba **bloqueada de forma permanente** — transmitía bien,
|
||||
pero ya nunca volvía a aparecer.
|
||||
3. **Identidad por dirección IP bajo CGNAT.** El operador celular agrupa muchas SIM detrás de
|
||||
una misma IP pública; al identificar por IP, el servidor **confundía unas unidades con
|
||||
otras**, generando saltos falsos y mezcla de datos.
|
||||
|
||||
## Hallazgo clave
|
||||
|
||||
Confirmamos que **varias unidades reportadas como "muertas" estaban en realidad EN LÍNEA y
|
||||
transmitiendo correctamente**: la falla estaba en el backend, no en su hardware. Esto nos
|
||||
permite separar con certeza las fallas de software de las de campo.
|
||||
|
||||
## Correcciones desplegadas
|
||||
|
||||
- **Procesamiento concurrente:** el reenvío a Wialon ya no bloquea la recepción. Una unidad
|
||||
lenta o un enlace superior caído **ya no afecta al resto de la flota**.
|
||||
- **Filtro anti-salto con auto-recuperación:** **nunca** bloquea de forma permanente a una
|
||||
unidad sana — se re-ancla por sí solo — y sigue rechazando saltos físicamente imposibles.
|
||||
- **Identidad por dispositivo (a prueba de CGNAT):** se elimina la mezcla de datos entre
|
||||
unidades que comparten IP del operador.
|
||||
- **Despliegue seguro:** con respaldo automático, verificación de estado y **reversión
|
||||
automática** ante cualquier falla.
|
||||
|
||||
## Mayor visibilidad
|
||||
|
||||
- El tablero (**monitor.helgen.dev**) ahora distingue una **"unidad sin señal"** de una
|
||||
**unidad en línea que el backend no estaba reenviando** — para no confundir un problema de
|
||||
plataforma con una falla de equipo.
|
||||
- Las bitácoras se envían a una plataforma en la nube (Grafana) para diagnóstico y
|
||||
trazabilidad, **sin saturar el disco del servidor**.
|
||||
|
||||
## Validación
|
||||
|
||||
Pruebas de extremo a extremo que reproducen enlace superior lento, IPs compartidas (CGNAT) y
|
||||
saltos de posición: **10 de 10 verificaciones aprobadas** antes de salir a producción.
|
||||
|
||||
## Estado y próximos pasos
|
||||
|
||||
| Frente | Estado |
|
||||
|---|---|
|
||||
| Backend / servidor (confiabilidad) | ✅ **Corregido y en producción** |
|
||||
| Ruggedización de la electrónica de campo | 🔧 En proceso (paralelo) |
|
||||
| PCB de producción | 🔧 En diseño (paralelo) |
|
||||
| Manejo térmico del gabinete | 🔧 En proceso (paralelo) |
|
||||
|
||||
El backend se **rediseñó para escalar a cientos de unidades**, en línea con la meta de **30
|
||||
unidades operativas para agosto**.
|
||||
|
||||
---
|
||||
|
||||
*Borrador preparado por Helgen Technologies para revisión de Leo antes de compartirse con el
|
||||
cliente.*
|
||||
39
docs/historia/icave-nota-viajes-wialon.md
Normal file
39
docs/historia/icave-nota-viajes-wialon.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Viajes en Wialon: HBOX RTK vs GPS heredado en la RTG ZPMC-20 — qué dicen los datos
|
||||
|
||||
**Para:** Agustín Alfonseca (ICAVE) · **De:** Leo Rodríguez, Helgen Technologies · **Fecha:** 6-oct-2026 · v1.0
|
||||
**Datos:** exportaciones de Wialon de `RT-ZPM-20 (RTK)` y `RT-ZPMC-20` del 5-oct (15:00–17:00) y 6-oct (08:00–10:46), reportes *Trips* de Wialon de ambas unidades. Informes interactivos adjuntos: `informe_viajes_2026-10-05.html`, `informe_viajes_2026-10-06.html`.
|
||||
|
||||
## Resumen
|
||||
|
||||
**Los viajes de Wialon de las dos unidades no son comparables porque el GPS heredado reporta movimiento con la grúa parada.** Wialon aplica la misma configuración de viajes a las dos unidades — los reportes de ambas se reproducen exactamente con velocidad ≥ 1 km/h, parada ≥ 300 s y distancia mínima ≈ 150 m —; la diferencia está en los datos que manda cada equipo:
|
||||
|
||||
| | GPS heredado (RT-ZPMC-20) | HBOX RTK (RT-ZPM-20) | Movimiento real de la grúa* |
|
||||
|---|---|---|---|
|
||||
| 5-oct 15:00–17:00 | 1 viaje · **1 h 47 min · 3.30 km** | 1 viaje · 6 min · 0.19 km | **0.41 km en 8 min** (6 maniobras) |
|
||||
| 6-oct 08:00–10:46 | 2 viajes · **2 h 40 min · 6.17 km** | 1 viaje · 2 min · 0.17 km | **0.35 km en 6 min** (8 maniobras) |
|
||||
|
||||
\* Medido con el HBOX a 1 posición/segundo y resolución de 0.2 m (12 satélites, HDOP 0.5), sumando sólo los tramos en que la unidad reporta velocidad.
|
||||
|
||||
## Qué está pasando
|
||||
|
||||
1. **El GPS heredado "se mueve" parado.** En los intervalos en que el HBOX demuestra que la grúa no se movió, el GPS heredado reporta 1–5 km/h el **34 % (5-oct) y 44 % (6-oct)** del tiempo, y su posición salta **6–10 m típicos (hasta 140 m)** entre mensajes consecutivos (cada 30 s). Con esos saltos Wialon nunca ve 300 s seguidos "parado", así que no cierra el viaje: un solo viaje cubre toda la jornada y acumula **1.7 km (5-oct) y 3.6 km (6-oct) de kilometraje fantasma**, es decir **8–18 veces el movimiento real**. Cuando la grúa sí se movió, ese GPS lo detectó sólo el 47–70 % de las veces.
|
||||
2. **El HBOX no inventa viajes, pero Wialon le descarta los reales.** En reposo manda 0 km/h y una posición estable (< 1 m). Las maniobras de una RTG son cortas — **20 a 120 s, 10 a 170 m, a 2–6 km/h** — y la distancia mínima de viaje configurada en Wialon (≈ 150 m) descarta la mayoría: 5 de 6 maniobras el 5-oct y 7 de 8 el 6-oct. Por eso el HBOX muestra "menos viajes": no porque la grúa se mueva menos, sino porque Wialon filtra lo que el GPS heredado nunca midió.
|
||||
|
||||
En el mapa del informe se ve a simple vista: la traza del HBOX es una línea limpia a lo largo del bloque; la del GPS heredado es una maraña alrededor de una grúa estacionada.
|
||||
|
||||
## Qué proponemos
|
||||
|
||||
| Acción | Quién | Cuándo |
|
||||
|---|---|---|
|
||||
| **Ajustar en Wialon el detector de viajes de las unidades HBOX**: velocidad mínima 1 km/h, parada mínima 120–300 s, **distancia mínima 10 m**, tiempo mínimo 0 s. Con eso Wialon reporta las 6 y 8 maniobras reales del 5 y 6-oct (0.50 y 0.48 km) en lugar de 1 y 1. El HBOX soporta ese umbral; el GPS heredado no (le saldrían aún más viajes falsos). | Helgen / ICAVE (cuenta Wialon) | esta semana |
|
||||
| **Filtro de movimiento en el gateway de Helgen** (ya desarrollado y probado): mientras la unidad está parada reenvía velocidad 0 y posición fija, de modo que tracks y contadores de Wialon no integren deriva. Se activa primero sólo en la ZPMC-20 y se valida 2–3 días contra el informe. | Helgen | activación 7-oct, validación al 10-oct |
|
||||
| **Combustible por horas de motor, no por km de viaje.** Una RTG consume izando, parada; los viajes por velocidad GPS son un mal proxy de combustible con cualquier GPS. El HBOX ya detecta ignición; proponemos reenviarla a Wialon como parámetro y calcular **horas de motor** (y, si ICAVE lo prefiere, viajes "por sensor de ignición"), que sí son consistentes entre unidades. | Helgen (propuesta técnica) | propuesta 10-oct |
|
||||
| **Herramienta de verificación en el monitor de Helgen** (`/trips`): para cualquier unidad y ventana, recalcula los viajes con los parámetros de Wialon sobre los datos a 1 Hz y permite subir la exportación de otro GPS y los reportes de Wialon para comparar. Disponible para ICAVE a solicitud. | Helgen | desplegado |
|
||||
|
||||
**Lo que no recomendamos:** "igualar" el HBOX al GPS heredado. Eso sólo se logra agregándole el mismo ruido, y los kilómetros resultantes seguirían siendo ficticios.
|
||||
|
||||
Requerimos formalmente la autorización de ICAVE para ajustar los parámetros del detector de viajes de las unidades HBOX en la cuenta de Wialon, o en su defecto la confirmación de que Helgen puede hacerlo directamente.
|
||||
|
||||
Saludos cordiales,
|
||||
Leo Rodríguez
|
||||
Helgen Technologies
|
||||
90
docs/historia/icave-respuesta-observaciones.md
Normal file
90
docs/historia/icave-respuesta-observaciones.md
Normal file
@ -0,0 +1,90 @@
|
||||
# Respuesta a observaciones — Proyecto Drogheda (H-BOX RTK)
|
||||
|
||||
**Borrador para revisión de Leo antes de enviarse.** Destinatario: Agustín Alfonseca
|
||||
(ICAVE). Helgen Technologies · 8 de julio de 2026.
|
||||
|
||||
> **⚠️ Nota para Leo (no forma parte del correo) — confirmar antes de enviar:**
|
||||
> - **Fechas comprometidas:** recepción de PCBs impresos **~13–17 jul**; comparativa/
|
||||
> pruebas de confiabilidad en ICAVE **17 jul**; **30 unidades operativas en agosto**.
|
||||
> Ajusta si el proveedor de PCB movió la ventana.
|
||||
> - **Backend:** el correo afirma que las 3 correcciones ya están **desplegadas y
|
||||
> verificadas** (consistente con el update del 20-jun). Confirmar que sigue vigente.
|
||||
> - **Térmico:** lo dejo como *verificación en curso* (falta la medición con termopar),
|
||||
> no como resuelto — para no sobre-prometer.
|
||||
> - Decidir si adjuntas el nuevo deck de Avance / render del PCB EinarBoard 1.0.
|
||||
|
||||
---
|
||||
|
||||
**Asunto:** Proyecto Drogheda (H-BOX RTK) — Respuesta a tus observaciones, estado actual y próximos pasos
|
||||
|
||||
Estimado Agustín:
|
||||
|
||||
Gracias por la revisión tan detallada y por los puntos que nos compartiste. Los tomamos
|
||||
uno por uno; abajo respondo a cada observación con el estado actual y las fechas
|
||||
comprometidas. En resumen: **el software y el backend ya están estabilizados y
|
||||
verificados**, y la electrónica de campo **pasa esta semana de prototipo a su versión
|
||||
industrial definitiva**.
|
||||
|
||||
**1. Electrónica interna (cableado Dupont / aspecto de prototipo).**
|
||||
Aceptamos la observación. Ese armado fue una etapa **deliberada de la Fase 2**: nos
|
||||
permitió congelar la lógica del *Watchdog* y el firmware en el entorno más exigente
|
||||
(ICAVE) sin arriesgar una manufactura prematura. Esa etapa ya cumplió su función. **El PCB
|
||||
industrial unificado (EinarBoard 1.0) está cerrado y auditado**, y la orden de fabricación
|
||||
está liberada; **recibimos los primeros prototipos impresos la semana del 13–17 de julio.**
|
||||
Con ello eliminamos por completo los cables sueltos, el estrés de soldadura y cualquier
|
||||
vulnerabilidad por vibración — y será el estándar idéntico para ICAVE, TIMSA, EIT y LCT.
|
||||
|
||||
**2. Protección eléctrica (fusible).**
|
||||
La quema del fusible ocurrió bajo la demanda real medida en ICAVE. Se aplicó una
|
||||
corrección recalibrando su capacidad a esa demanda, y el H-BOX conserva su **protección
|
||||
nativa contra sobrecarga con recuperación automática (Modo Hiccup)** en la fuente. Estamos
|
||||
además **cuantificando la temperatura interna real del gabinete con un termopar** durante
|
||||
un turno completo de sol, para blindar el diseño térmico contra la temperatura medida —
|
||||
no contra la de ficha técnica.
|
||||
|
||||
**3. Disponibilidad y confiabilidad (desconexiones / módulo 4G).**
|
||||
Aquí separamos dos causas distintas:
|
||||
- **Backend (resuelto y desplegado).** El análisis de causa raíz identificó y **corrigió
|
||||
tres defectos de software del servidor** que sacaban unidades de Wialon —incluidas
|
||||
unidades que transmitían bien pero la plataforma descartaba—: bloqueo en cadena,
|
||||
filtro anti-salto sin auto-recuperación, e identidad por IP bajo CGNAT. **Las tres
|
||||
correcciones están desplegadas en producción y verificadas** (10/10 pruebas de extremo
|
||||
a extremo). El tablero ahora distingue una unidad *sin señal* de una unidad *en línea
|
||||
que el backend no reenviaba*.
|
||||
- **Módulo 4G (en aislamiento).** Persisten intermitencias físicas del módulo; el
|
||||
*Watchdog* de la Blackpill las mitiga reiniciando el hardware de forma autónoma para que
|
||||
el sistema no muera en campo. **Desmontamos dos unidades para aislar la causa en
|
||||
laboratorio**, y presentaremos la **comparativa de precisión de rutas GPS** que muestra
|
||||
que, cuando el módulo transmite, la exactitud es de nivel centimétrico.
|
||||
|
||||
**4. Versión final del gabinete.**
|
||||
El diseño del gabinete está finalizado y en espera de materiales. La electrónica interna
|
||||
será **idéntica para las 4 unidades de negocio**; sólo los herrajes de sujeción externa se
|
||||
adaptan a la maquinaria específica de cada operadora. El chasis mantiene su resistencia a
|
||||
vibración industrial.
|
||||
|
||||
**5. Estación base.**
|
||||
La estación base principal está **operativa y lista**; se enviará e instalará en el primer
|
||||
paquete logístico para asegurar la red RTK de alta precisión.
|
||||
|
||||
**6. Integración con VERONICA (retranslator).**
|
||||
La retransmisión nativa **ya funciona correctamente para ICAVE**. Para habilitar el reenvío
|
||||
directo en TIMSA, EIT y LCT, **requerimos formalmente que la administración nos libere las
|
||||
credenciales de acceso de esas tres unidades de negocio** — es el único insumo que nos
|
||||
falta de su lado para replicar la integración.
|
||||
|
||||
**7. Plan de despliegue y fechas.**
|
||||
La meta se mantiene: **30 unidades operativas para agosto.** La estrategia es escalonada y
|
||||
deliberada para proteger la inversión: consolidamos primero el bloque inicial en ICAVE con
|
||||
los PCBs industriales de esta quincena y, una vez verificado el ensamble final ahí,
|
||||
replicamos el envío e instalación en TIMSA, EIT y LCT — sin retrabajos simultáneos en
|
||||
varios puertos. La **comparativa de confiabilidad en ICAVE la tenemos el 17 de julio.**
|
||||
|
||||
Quedamos atentos a tus comentarios y a la gestión de las credenciales de las tres unidades
|
||||
restantes. Con gusto agendamos una llamada para revisar la comparativa en cuanto esté.
|
||||
|
||||
Saludos cordiales,
|
||||
|
||||
**Leo Rodríguez**
|
||||
Helgen Technologies
|
||||
leo@helgen.tech
|
||||
188
docs/historia/kicad-submodulo.md
Normal file
188
docs/historia/kicad-submodulo.md
Normal file
@ -0,0 +1,188 @@
|
||||
# KiCad → git submodule + Gitea mirror — migration record & runbook
|
||||
|
||||
**Date:** 2026-08-27 · **Status:** local side DONE, pushes pending (need network + credentials)
|
||||
|
||||
---
|
||||
|
||||
## 1. What was done
|
||||
|
||||
`03-Architecture-Hardware/kicad` is no longer tracked file-by-file in
|
||||
`project-drogheda`. It is now a **standalone git repository**, referenced from the
|
||||
parent as a submodule.
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Submodule path | `03-Architecture-Hardware/kicad` |
|
||||
| Submodule name | `kicad` |
|
||||
| Canonical remote (`origin`) | `git@github.com:lrodriguez15/hbox-kicad.git` |
|
||||
| Mirror remote (`helgen`) | `ssh://git@git.helgen.online:222/Helgen-Services/HBox_PCB.git` |
|
||||
| Submodule branch | `main`, **72 commits**, HEAD `4bf41c6` |
|
||||
| Parent commit | `5bb2e12` — "kicad → submodule (hbox-kicad)" |
|
||||
| Rollback tag (parent) | `pre-kicad-submodule-2026-08-26` → `daafd5c` |
|
||||
|
||||
**History was preserved.** The 71 board commits were extracted with
|
||||
`git subtree split --prefix=03-Architecture-Hardware/kicad`, so blame and bisect
|
||||
still work on the schematic and PCB. Commit **hashes differ** from the parent's
|
||||
(subtree split rewrites them); messages, authors and dates are identical.
|
||||
|
||||
Verified: every file that existed under `kicad/` at `daafd5c` is byte-identical
|
||||
in the submodule (blob-hash comparison, 309/309 files). `git fsck
|
||||
--connectivity-only` on the new repo is clean.
|
||||
|
||||
### Deliberate content changes (1 commit on top of the split)
|
||||
|
||||
- **added** `.gitignore` — KiCad `.kicad_prl`, `*.lck`, `*.T*bak` agent scratch
|
||||
backups, `.history/`, `.mcp-backups/`, `_to_delete/`, `*_drc_violations.json`,
|
||||
`*.log`, `snapshots/`, Office lock files
|
||||
- **added** `README.md` — layout, remotes, history note
|
||||
- **untracked** `hbox-watchdog-v3.kicad_prl` (per-user KiCad UI state)
|
||||
- **untracked** `~hbox-watchdog-v5.kicad_pro.lck` (KiCad session lock, already
|
||||
deleted on disk)
|
||||
|
||||
### Not done, on purpose
|
||||
|
||||
- **Parent history was not rewritten.** Its `.git` stays ~239 MB because the old
|
||||
kicad blobs are still in it. Nothing force-pushed, no clone broken. The parent
|
||||
simply stops growing from here on. If you ever want it slim, that is a separate
|
||||
`git filter-repo` + force-push job.
|
||||
- Branch `kicad-split` still exists in the parent as the split's output. Delete it
|
||||
once the push succeeds: `git branch -D kicad-split`.
|
||||
|
||||
---
|
||||
|
||||
## 2. What you still have to do
|
||||
|
||||
### Step 1 — create the GitHub repo and push the submodule
|
||||
|
||||
```bash
|
||||
cd ~/Documents/Claude/Projects/Drogheda/03-Architecture-Hardware/kicad
|
||||
|
||||
# create it (private) — or make it in the web UI as lrodriguez15/hbox-kicad
|
||||
gh repo create lrodriguez15/hbox-kicad --private
|
||||
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
~240 MB on the first push, mostly the STL/STEP mechanical exports. Expect a few
|
||||
minutes.
|
||||
|
||||
### Step 2 — push the parent
|
||||
|
||||
```bash
|
||||
cd ~/Documents/Claude/Projects/Drogheda
|
||||
git push origin main
|
||||
git push origin pre-kicad-submodule-2026-08-26 # rollback tag
|
||||
git branch -D kicad-split # split scaffolding, no longer needed
|
||||
```
|
||||
|
||||
Push the submodule **first**. A parent commit whose gitlink points at a commit
|
||||
nobody can fetch will break every clone.
|
||||
|
||||
### Step 3 — set up the Gitea pull mirror
|
||||
|
||||
Gitea can only make a repo a *pull* mirror **at migration time** — it cannot be
|
||||
switched on for a repo that already exists. `Helgen-Services/HBox_PCB` is
|
||||
currently empty, so:
|
||||
|
||||
1. Gitea → `Helgen-Services/HBox_PCB` → **Settings → Danger Zone → Delete**.
|
||||
2. **+ → New Migration → GitHub**.
|
||||
- Clone URL: `https://github.com/lrodriguez15/hbox-kicad.git`
|
||||
- Auth: your GitHub username + a **classic PAT with `repo` scope**
|
||||
(required — the GitHub repo is private)
|
||||
- Owner: `Helgen-Services` · Repository name: `HBox_PCB`
|
||||
- ✅ **This repository will be a mirror**
|
||||
3. After migration: **Settings → Mirror Settings** → set the interval (default is
|
||||
`8h`; `1h` is reasonable) and use **Synchronize Now** to test.
|
||||
|
||||
You push only to GitHub; Gitea follows on its own. **This needs no SSH key** —
|
||||
the mirror pulls over HTTPS with the PAT, which is why it is the low-friction
|
||||
option while your SSH credentials are still pending.
|
||||
|
||||
If you would rather keep the existing `HBox_PCB` repo as-is, the fallback is a
|
||||
local dual push instead of a server-side mirror:
|
||||
|
||||
```bash
|
||||
cd .../kicad
|
||||
git remote set-url --add --push origin git@github.com:lrodriguez15/hbox-kicad.git
|
||||
git remote set-url --add --push origin ssh://git@git.helgen.online:222/Helgen-Services/HBox_PCB.git
|
||||
# now `git push origin main` hits both — but both must be reachable, every time
|
||||
```
|
||||
|
||||
### Step 4 — SSH to Gitea (only if you want to push there directly)
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "leo@helgen.tech" -f ~/.ssh/id_ed25519_helgen
|
||||
pbcopy < ~/.ssh/id_ed25519_helgen.pub # paste into Gitea → Settings → SSH/GPG Keys
|
||||
|
||||
cat >> ~/.ssh/config <<'EOF'
|
||||
Host git.helgen.online
|
||||
HostName git.helgen.online
|
||||
Port 222
|
||||
User git
|
||||
IdentityFile ~/.ssh/id_ed25519_helgen
|
||||
EOF
|
||||
|
||||
ssh -T git@git.helgen.online -p 222 # expect a Gitea greeting
|
||||
```
|
||||
|
||||
The `helgen` remote is already configured in the submodule, so after this
|
||||
`git push helgen main` works. **Do not** push to Gitea if you set it up as a pull
|
||||
mirror in Step 3 — a mirror is read-only and force-overwrites local pushes on the
|
||||
next sync.
|
||||
|
||||
---
|
||||
|
||||
## 3. Day-to-day workflow from now on
|
||||
|
||||
Board work is committed **inside** the submodule, then the parent is told which
|
||||
commit to point at:
|
||||
|
||||
```bash
|
||||
cd .../Drogheda/03-Architecture-Hardware/kicad
|
||||
git add hbox-watchdog-v6.kicad_sch hbox-watchdog-v6.kicad_pcb
|
||||
git commit -m "v6 T5: LTC4040 errata + U7 delete"
|
||||
git push # → GitHub, Gitea follows
|
||||
|
||||
cd ../.. # parent repo
|
||||
git add 03-Architecture-Hardware/kicad # stages the new gitlink
|
||||
git commit -m "kicad: v6 T5"
|
||||
git push
|
||||
```
|
||||
|
||||
Forgetting the second half is the classic submodule trap: the board work is safe
|
||||
on GitHub, but the parent still points at the old commit.
|
||||
|
||||
### Cloning fresh
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules git@github.com:lrodriguez15/project-drogheda.git
|
||||
# already cloned without it:
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Anyone who only has Gitea access can redirect the submodule locally without
|
||||
editing `.gitmodules`:
|
||||
|
||||
```bash
|
||||
git config --global url."ssh://git@git.helgen.online:222/Helgen-Services/HBox_PCB.git".insteadOf \
|
||||
"git@github.com:lrodriguez15/hbox-kicad.git"
|
||||
```
|
||||
|
||||
For a fast, board-files-only checkout: `git clone --filter=blob:none` skips the
|
||||
historical STL/STEP blobs until something actually needs them.
|
||||
|
||||
---
|
||||
|
||||
## 4. Open items / observations
|
||||
|
||||
- **`~hbox-watchdog-v6.kicad_pro` was modified while this ran** — KiCad is (or
|
||||
was) open on the v6 project. `hbox-watchdog-v6.kicad_pro` shows as modified in
|
||||
the submodule; commit or discard it as you see fit.
|
||||
- **Your other three submodules point at `ssh://git.helgen.dev:6611/…`**
|
||||
(`hbox-firmware`, `htracker`, `hbox-log-tools`) and are currently uninitialised.
|
||||
If `.dev:6611` has been superseded by `git.helgen.online:222`, those URLs need
|
||||
updating too — say the word and I'll do it in one pass.
|
||||
- **This VM has no network access**, which is why every push above is left for
|
||||
you to run. Everything that could be done offline is done.
|
||||
- `_to_delete/` folders under `03-Architecture-Hardware/` and `kicad/` hold git
|
||||
lock/temp files that the sandbox was not permitted to unlink. Safe to delete.
|
||||
229
docs/historia/pinmap-auditoria-054.md
Normal file
229
docs/historia/pinmap-auditoria-054.md
Normal file
@ -0,0 +1,229 @@
|
||||
# HBOX V4 / V5 — pin-map audit against firmware 0.5.4 + bench log analysis
|
||||
|
||||
**Date:** 26-Sep-2026 · **Firmware:** `hbox_v5_bringup` 0.5.4 (`board_v4.h` / `board_v5.h`) ·
|
||||
**Boards:** `hbox-watchdog-v4.kicad_pcb` (as fabbed, 01-Aug) · `hbox-watchdog-v5.kicad_pcb` (= fab IPC-356 netlist of 21-Aug, 768 pads compared, no net differences)
|
||||
**Inputs:** `06-Testing-Validation/log1.txt` (13:12, stack OFF, vbat 3.14 V) and `STAT up=152s vbat=4.03 v5=0.00(no).txt` (13:14, battery duty cycle)
|
||||
|
||||
## 1. Verdict
|
||||
|
||||
1. **The pin map is correct on both boards.** Every `PIN_*` in `board_v4.h` and `board_v5.h`, plus the hard-coded `RELAY(PA10, PA9)` and USB, lands on the right net, and each net reaches the right load. This was checked at pad level and by a copper-connectivity check on the fabbed PCB files. V4 and V5 are netlist-identical outside the MCU. The only differences are the fan-tach parts (P6.4, Rtp1/Rts1, V4 only), Rig2 (V5 only), P1.4 and the W5500 exposed pad.
|
||||
2. **`log1` shows no hardware fault. The firmware kept the stack OFF on purpose.** Three conditions each block it on their own: reset = **POR**, **0.00 V on P2** (PA4) and **vbat 3.14 V** (below the 3.40 V cut). Because of this the ArduSimple and the 4G are never powered, and a reset does not bring them back (§2).
|
||||
3. **V4 has one real hardware problem, and it is not in the pin map.** The Blackpill 5V header pins are wired straight to its USB VBUS, and V4 ties those pins to `/SYS5V`. With a laptop on the Blackpill USB, the PC's USB port is hard-paralleled with the board's 5 V rail. On battery the PC (5.0 V) out-pulls the LTC4040 boost (4.79 V), so it is the PC that tries to feed the modem. **Battery tests on V4 with the USB console attached are not valid** (§4.1).
|
||||
4. **The pack is flat and the firmware cannot recharge it.** Every 0.5.x build keeps `CHRG_EN` HIGH. Each battery test drains the pack permanently until it can no longer open a duty window. A pack at 3.14 V also needs attention to avoid over-discharge (§4.3).
|
||||
5. The **V5 "4G dark after low-power mode"** problem is real and separate. The MCU side of the duty cycle works (log 2). The modem side has to be measured. See §5 for the three measurements that decide the fix.
|
||||
|
||||
## 2. What `log1` says
|
||||
|
||||
```
|
||||
STAT up=80s vbat=3.14 v5=0.00(no) ign=OFF chrg=idle ... stack=OFF modo=AUTO ... bkp=0
|
||||
$PHLG,0.5.4-fm,BENCH,SRCH,...,3.14,0.00,...,POR,...,80,BATT,0.00,3.13,0
|
||||
```
|
||||
|
||||
| Firmware rule (0.5.4) | Value in log1 | Effect |
|
||||
|---|---|---|
|
||||
| AUTO powers the stack only when **PA4 ≥ 4.50 V for 3 s** (`V5_STACK`, `AUTO_SETTLE_MS`) | v5 = 0.00–0.01 V for the whole capture | Stack never switched on |
|
||||
| Battery windows start only after a **warm** reset (IWDG/BOR/SOFT/LPWR). **POR and PIN (button) are excluded** | reset = POR | No duty window, and a button press would not start one either |
|
||||
| Battery windows need **vbat ≥ 3.40 V** | vbat = 3.14 V (min 3.13) | Would be skipped even after a warm reset |
|
||||
|
||||
The I²C and analog paths are healthy: SHT40/TMP102 read, `i2cerr=0`, NTC 26.6 °C, MCU 31.9 °C, charger idle with no fault. **The one missing input is 5 V at P2.** One of these is true:
|
||||
- the unit was on USB console only (no 24 V), which is normal but powers nothing;
|
||||
- 24 V was on but the SD-15B output is not reaching P2 (the same symptom as RTK-005 on 18-Sep: harness / GX16 / SD-15B);
|
||||
- 5 V reaches P2 but not PA4 (R7/R8 divider). This is unlikely, and the §6 measurement separates it.
|
||||
|
||||
## 3. Pin audit
|
||||
|
||||
`✔` = the MCU pin, the net and the load all agree with the header file. V4 socket pads were mapped with the WeAct header order (header 1 runs 5V…VB from the USB end, header 2 runs B12…3V3 from the USB end). **J1 is the `RevNum_EJ1` footprint: pad 20 (`/SYS5V`) and J2 pad 1 (`/ETH_CS`) sit at the same end (x = 107.2), which is the USB end.** The E-J1 mirror is fixed in V4, and the silkscreen labels (`5V`, `NRST`, `PCTL`…) match.
|
||||
|
||||
| Signal | FW pin | V5 U16 (LQFP64) | V4 socket pad → Blackpill pin | Load / other end | |
|
||||
|---|---|---|---|---|---|
|
||||
| BAT_SENSE | PA0 | 14 | J1.6 → A0 | R5/R6 10k/10k from `/BAT_PLUS`, C5 | ✔ |
|
||||
| PWR_CTRL | PA1 | 15 | J1.7 → A1 | R1 1k → Q5 (B,E,C = 1,2,3) → `/GATE_LS` (R2 10k to SYS5V) → Q1 AO3401A (G,S,D = 1,2,3) → `/SW_5V` → MOD1.5 + U7 VIN/EN | ✔ |
|
||||
| GNSS_TX / RX | PA2 / PA3 | 16 / 17 | J1.8 → A2 / J1.9 → A3 | MOD1.16 / MOD1.15 (simpleRTK2B UART1) | ✔ |
|
||||
| 5V_SENSE | PA4 | 20 | J1.10 → A4 | R7/R8 10k/10k from `/VEH5V` (P2.1), C7 | ✔ |
|
||||
| IGN_SENSE | PA5 | 21 | J1.11 → A5 | Rig1 22k from IGN_IN, Rigb1 3.3k (+Rig2 22k on V5), BAT54 clamp | ✔ |
|
||||
| FAN_PWM | PA6 | 22 | J1.12 → A6 | fan low-side gate | ✔ |
|
||||
| CHRG_EN | PA8 | 41 | J2.5 → A8 | U6 LTC4040 pin 4 (HIGH = charge off), R56 | ✔ |
|
||||
| USART1 TX / RX | PA9 / PA10 | 42 / 43 | J2.6 → A9 / J2.7 → A10 | K1.10 (NO) / K1.1 (NC) + MOD1.23 (F9P TX2 tap); K1 COM → `/DIN_4G` → J7.3 | ✔ |
|
||||
| USB D− / D+ | PA11 / PA12 | 44 / 45 | Blackpill USB-C (J2.8/9 left open) | V5: J11 + PESD | ✔ |
|
||||
| ETH_RST | PA15 | 50 | J2.10 → A15 | W5500 pin 37 | ✔ |
|
||||
| EXT_LED | PB0 | 26 | J1.14 → B0 | Q4 gate | ✔ |
|
||||
| NTC | PB1 | 27 | J1.15 → B1 | R42 10k to 3V3 + P12 | ✔ |
|
||||
| ETH_INT | PC0 (V5) / PB2 (V4) | 8 | J1.16 → B2 | W5500 pin 36 | ✔ |
|
||||
| CHRG_STAT / FAULT | PB3 / PB4 | 55 / 56 | J2.11 / J2.12 | U6 pins 8 / 9, 100k pull-ups | ✔ |
|
||||
| BYP_CTRL | PB5 | 57 | J2.13 → B5 | R40 → Q6 → K1 coil, D6 flyback | ✔ |
|
||||
| I2C SCL / SDA | PB6 / PB7 | 58 / 59 | J2.14 / J2.15 | U3, U4, U10, U13, P5, 4.7k pull-ups | ✔ |
|
||||
| SER_IRQ | PB8 | 61 | J2.16 → B8 | U10 pin 15, R81 | ✔ |
|
||||
| ETH SPI | PB12–15 | 33–36 | J2.1–4 | W5500 | ✔ |
|
||||
| LED_C13 | PC13 | 2 | Blackpill LED (J1.2 open) | V5: D17 + R102 to 3V3 (active low) | ✔ |
|
||||
| NRST | — | 7 | J1.5 → R | **V5:** SW1 + C104 100 nF + P10 + TP5. **V4:** SW1 + P10 | ✔ |
|
||||
| BOOT0 / BOOT1 | — | 60 / 28 | on Blackpill | V5: R98 10k↓ + SW2 to 3V3; R99 10k↓ | ✔ |
|
||||
|
||||
V5 support pins also check out: VCAP1 = C111 4.7 µF; VDDA through R104 10 Ω + C109/C112; HSE = Y3 25 MHz with 15 pF; LSE = Y4; VBAT tied to 3V3.
|
||||
SW1 on both boards is the standard 6×6 THT footprint: NRST on the pad pair 6.5 mm apart, GND on the other pair (part C83205, K2-6639DP).
|
||||
|
||||
## 4. Hardware and firmware findings
|
||||
|
||||
### 4.1 V4: USB VBUS is hard-wired to `/SYS5V` (V4 only)
|
||||
The WeAct 5V header pins are the USB VBUS, with no diode. V4 connects J1.20 and J2.18 to `/SYS5V`, and has no other 3V3 source apart from the Blackpill LDO. V5 does not have this problem: J11 VBUS reaches only the MCU LDO, through D13 in a diode-OR with D12.
|
||||
- **USB only:** the laptop powers SYS5V, K1, U12/W5500 and back-feeds MAIN5V through P11. The stack stays off, because VEH5V = 0.
|
||||
- **24 V + USB:** SD-15B (~5.2 V) and the laptop VBUS fight each other.
|
||||
- **Battery + USB:** the laptop's 5.0 V is higher than the boost's 4.79 V, so the laptop port carries the stack. It cannot supply the LTE burst: **4G stays dark and the result says nothing about the battery path.**
|
||||
- `board_v4.h` says "flash by DFU on USB only (W5500 unpowered)". **That is wrong on V4.** USB alone powers the W5500 through SYS5V → U12. If DFU ever fails to enter on V4, this is why (INTn on PB2 = BOOT1).
|
||||
|
||||
To confirm on your module revision: with the Blackpill out of the board, check continuity from the USB-C VBUS to the 5V header pin (expect ~0 Ω).
|
||||
**Bench rule until reworked:** with the board powered, connect the console through a USB cable with VBUS cut (data only), or through a USB isolator. **Rework option:** lift the two Blackpill 5V pins out of the sockets and feed the Blackpill 5V from SYS5V through a Schottky diode (anode SYS5V, e.g. at D6 cathode). USB can then never reach SYS5V.
|
||||
|
||||
### 4.2 Reset behaviour
|
||||
- **Hardware:** on both boards the MCU reset button is wired correctly (table above). The quick test is to press SW1 with the console open. The first line after the banner must read **`reset=PIN`**. If it does, the button works.
|
||||
- **Firmware:** a button reset **never powers the stack without 5 V on P2**. POR and PIN are deliberately excluded from the "warm reset → battery mode" path in 0.5.2+. On battery, pressing reset therefore makes a unit go quiet until the vehicle 5 V returns. This matches "after reset the ArduSimple doesn't come back".
|
||||
- **SW4G1** (the 4G reset) only pulls XBee pin 5 low. Whether the 4G NTRIP Master resets on that pin depends on the module. If pressing it with the modem running does nothing, the module does not use pin 5, and the only way to reset the modem is to remove its power (a stack cycle).
|
||||
|
||||
### 4.3 Charging is off in every 0.5.x build
|
||||
`CHRG_EN` is held HIGH from boot ("paquete de un solo uso"). Consequences:
|
||||
- after one or two battery tests the pack stays low for good;
|
||||
- below 3.40 V every duty wake is skipped;
|
||||
- at 3.14 V resting the cell is essentially empty. The LTC4040 boost keeps running the board until its own UVLO (2.45–2.6 V), which is deep over-discharge for Li-ion.
|
||||
|
||||
Disconnect or recharge that pack now, using an external charger. Battery tests cannot be repeated until the firmware can charge.
|
||||
|
||||
### 4.4 Minor
|
||||
- The AUTO power-on also needs **v5 ≥ 4.50 V** (not only "present" ≥ 4.20 V). With 4.20–4.50 V at P2 the stack stays off, and no EVT line says why.
|
||||
- The IGN divider reads 24 V correctly (V4 3.13 V, V5 2.77 V at the pin). A 12 V ignition, 1.4–1.6 V at the pin, sits in the undefined input band.
|
||||
|
||||
## 5. V5 battery problem ("4G does not turn on after the low-power mode")
|
||||
|
||||
The second log shows the **MCU side working exactly as designed**:
|
||||
- windows #2 and #3 open 600 s apart;
|
||||
- the F9P sends NMEA 0.9 s after `STACK ON` and has a GPS fix after ~12 s;
|
||||
- Modo B engages at +10 s and 4 `$PHLG` go into the modem DIN per window;
|
||||
- each window closes at 120 s;
|
||||
- vbat is 4.08 V at rest and 4.00–4.06 V in the window (min 3.92 V).
|
||||
|
||||
The MCU cannot see whether the modem is alive. The first window works because the modem was already registered on vehicle power. The failure is a **cold modem start from the battery boost**: MAIN5V 4.79 V → P11 → F1 → Q1 (switched hard, no soft-start, into C3/C40 + the ArduSimple + U7) → 3V3_MODEM. The EMC audit already flagged the narrow `/SW_BAT` (0.3 mm) and `/BAT_PLUS` (0.3–0.5 mm) paths in exactly this current loop. Three measurements settle it:
|
||||
|
||||
| # | Test (during a battery wake) | If… | Then the fix is… |
|
||||
|---|---|---|---|
|
||||
| M1 | Min-hold DMM or scope on **3V3_MODEM (C43)**, **SW_5V (C3)** and **MAIN5V (C32)** for the first 60 s | 3V3_MODEM dips below ~3.1 V, or SW_5V below ~4.3 V | a supply problem: continue with M2 |
|
||||
| M2 | Replace the pack with a **lab PSU 3.8 V / 5 A** on short leads into P4 | the modem starts on every wake | pack, wiring or trace impedance (EMC items 1–2); the pack side, not firmware |
|
||||
| M3 | When the modem is dark inside a window, press **SW4G1** | the modem boots | a start-up latch: wire a spare GPIO to `/MODEM_RST` and pulse it from firmware |
|
||||
|
||||
Also check on the server whether `$PHEV DUTY_WAKE` and `$PHLG … BATT` arrived around 13:02 and 13:12 (windows #2/#3 of log 2). That confirms the modem side independently of the bench.
|
||||
|
||||
## 6. Bench sequence for the board in `log1` (≈15 min)
|
||||
|
||||
1. Remove the Blackpill USB (V4) or keep only J11 (V5). Charge or replace the pack to ≥ 3.8 V.
|
||||
2. Apply 24 V. Measure **P2.1 to GND** (expect 5.1–5.3 V) and **R8 top / PA4** (expect half of that).
|
||||
- 0 V at P2: fix the SD-15B / GX16 harness.
|
||||
- 5 V at P2 but not at PA4: check R7/R8 and the socket contact at J1.10 (V4).
|
||||
3. Reconnect the console (VBUS-cut cable on V4). Expect `V5 PRESENTE`, then `STACK ON reason=auto` 3 s later. `v` prints the checkpoints: PWR_CTRL 3.3 V → GATE_LS < 0.3 V → SW_5V ≈ 5 V.
|
||||
4. Press SW1: the next boot must read `reset=PIN`, and the stack comes back 3 s later because 5 V is present.
|
||||
5. Only then repeat the battery test (V5 first), with M1–M3.
|
||||
|
||||
## 7. Proposed 0.5.5 (for approval — nothing changed yet)
|
||||
|
||||
| Priority | Change | Size |
|
||||
|---|---|---|
|
||||
| 1 | Treat a **PIN reset like a warm reset**: with no 5 V and vbat ≥ 3.40 V, the first battery window opens at +60 s. The button then revives a unit on battery. | tiny |
|
||||
| 1 | **Say why the stack is held:** `espera=5V(P2 0.00V)` / `vbat<3.40` / `v5<4.50` / `POR sin 5V` in STAT, plus a boot EVT. | tiny |
|
||||
| 2 | **Charge enable** when 5 V is present and tbat/tntc < 40 °C, off at ≥ 45 °C, with the LTC4040 timer restart (the hostsim-validated v3.0 policy). Without it every battery test consumes a pack. | medium |
|
||||
| 3 | **Modem start on battery**, picked by M1–M3: a PWM soft-start on PA1 (TIM2_CH2, ~100 ms ramp) if the fault is the inrush dip, or a modem-alive input plus a reset pulse (one bodge wire: V5 PC1–PC3 / V4 PA7 are free) if it is a start-up latch. | small–medium |
|
||||
|
||||
Same source for V4 and V5; the binaries stay `hbox-v5-bringup_0.5.5.bin` and `hbox-tarjetaV4_0.5.5.bin`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Addendum (26-Sep, 15:00): what V4 still needs with firmware 0.5.5
|
||||
|
||||
**Binary checked.** `release/hbox-tarjetaV4_0.5.5.bin` has SHA-256 `39ea770b…2911`. That is the same as `out-v4`, built from commit `2e0bf24` with `board_v4.h`. Its strings read `0.5.5-fm`, `EinarBoard v4 (HBOX v4)` and `STM32F411CEU6 Blackpill`.
|
||||
|
||||
**The 0.5.5 change is correct on V4.** V4 declares `GNSS` as `Uart(PA3, PA2)`. `end()` followed by `pinMode(INPUT)`, and `begin()` again at stack-on, behaves the same as on V5. In `setStack(true)`, power goes on before the UART drives PA2, and at stack-off power goes off first.
|
||||
|
||||
### 8.1 One gap left in 0.5.5 (both boards): PA9/PA10 after Modo B
|
||||
The STM32duino `HardwareSerial::end()` → `uart_deinit()` only resets and clock-gates the USART. **It never touches the GPIOs.** I checked this in `uart.c` on core 2.9 and on `main`. The pins keep the pin-map mode `AF_PP + GPIO_PULLUP`. 0.5.5 fixes this for the GNSS UART with explicit `pinMode(INPUT)`, but `relayDisengage()` still only calls `RELAY.end()`.
|
||||
|
||||
With K1 released, the node `USART1_RX` = PA10 + Rdfu1 (22 k to 3V3) + MOD1.23 (F9P TX2) + K1 NC is connected to `DIN_4G`, the modem's RX. So during the whole 8-minute rest, PA10's ~40 k pull-up in parallel with Rdfu1 pushes up to ~0.2 mA into the unpowered modem through its RX ESD diode.
|
||||
|
||||
This is a weaker version of the PA2 path, but it points the same way. The state that you saw revive the modem was an MCU reset, and in that state PA10 has no pull-up and only Rdfu1 remains. 0.5.5 does not reproduce that state after a Modo B window.
|
||||
|
||||
**Fix (1 line, both boards): done in 0.5.6** (commit `8d203df`, `relayDisengage()`). The standalone patch file was superseded and moved to `_to_delete/`.
|
||||
|
||||
If `DIN_4G` still reads > 0.3 V at rest after this fix, the remaining source is Rdfu1 itself. The next step would then be to drive PA10 LOW about 500 ms after stack-off, releasing it before `PWR_CTRL` goes HIGH, or to fit Rdfu1 as DNP.
|
||||
|
||||
### 8.2 V4-only: the console USB powers the board rail
|
||||
On V4, the Blackpill USB (the "USB de consola del HBOX" in the battery procedure) is VBUS wired straight onto `/SYS5V` (§4.1). This has three effects:
|
||||
- **In a battery run, the laptop (~5.0 V) beats the boost (4.79 V).** The laptop carries the MCU at rest and part or all of the stack in the window. The rails, `vbat`/`vbatmin` and the pass/fail result then describe the laptop, not the battery.
|
||||
- **With 24 V on, SD-15B current (~5.2 V) flows back into the laptop's port.**
|
||||
- On a USB-A port the stack's burst can trip the port, and the console disconnects mid-run.
|
||||
|
||||
**Until reworked, every V4 run with 24 V or a battery uses a console cable with VBUS blocked.** For example, a USB-A→C cable with Kapton over pin 1 of the A plug, or a cable with the red wire cut. It enumerates fine because the core does not use VBUS sensing.
|
||||
- **Check:** with no 24 V and no pack, plug it in. The Blackpill must stay dark.
|
||||
- **Flashing (DFU):** use a normal cable, with 24 V off.
|
||||
- **Permanent rework:** take the Blackpill's two 5V pins out of the sockets, and feed its 5V pin from SYS5V (e.g. K1 pin 2) through a Schottky (SS14). USB can then never reach SYS5V.
|
||||
|
||||
### 8.3 Before any V4 battery run (the board from `log1`)
|
||||
1. **Pack ≥ 3.8 V, charged externally.** 0.5.5 still keeps `CHRG_EN` high. At 3.14 V every wake is skipped (< 3.40 V), and the pack is being over-discharged.
|
||||
2. **With 24 V on, P2 must read 5.1–5.3 V.** `log1` showed 0.00 V at PA4.
|
||||
3. **simpleRTK2B switches: `IOREF=3.3V` OFF and `5V=OUTPUT` OFF.**
|
||||
- IOREF (MOD1.2) is fed from the board's always-on 3V3. With `IOREF=3.3V` ON, that 3V3 would be tied into the simpleRTK2B rail and would keep the F9P alive at rest: a strong back-feed.
|
||||
- **Diagnostic:** every wake must start with the F9P boot banner `$GNTXT,…u-blox AG…` (it does in the 13:14 log).
|
||||
4. **Do not press RESET during a battery run.** POR and PIN resets never start battery mode (unchanged in 0.5.5), so the unit goes silent until 5 V returns.
|
||||
|
||||
### 8.4 Rest-state readings: what they mean
|
||||
Take these with the stack OFF during a battery rest, ≥ 30 s after `DUTY reposo`, black probe on GND:
|
||||
|
||||
| Point (V4 = V5 refs) | Expected | If not |
|
||||
|---|---|---|
|
||||
| 3V3_MODEM (J7.1 / C43) | < 0.3 V | find the source with the next three rows |
|
||||
| SW_5V (MOD1.5 / C3) | < 0.3 V | U7 body diode is back-feeding from 3V3_MODEM, or USB on the simpleRTK2B |
|
||||
| DIN_4G (J7.3) | < 0.3 V | PA10 pull-up + Rdfu1: apply the §8.1 patch, then consider the PA10 clamp |
|
||||
| DOUT_4G (J7.2) | < 0.3 V | IOREF-side pull-up inside the simpleRTK2B. Rework: feed MOD1.2 (IOREF) from 3V3_MODEM instead of 3V3 |
|
||||
|
||||
**Text to add to the battery procedure/acta (Material / Configuración)**, for the owner of `hbox-docs` to paste:
|
||||
> **Tarjeta V4:** el USB del Blackpill está unido al riel de 5 V de la tarjeta. Para cualquier corrida con 24 V o con batería usar el **cable de consola con VBUS bloqueado** (cinta Kapton en el pin 1 del conector USB-A). Comprobación: sin 24 V y sin batería, al conectar ese cable el Blackpill queda apagado. Con cable normal la laptop alimenta la etapa y la prueba no vale. Para flashear sí se usa cable normal, con 24 V apagado.
|
||||
|
||||
---
|
||||
|
||||
## 9. Addendum (26-Sep, 16:00): firmware 0.5.6 on V4 — what is still open
|
||||
|
||||
**Binary checked.** `release/hbox-tarjetaV4_0.5.6.bin` has SHA-256 `1e604edd…ff95`, the same as `out-v4`, built from commit `8d203df`. Its strings read `0.5.6-fm`, `EinarBoard v4 (HBOX v4)` and `MODEM_RST`.
|
||||
|
||||
**Firmware review: no further change is needed for V4.**
|
||||
- PA9 and PA10 are released after Modo B (§8.1).
|
||||
- The `MODEM_RST` pulse behaves as intended. It fires 3 s after every `STACK ON` and lasts 500 ms, and stack-off cancels or releases it.
|
||||
- P7 is forced in every PCF8574 write, so LED updates cannot clobber the pulse.
|
||||
- The boot gauge walk writes bit 7 = 1, so P7 is released after any reset.
|
||||
- With no bodge fitted, the pulse drives a pin that is not connected, so it is harmless.
|
||||
|
||||
Everything still open is hardware or bench work.
|
||||
|
||||
### 9.1 The bodge: diode type matters
|
||||
**Position.** U13 pin 12 (P7) is unconnected on both boards (checked).
|
||||
- **V4:** U13.12 at (157.9, 90.7) → diode → SW4G1 pad 1 (`/MODEM_RST`) at (181.1, 99.7), ≈ 25 mm.
|
||||
- **V5:** ≈ 21 mm.
|
||||
|
||||
**Diode.** Use **BAT54**, the same part as D5 on the board, or another *small-signal* Schottky (BAT43, BAT85, 1N5711).
|
||||
- **Avoid 1N5819.** At rest P7 is high on its ~100 µA pull-up, so the diode is reverse-biased for the whole 8 minutes. Its leakage flows P7 → MODEM_RST → into the modem.
|
||||
- A large-area power Schottky leaks tens to hundreds of µA when hot, capped only by that pull-up.
|
||||
- That is the same order as the back-feed 0.5.5/0.5.6 just removed.
|
||||
- **1N4148** has no leakage problem, but drops 0.5–0.65 V. Keep it only if the `M` check reads < 0.4 V.
|
||||
- The published instructions list "BAT54 / 1N5819 (1N4148 sirve)". I suggest "BAT54 (o Schottky de señal); no 1N5819".
|
||||
|
||||
**Optional firmware refinement.** Hold P7 LOW for the whole stack-off period and release it 3 s after `STACK ON`, instead of the 500 ms pulse. The diode is then never reverse-biased at rest, and the modem's own reset pull-up helps bleed its rail toward 0 V. Not required.
|
||||
|
||||
### 9.2 simpleRTK2B switch `IOREF=3.3V` must be OFF (also `5V=OUTPUT` OFF)
|
||||
MOD1.2 (IOREF) is wired to the board's **always-on 3V3**. Per ArduSimple, the `IOREF=3.3V` switch ties IOREF to the simpleRTK2B's *own* 3.3 V rail. So with that switch ON, the board's 3V3 powers the **F9P during the whole rest**:
|
||||
- its TX2 idles high → K1 NC → `DIN_4G` → the modem rail;
|
||||
- roughly **0.3–0.4 W** comes out of the pack for the entire rest.
|
||||
|
||||
This is the only path that can keep the simpleRTK2B alive while `SW_5V` sits at ~0.6 V, apart from a USB cable on the simpleRTK2B. That fits candidate (1) in the 0.5.6 CHANGELOG.
|
||||
|
||||
**Checks:**
|
||||
- At rest, the simpleRTK2B **POWER** LED is off. Its **IOREF** LED stays on, which is normal.
|
||||
- Every wake starts with `$GNTXT,…u-blox AG…` on the console, meaning the F9P really rebooted.
|
||||
|
||||
The reset pulse revives the modem regardless of this switch. But if the F9P stays alive at rest, the duty-cycle battery budget is gone. This switch check is not in any procedure or checklist yet; it belongs in module integration.
|
||||
|
||||
### 9.3 Still pending from §8
|
||||
- **V4 console cable with VBUS blocked** (§8.2). The published battery procedure (fw 0.5.6) still says "USB del HBOX" with no V4 caveat.
|
||||
- **The `log1` pack (3.14 V) must be charged externally.** 0.5.6 is still `-fm` with the charger off.
|
||||
- **With 24 V on, P2 must read 5.1–5.3 V** (`log1` read 0.00 V).
|
||||
197
docs/historia/respaldo-bajo-consumo.md
Normal file
197
docs/historia/respaldo-bajo-consumo.md
Normal file
@ -0,0 +1,197 @@
|
||||
---
|
||||
tags: [project-drogheda, firmware, power, battery, thermal]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO
|
||||
date: 2026-09-09
|
||||
status: Draft — design proposal
|
||||
---
|
||||
|
||||
# Backup mode: duty-cycled low power, and what it does to the charging problem
|
||||
|
||||
!!! warning "Designators from before 22-Oct (T43)"
|
||||
This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the V5.6/V5.7 the LTC4040 is U14 (was U6); on the v6, U6 is the modem and the LTC4040 is U17. To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it.
|
||||
|
||||
> **Bottom line:** yes, do the duty-cycle. But **the wake interval barely matters — the
|
||||
> sleep floor is everything.** 10 min vs 60 min changes runtime by 2 %. Dropping the
|
||||
> sleeping draw from 150 mW to 20 mW changes it by **5.5×**. And the payoff is bigger than
|
||||
> runtime: at ~1.5 Wh/day the pack needs **~10 minutes of charging a day**, which the
|
||||
> narrow cool window we already have can supply. **The duty-cycle largely dissolves the
|
||||
> temperature problem** — that is the real reason to do it.
|
||||
|
||||
---
|
||||
|
||||
## 1. "Can't we just move the charge temperature?"
|
||||
|
||||
The **threshold** is ours (`RNTB`). The **limit** is the cell's. Charging an NMC/LCO 18650
|
||||
at 55 °C is not a tuning choice — it is accelerated SEI growth, gas generation and a safety
|
||||
liability inside a sealed IP67 box in full sun. Not something to ship on 30 units.
|
||||
|
||||
What you *can* legitimately buy, without changing the cell:
|
||||
|
||||
- **Lower the float.** Damage rate is temperature × voltage × time. You are already at
|
||||
4.00 V rather than 4.20 V, which is most of the win. `JP3`/`JP4` → F1=0/F0=0 gives
|
||||
**3.95 V** and a little more margin. That justifies nudging the hot pause to maybe
|
||||
**48–50 °C** *if the cell's datasheet supports it*. A few degrees, not fifteen.
|
||||
- Anything beyond that needs a cell rated for it — see the high-temp LFP note.
|
||||
|
||||
**But read §3 before deciding any of this.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Duty-cycled backup — the numbers
|
||||
|
||||
Pack ~43 Wh, 85 % usable = **36.5 Wh**. Boost efficiency 88 %.
|
||||
|
||||
**Today, continuous:** ~2.5 W at the 5 V rail → **~13 h**; with the fan running, ~8 h.
|
||||
|
||||
**Duty-cycled**, wake = 10 s at 1.5 W (modem held in PSM, no re-attach):
|
||||
|
||||
| sleep floor | 10 min | 30 min | 60 min |
|
||||
|---|---:|---:|---:|
|
||||
| 150 mW | 7.7 d | 8.5 d | 8.7 d |
|
||||
| 100 mW | 10.7 d | 12.4 d | 12.9 d |
|
||||
| 50 mW | 17.9 d | **23.0 d** | 24.7 d |
|
||||
| 20 mW | 29.8 d | **47.3 d** | 55.5 d |
|
||||
| 10 mW | 38.3 d | 73.1 d | 94.6 d |
|
||||
|
||||
If the carrier will not give us PSM and every wake is a full LTE re-attach (~35 s):
|
||||
|
||||
| sleep floor | 10 min | 30 min | 60 min |
|
||||
|---|---:|---:|---:|
|
||||
| 150 mW | 5.6 d | 7.5 d | 8.1 d |
|
||||
| 50 mW | 9.7 d | 16.9 d | 20.8 d |
|
||||
| 20 mW | 12.5 d | 27.3 d | **38.8 d** |
|
||||
|
||||
**Read the rows, not the columns.** Your 10-minute instinct costs 20–50 % against 30 min
|
||||
and buys the operator nothing they need. Go **30 min**, and spend the effort on the floor.
|
||||
|
||||
---
|
||||
|
||||
## 3. 🔴 The part that matters most — it also fixes the charging
|
||||
|
||||
| Duty cycle | Daily energy | Charging needed at 2.42 A |
|
||||
|---|---:|---:|
|
||||
| 30 min wake, 50 mW sleep | 1.59 Wh/day | **~10 min/day** |
|
||||
| 30 min wake, 20 mW sleep | 0.77 Wh/day | **~5 min/day** |
|
||||
| 10 min wake, 100 mW sleep | 3.41 Wh/day | ~22 min/day |
|
||||
|
||||
Today the pack must replace ~30 Wh after a long outage and we give it a charge window that
|
||||
closes at 42 °C. At **1.6 Wh/day** you need *ten minutes* of cool weather. Port nights drop
|
||||
well below 40 °C — the existing window is already enough.
|
||||
|
||||
So the ordering changes: **do the firmware work first.** It is free, it ships to the
|
||||
installed fleet over the existing update path, and it may retire the cell question entirely.
|
||||
Keep the high-temp LFP option open — but stop treating it as the first move.
|
||||
|
||||
It also means the 43 Wh pack is oversized. At 1.6 Wh/day a **single 12 Wh cell gives 6+
|
||||
days** — cheaper, cooler, smaller, easier to keep topped up.
|
||||
|
||||
---
|
||||
|
||||
## 4. What actually sets the sleep floor
|
||||
|
||||
Ranked by what I expect to find on the bench. **All of these need measuring, not assuming.**
|
||||
|
||||
| Suspect | Est. | Fix |
|
||||
|---|---:|---|
|
||||
| **`U17` AMS1117-3.3** — quiescent 5 mA typ / 11 mA max, and it burns 1.7 V × I as heat | **25–55 mW** | Swap for a low-Iq LDO. `U21` (AP2112K, 55 µA) is already in the BOM; a TPS7A02-class part is ~1 µA. **This is probably the single biggest item and it is a one-part change** |
|
||||
| **`K1` coil** — ~30 mA from SYS5V while the MCU is inserted | **150 mW** | See §5 |
|
||||
| **LTC4040 boost** at very light load — light-load efficiency and its own Iq | 10–40 mW? | Measure. If bad, this caps everything and needs a rethink |
|
||||
| Battery gauge LEDs `PB12–15`, `D17`, `D18`, `DB1–4`, `D4G1` | 50–150 mW | All off in backup, or one 50 ms blink per wake |
|
||||
| `U12` MP1584 → `/3V3_ETH` + `W5500` (~130 mA) | 400+ mW | Ethernet is useless on battery — disable `U12`, hold `W5500` in reset |
|
||||
| `MAX3232` charge pump, `SP3485`, `SC16IS752` | 5–15 mW | Shutdown pins / disable |
|
||||
| `U18` `V_BCKP` | **0.15 mW** | ✅ already right — see §6 |
|
||||
| MCU in STOP + LSE RTC | 0.1–0.2 mW | Achievable; today it is ~165 mW because `loop()` never stops |
|
||||
|
||||
⚠ **Today's `SLEEP` state is not a low-power state.** It cuts `PWR_CTRL` and then keeps
|
||||
spinning `loop()` at 84 MHz with Serial, I²C polling and the gauge alive. Real STOP-mode
|
||||
+ RTC wake is the substantive firmware change here. `Y4` (32.768 kHz) is on the board, so
|
||||
LSE-driven RTC wake is available.
|
||||
|
||||
---
|
||||
|
||||
## 5. `K1` — reverse the earlier call
|
||||
|
||||
The Firmware v3.1 brief said: *"bobina ~30 mA de SYS5V … se queda insertado; 30 mA contra
|
||||
12 Ah es ruido."* That was correct for a 13-hour continuous budget. **It is wrong for a
|
||||
duty-cycled one** — 150 mW against a 20–50 mW target is 3–7× the entire rest of the floor,
|
||||
and it alone would cap you at 8 days no matter what else you fix.
|
||||
|
||||
**New rule: `BYP_CTRL` low between wakes, high only during the TX window.** 150 mW × 10 s
|
||||
per 30 min = 0.8 mW average — it disappears. The fail-safe semantics are unchanged
|
||||
(de-energised = F9P→modem direct), and between wakes there is nothing to inject anyway.
|
||||
|
||||
Longer term, a **latching relay** (G6K-class, pulse to switch, zero hold) removes the
|
||||
question permanently. Worth pricing for the v6 BOM.
|
||||
|
||||
---
|
||||
|
||||
## 6. Don't chase position — you already have the right parts
|
||||
|
||||
The machine is **parked**. Its position does not change during an outage, so re-acquiring
|
||||
a fix every wake is pure waste.
|
||||
|
||||
- `V_BCKP` is already wired (`/3V3` via 1 kΩ + Schottky, 45 µA) and keeps ephemeris, so
|
||||
when you *do* want a fix it is a hot start (~2–5 s), not a 30 s cold start. Good design,
|
||||
already done.
|
||||
- Report the **stored** position with a `pwr=BATT` flag and a staleness marker. Take a real
|
||||
fix only every **N-th** wake (e.g. every 2 h) as an anti-theft check.
|
||||
- **`U26` (SC7A20 accelerometer) is on the v6 board.** Wire its interrupt to a wake-capable
|
||||
pin and you get motion-triggered wake for free: asleep while the machine is still, awake
|
||||
within a second if someone moves it. That is strictly better than polling GNSS.
|
||||
⚠ `U26` does not appear in any of the v6 briefs — confirm what it was placed for and that
|
||||
its INT line reaches an EXTI/wake pin before counting on this.
|
||||
|
||||
---
|
||||
|
||||
## 7. Modem: PSM before power-cycling
|
||||
|
||||
Power-cycling the modem every 10 minutes means paying a full network attach every 10 minutes
|
||||
— 20–30 s at 100–300 mA with 2 A bursts. **3GPP PSM keeps the module registered while it
|
||||
sleeps at tens of µA**, with no re-attach cost; eDRX is the lighter variant.
|
||||
|
||||
- The A7670SA (Cat-1bis) supports PSM. **Whether you get it is a Telcel question**, not a
|
||||
module question — it depends on the APN/plan and the network granting the requested TAU.
|
||||
**Verify this early; the whole table in §2 hinges on which column you land in.**
|
||||
- Fall back to `MODEM_PWR_EN` power-cycling only if PSM is refused — and then stretch the
|
||||
interval to 30–60 min so the attach cost amortises.
|
||||
|
||||
⚠ **Inrush risk.** You already tripped the LTC4040 `PFI` at 4.60 V on LTE inrush with no
|
||||
pack connected. Duty-cycling means taking that 2 A / 7.6 W burst **repeatedly, at low SoC,
|
||||
on a hot or aged pack**. Hold the ≥ 470 µF modem bulk, and bench the worst case: coldest
|
||||
SoC × hottest pack × attach burst. This is the most likely way the design fails in the field.
|
||||
|
||||
---
|
||||
|
||||
## 8. Proposed state machine
|
||||
|
||||
```
|
||||
RUNNING ──(5V_SENSE PA4 falls, debounced)──► BACKUP_ENTRY
|
||||
BACKUP_ENTRY: hot-start fix (or use last) → $PHLG{pwr=BATT, evt=POWER_LOSS} → TX now
|
||||
(operators care most in the first minute — do not make them wait 30)
|
||||
→ BACKUP_SLEEP: PWR_CTRL low · MODEM→PSM · BYP_CTRL low · U12/W5500 off
|
||||
transceivers shutdown · LEDs off · fan off · MCU STOP + RTC(T)
|
||||
→ wake on RTC(T) or accelerometer INT or PA4 rising:
|
||||
read vbat/tbat → if vbat < BAT_CRITICAL: TX final "shutting down" → deep off
|
||||
else: BYP_CTRL high → modem wake → TX heartbeat → back to BACKUP_SLEEP
|
||||
→ PA4 rising: full wake, resume RUNNING
|
||||
```
|
||||
|
||||
**Adaptive cadence** (cheap, and better product behaviour): 15 min for the first hour,
|
||||
30 min to 24 h, 60 min after. Include the **drain rate** in `$PHLG` so ops can see how long
|
||||
the unit has left rather than guessing.
|
||||
|
||||
**Fan: off in backup, full stop.** A 5 V fan is 0.75–1.25 W — it would consume the entire
|
||||
budget in hours. If the box is too hot to sit idle without a fan, that is a thermal problem,
|
||||
not a power-management one.
|
||||
|
||||
---
|
||||
|
||||
## 9. Do these three first
|
||||
|
||||
1. **Measure the sleeping current** on a v5.5 board with `PWR_CTRL` low, MCU in STOP, and
|
||||
the loads in §4 disabled one at a time. That single number decides everything in §2.
|
||||
2. **Ask Telcel about PSM/eDRX** on the current APN and plan. It sets which table applies.
|
||||
3. **Pull the `tbat`/`tntc` fleet histogram** — with §3's 10 min/day figure in hand, the
|
||||
question is no longer "is it ever cool enough" but "is it cool enough for ten minutes",
|
||||
which is a much easier bar and may already be met.
|
||||
161
docs/historia/respaldo-procedimiento-evaluacion.md
Normal file
161
docs/historia/respaldo-procedimiento-evaluacion.md
Normal file
@ -0,0 +1,161 @@
|
||||
---
|
||||
tags: [project-drogheda, test-plan, power, battery, firmware, backend]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO
|
||||
date: 2026-09-10
|
||||
status: Draft — evaluation procedure
|
||||
---
|
||||
|
||||
# Backup-power evaluation: the procedure
|
||||
|
||||
!!! warning "Designators from before 22-Oct (T43)"
|
||||
This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the V5.6/V5.7 the LTC4040 is U14 (was U6); on the v6, U6 is the modem and the LTC4040 is U17. To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it.
|
||||
|
||||
> **It is not a lot of things to do. It is three measurements and a decision.**
|
||||
> Everything else in the last three notes is *contingent* on them — most of that list
|
||||
> deletes itself once D3, P1 and P2 come back. Do not start firmware before Gate G1.
|
||||
|
||||
**Critical path:** D3 (requirement) → P1 (sleep floor) → P2 (modem) → F1 (firmware) →
|
||||
S1 (server) → decide the cell last. ≈ **3 weeks of elapsed time, ~7 person-days of work.**
|
||||
|
||||
---
|
||||
|
||||
## Phase D — desk, no hardware. 1 day. Kills most of the list.
|
||||
|
||||
| # | Task | Owner | Output |
|
||||
|---|---|---|---|
|
||||
| **D1** | Fleet `tbat`/`tntc` histogram from `htracker`: hourly distribution per unit over 30 d; **% of day above 42 °C**; longest continuous window **below** 40 °C; correlate `cflt` and charge events with those windows | backend | one chart + two numbers |
|
||||
| **D2** | Ask Telcel: is **PSM / eDRX** available on our APN and plan? What TAU / Active Timer will the network grant? | Leo | yes/no + granted timers |
|
||||
| **D3** | 🔴 **Pin the requirement.** From ICAVE ops / the contract, not from us: **how long must a unit keep reporting on battery, and what must it report?** | Leo | a number in hours, and a message spec |
|
||||
|
||||
**Gate G0 — do not proceed without D3.** Every number downstream is sized by it. If the
|
||||
answer is "report the outage and survive until someone attends, ~24 h", the whole problem
|
||||
is already solved by the firmware and the cell question closes. If it is "7 days of live
|
||||
tracking", that is a different product and needs a different pack.
|
||||
|
||||
### D1 query sketch
|
||||
```sql
|
||||
SELECT cell_id, date_trunc('hour', ts) AS h,
|
||||
avg(tbat) AS tbat_avg, max(tbat) AS tbat_max,
|
||||
sum((tbat > 42)::int)::float / count(*) AS frac_above_42
|
||||
FROM phlg WHERE ts > now() - interval '30 days' AND tbat > -100
|
||||
GROUP BY 1,2 ORDER BY 1,2;
|
||||
```
|
||||
Then: per unit, the longest run of consecutive hours with `tbat_max < 40`.
|
||||
**Success bar is not "always cool" — it is "≥ 15 min below 40 °C per day"** (see the
|
||||
duty-cycle brief §3). That is a much easier bar and may already be met.
|
||||
|
||||
---
|
||||
|
||||
## Phase P — bench. The measurement that decides everything. 2–3 days.
|
||||
|
||||
### Rig
|
||||
One v5.5 board, bench PSU at **3.7 V into `P4`** (pack position), current measured
|
||||
**at `BAT+`** — that is what the pack actually delivers, and it captures the LTC4040
|
||||
boost's light-load efficiency, which is one of our real unknowns. Measure the 5 V rail in
|
||||
parallel so you get both numbers and can derive the boost efficiency.
|
||||
|
||||
⚠ **A bench DMM will not do this.** You need µA-to-A dynamic range on one trace: a
|
||||
**Nordic PPK2** (~US$110) or Joulescope/Otii. Buy it — it pays for itself in this test
|
||||
alone, and every low-power decision after this depends on trusting the number.
|
||||
|
||||
### P1 — the subtraction ladder 🔴
|
||||
|
||||
Measure, then remove one load, measure again. The **deltas are the deliverable** — they
|
||||
tell you which fixes are worth writing and which are noise.
|
||||
|
||||
| Step | State | Expect |
|
||||
|---|---|---|
|
||||
| a | Full `RUNNING`, stack up, fan off | ~2.5 W baseline |
|
||||
| b | `PWR_CTRL` low (RTK stack off) | |
|
||||
| c | `BYP_CTRL` low (`K1` released) | **−150 mW** — confirms the K1 reversal |
|
||||
| d | `U12` disabled + `W5500` held in reset | **−400 mW?** |
|
||||
| e | All LEDs off (incl. gauge `PB12–15`) | −50…150 mW |
|
||||
| f | `MAX3232` / `SP3485` / `SC16IS752` in shutdown | −5…15 mW |
|
||||
| g | MCU in **STOP + LSE RTC** (not `loop()`) | −150 mW |
|
||||
| h | `U17` lifted, `/3V3` fed from bench supply through the meter | **isolates the AMS1117's own Iq — the number I most expect to be ugly** |
|
||||
|
||||
**Gate G1: is the achievable floor under 50 mW?**
|
||||
Yes → proceed to firmware. No → find out what is holding it up *before* writing any.
|
||||
If step (h) is 25–55 mW as expected, the LDO swap is a one-part change worth doing first.
|
||||
|
||||
### P2 — modem energy and the burst
|
||||
1. Real Telcel SIM. If D2 said yes: `AT+CPSMS=1,,,"<TAU>","<Active>"`. Measure **modem
|
||||
sleep current in PSM**, and **energy per wake→TX→sleep cycle** in joules.
|
||||
2. If D2 said no: measure **energy per full attach cycle** the same way.
|
||||
3. 🔴 **The burst test.** With a real pack at **low SoC** and the pack at **45 °C**,
|
||||
trigger an LTE attach and watch `MAIN5V` on a scope. Does `PFI` (4.60 V) trip?
|
||||
This is the most likely way the design fails in the field and it is cheap to check now.
|
||||
|
||||
**Gate G2:** PSM or power-cycle, and the interval locked. Feed the measured joules back
|
||||
into the runtime table.
|
||||
|
||||
---
|
||||
|
||||
## Phase F — firmware. 3–4 days, once G1 and G2 pass.
|
||||
|
||||
- `BACKUP_SLEEP` as a real state behind a compile flag (`ENABLE_BACKUP_DUTYCYCLE`), per the
|
||||
state machine in the duty-cycle brief §8. Today's `SLEEP` stays as the brownout failsafe.
|
||||
- Exercise it first in the **`hostsim` harness** already in the repo — state transitions,
|
||||
adaptive cadence, `BAT_CRITICAL` final message — before it touches silicon.
|
||||
- **F1 acceptance, on the bench:** real pack, real SIM, **72 h continuous** with the meter
|
||||
logging. Deliverable = **measured Wh/day** and extrapolated runtime.
|
||||
- **Gate G3: measured Wh/day within 30 % of the model.** If it is not, the model is wrong
|
||||
and everything downstream needs re-deciding — do not paper over it.
|
||||
|
||||
---
|
||||
|
||||
## Phase S — server-side synthesis. Your idea, and it is a good one. 1–2 days.
|
||||
|
||||
Holding the position server-side decouples what the customer sees from what the radio does.
|
||||
The device sends a heartbeat every 30–60 min; `retranslator.py` keeps feeding Wialon at
|
||||
1 Hz from the last known fix. That is the right split.
|
||||
|
||||
**But three rules, and they are not optional:**
|
||||
|
||||
1. **Mark it.** Synthesized points carry an explicit source flag and render differently in
|
||||
the UI. We are generating data that did not come from a sensor; anyone reading the track
|
||||
must be able to see that. Do not let a synthesized point be indistinguishable from a real one.
|
||||
2. 🔴 **Hard stop on a missed heartbeat.** The moment a heartbeat is late by more than one
|
||||
interval, **stop synthesizing** and switch the unit to *"last seen at X, T ago —
|
||||
unknown"*. Synthesizing past the heartbeat window turns a dead unit into a healthy-looking
|
||||
one on the operator's screen. That is strictly worse than reporting nothing, and it is
|
||||
exactly the failure this feature would be blamed for.
|
||||
3. **Movement breaks the illusion instantly.** The `U26` accelerometer wake exists for this:
|
||||
if the machine moves during an outage, the unit wakes and sends a real fix, and the server
|
||||
drops out of synthesis on the spot. Without motion-wake, a towed machine keeps reporting
|
||||
from the yard — with a port customer, that is a liability, not a bug.
|
||||
|
||||
**S1 test:** on the bench, cut vehicle power → verify Wialon shows a continuous 1 Hz track
|
||||
flagged as battery-mode → cut the unit entirely → **verify the track stops and the unit goes
|
||||
"unknown" within one heartbeat interval**, not later. Also check whether ICAVE's contract or
|
||||
Wialon billing counts records — synthesized ones may not be acceptable to count.
|
||||
|
||||
---
|
||||
|
||||
## Phase C — the cell decision, *last*
|
||||
|
||||
Only now, with **measured Wh/day** (F1) and the **real temperature histogram** (D1):
|
||||
|
||||
- **Daily energy < ~2 Wh and D1 shows ≥ 15 min/day below 40 °C** → **keep the current cell.**
|
||||
No LFP, no `RNTB` change, no new case. Consider *shrinking* the pack instead.
|
||||
- **Otherwise** → high-temp LFP + `RNTB` → 5.6 k + B3380 NTC + `JP2` re-strap, per the
|
||||
battery note. Order sample cells at D1 time so they are on the shelf if needed — that is
|
||||
the only long-lead item, and sampling two cells costs nothing.
|
||||
|
||||
---
|
||||
|
||||
## Phase V — field pilot. 30 days.
|
||||
|
||||
Two units at ICAVE on the new firmware, rest of the fleet as control. Track: actual outage
|
||||
events, hours survived, `vbat` trajectory, charge-window utilisation, and any operator
|
||||
complaint about the synthesized track. **Ship to the fleet only after 30 clean days.**
|
||||
|
||||
---
|
||||
|
||||
## What this is *not*
|
||||
|
||||
Not a v6 respin. Nothing above requires new hardware except a possible one-part LDO swap
|
||||
and, if Phase C says so, a cell and one resistor. The 30-unit build is not blocked by any
|
||||
of it — but **D3 should be answered before that build ships**, because it may change what
|
||||
pack goes in the box.
|
||||
63
docs/historia/v5-auditoria-emc.md
Normal file
63
docs/historia/v5-auditoria-emc.md
Normal file
@ -0,0 +1,63 @@
|
||||
# EinarBoard v5.5 — Auditoría EMC y fundamentos de layout (2026-08-22)
|
||||
|
||||
Contra el `.kicad_pcb` enviado a fabricar (15 pzas). Objetivo: decidir qué se corrige **antes de
|
||||
la segunda corrida** y qué pasa al diseño v6 (ZED-F9P + 4G on-board). Stackup: F.Cu señal+pour
|
||||
GND · In1 plano GND · In2 plano 3V3 · B.Cu señal+pour GND — topología correcta de 4 capas. ✔
|
||||
|
||||
## 🔴 Crítico — corregir ANTES de la segunda corrida
|
||||
|
||||
**1. `/SW_BAT` (nodo de conmutación del LTC4040) es una sola pista de 0.3 mm × ~15 mm.**
|
||||
U6 pines SW → L6 (inductor 2.2 µH/8 A) va todo en 0.3 mm por F.Cu. En modo respaldo (boost desde
|
||||
batería) ese nodo lleva la corriente completa del inductor: con 5 V/2.5 A de carga desde 3.7 V son
|
||||
**~4 A promedio** — una pista de 0.3 mm/1 oz se calienta decenas de °C y tira tensión. El
|
||||
datasheet del LTC4040 pide SW ancho y corto. *En la corrida actual:* funcionará en respaldo ligero;
|
||||
vigilar en banco el caso "apagón + ráfaga TX del 4G". *Corrección:* polígono o pista ≥2 mm.
|
||||
|
||||
**2. `/BAT_PLUS` — los últimos ~20 mm hacia U6/C35 quedan en 0.3–0.5 mm.**
|
||||
El tramo largo P4→(columna x≈193) va bien (1.0 mm en B.Cu + paralelo In2), pero la aproximación
|
||||
final a los pads BAT es una cadena de 0.3 mm. Misma corriente que SW_BAT (2.4 A carga / ~4 A
|
||||
descarga). *Corrección:* ensanchar a ≥1.5 mm + vías múltiples en el último tramo.
|
||||
|
||||
## 🟠 Importante — corregir en la segunda corrida si se puede, obligatorio en v6
|
||||
|
||||
**3. Lazo de retroalimentación del buck del ventilador (U2):** el divisor R20/R21 está a ~25 mm
|
||||
del pin FB y la pista `/FB_FAN` recorre **60.7 mm**, con un punto a ~0 mm del cobre de `/SW_FAN`
|
||||
(no es corto — DRC limpio — pero es acoplamiento directo del nodo ruidoso al nodo de alta
|
||||
impedancia). Riesgo: jitter/rizado en 5V_FAN y EMI radiada. *Corrección:* R20/R21 pegados al pin 4,
|
||||
FB corto y lejos de SW. (U7 y U12 están decentes: FB 8–9 mm, separación ≥2 mm.)
|
||||
|
||||
**4. Lazos calientes de entrada de los bucks:** Cin más cercano a 5–8.5 mm del pin VIN
|
||||
(U2→C11 6.3 mm · U7→C40 5.1 mm · U12→C101 8.5 mm). Regla: <3 mm. *Corrección:* un 1 µF+10 µF
|
||||
pegados a VIN-GND de cada MP1584.
|
||||
|
||||
**5. ESD del puerto DFU lejos del conector:** D14/D15/D16 (PESD) están a ~41 mm de J11, junto al
|
||||
MCU. Clampean, pero el tramo conector→clamp radia durante la descarga. *v6:* moverlos a <10 mm
|
||||
de J11. **J9 (USB del módem) no tiene ESD** — es arnés interno, aceptable, pero añadirlo en v6.
|
||||
|
||||
## 🟡 Menor — anotar para v6
|
||||
|
||||
- **Cristales lejos de sus chips:** Y3 HSE a 11.9 mm de U16, Y4 LSE a 15.2 mm, Y1 a 12.3 mm de
|
||||
U11. El plano GND de In1 blinda lo que cruza por capas inferiores (verificado: los cruces de
|
||||
/KCOIL, /I2C, /MAIN5V van bajo el plano), así que funciona — pero en v6 pegarlos a <5 mm y en
|
||||
paquetes 3225 en vez de HC-49.
|
||||
- **Par ETH RX desbalanceado:** RXP 16.1 mm vs RXN 23.6 mm (Δ7.5 mm). 100BASE-TX lo tolera de
|
||||
sobra funcionalmente; en v6 rutear como par acoplado y igualar.
|
||||
- Celdas de 20 mm sin vía de costura: 8 (mayoría bajo el zócalo del Ardusimple). Densidad global
|
||||
buena: 165 vías GND, mediana vecino-más-cercano 4.6 mm. ✔
|
||||
- Stub único: cola de 5.6 mm de /3V3 en In2 — es el propio plano de 3V3, inofensivo.
|
||||
|
||||
## ✔ Verificado y sano
|
||||
|
||||
Desacoplos: 12 caps ≤4 mm de su pin; los "lejanos" (C78/C30/C10) son bulk de entrada junto a los
|
||||
conectores — colocación correcta. Pares USB FS: J9 Δ1.5 mm, J11 ok — Full-Speed lo permite.
|
||||
Protección 24 V: D10 (SMBJ33A) a 9.9 mm de P1 y Q10 a 11.7 mm — bien. Anchos de potencia
|
||||
restantes: /SW_5V 1.0 mm continuo, /24V_IN 1.0 mm, /FAN_RET 1.0 mm ✔ (los mínimos de 0.25 en
|
||||
VEH5V/MAIN5V/3V3 son taps de sensado, no portadores). Zona keepout bajo el magjack presente ✔.
|
||||
Costura GND y planos: correctos.
|
||||
|
||||
## Para el arranque de v6 (ZED-F9P + 4G on-board)
|
||||
|
||||
Heredar de esta lista: SW/BAT anchos desde el día uno, Cin en el pin, FB corto, cristales 3225
|
||||
pegados, ESD en cada conector externo, pares diferenciales acoplados (el F9P añade USB y el 4G
|
||||
añade USB/UART de alta velocidad + **RF**: la antena LTE exige keepout de cobre, 50 Ω controlada
|
||||
y tierra sólida — eso define el stackup antes de colocar nada).
|
||||
75
docs/historia/v5-correcciones.md
Normal file
75
docs/historia/v5-correcciones.md
Normal file
@ -0,0 +1,75 @@
|
||||
# EinarBoard v5.5 — correcciones aplicadas (2026-08-20)
|
||||
|
||||
Aplicadas directamente sobre `hbox-watchdog-v5.kicad_sch` + `.kicad_pcb` (tu guardado de las
|
||||
08:42, "V55"). **Checkpoint git previo:** commit "checkpoint: v5.5 as saved by Leo before Claude
|
||||
fixes" — todo es reversible con `git checkout`.
|
||||
|
||||
## Qué se corrigió
|
||||
|
||||
**1. Q1 / Q2 / Q4 / Q10 — pines renumerados y pads con red.**
|
||||
Esquemático: Q1 → `Q_PMOS_GSD`, Q2/Q4 → `Q_NMOS_GSD`, Q10 → `Q_PMOS_GDS` (clones embebidos con
|
||||
la misma geometría: ningún cable se movió). PCB: los 12 pads ya tienen su red
|
||||
(Q1: G=/GATE_LS S=/SW_IN D=/SW_5V · Q2: G=/FAN_GATE S=GND D=/FAN_RET · Q4: G=/EXT_LED S=GND
|
||||
D=/LED_K · Q10: G=/Q10_GATE D(tab)=/24V_IN S=/24V). Los 3 shorting + 3 mask-bridge de Q4
|
||||
desaparecieron del DRC.
|
||||
|
||||
**2. DzG1 — el clamp del gate ahora existe.**
|
||||
Símbolo → `D_Zener_ANK` (pinout real BZX84: 1=A, 2=NC, 3=K). PCB: pad 1=/Q10_GATE, pad 3=/24V,
|
||||
pad 2 sin red. **Ruteado nuevo:** /24V ahora entra al cátodo (pad 3) por el sur; /Q10_GATE une
|
||||
Rrp1.1 → DzG1.1 → **gate de Q10** (red completa — antes ni un pad de Q10 tenía cobre). El tramo
|
||||
viejo que moría en el pin NC se eliminó.
|
||||
|
||||
**3. U13 → SOIC-16 angosto (3.9×9.9), el paquete real del PCF8574T (C7605).**
|
||||
Pads a ±2.475 mm, gráficos/courtyard escalados, y las 11 pistas existentes extendidas a los pads
|
||||
nuevos. La pista /BAT_PLUS que pasaba bajo el cuerpo viejo (chocaba con el pad 9 nuevo) se
|
||||
redirigió por el corredor entre filas de pads. El 3D ahora coincide.
|
||||
|
||||
**4. MP1584 ×3 — pad expuesto a GND.**
|
||||
Símbolo con pin 9 "EP" (apilado sobre GND); pads 9 de U2/U7/U12 → GND (zone_connect solid).
|
||||
Vías térmicas: U2 ×4, U12 ×2 (en x=142.7, esquivando una pista interna /IGN_IN en In1 que pasa
|
||||
justo bajo el EP), **U7 ×0** — el B.Cu bajo U7 está lleno de RS232/SW3_NODE; queda enfriando
|
||||
sólo por el pour de F.Cu. ⚠ Pendiente para ti: liberar cobre bajo U7 o aceptarlo.
|
||||
|
||||
**5. Planos internos renombrados** (redes intactas): In1 = `GND_PLANE_IN1` (GND),
|
||||
In2 = `P3V3_PLANE_IN2` (/3V3).
|
||||
|
||||
**6. Rig2 agregado al esquemático** (22k, IGN_SENSE→GND, junto a Rigb1) y enlazado a la huella.
|
||||
Ya sale en BOM y sobrevive al sync. ⚠ Decisión pendiente: la pata baja quedó 22k ∥ 3.3k = 2.87k
|
||||
(24 V → 2.77 V en PA5, funciona); la doc dice 22k/10k. Si quieres 10k, cambia Rigb1.
|
||||
|
||||
## Estado de verificación
|
||||
|
||||
- **ERC:** mismo único error pre-existente ("Label not connected" @ (12.9, 6.4) mm — revísalo);
|
||||
las advertencias nuevas "not found in library" son los clones embebidos (inofensivas).
|
||||
- **DRC: 0 cortos.** Los 65 "errores" restantes son casi todos mi cobre nuevo contra el **relleno
|
||||
viejo de las zonas** (el archivo guarda el pour de antes de los cambios). **Al abrir pcbnew:
|
||||
presiona B (rellenar zonas) y corre DRC** — deberían quedar: los 3 clearance de 0.14 mm tuyos
|
||||
(§abajo), los 3 NPTH de los barrenos M2.5 en el courtyard de MOD1 (waivable, por diseño), y
|
||||
verificar que el HV24 del tab de Q10 y el "starved thermal" @ (60.2, 40.3) se limpien con el
|
||||
refill.
|
||||
- Chequeo geométrico independiente: **cero solapes nuevos entre redes distintas** vs. la placa
|
||||
original (los 6 pre-existentes son tuyos: vías bajo pads de Y2/Y3/C94/C102 y el par
|
||||
TP1/CHRG_EN — revísalos alguna vez, no son de hoy).
|
||||
|
||||
## Lo que te queda por rutear (censo real, geometría corregida)
|
||||
|
||||
Tu "28 unconnected" es esto (pours excluidos; GND/3V3 van por plano):
|
||||
|
||||
- **Potencia 24 V (lo más importante):** `P1.1 → D10 → Q10.2 (tab /24V_IN)` y `Q10.3 (/24V) →
|
||||
C22/C10/C11/U2.7/Ren1` — el FET de polaridad inversa sigue sin cobre de potencia. Pista ancha.
|
||||
- **Q1 (corte de 5 V):** /SW_IN F1.2→Q1.2 · /SW_5V Q1.3→U7 · /GATE_LS R2/Q5→Q1.1
|
||||
- **Q2 (fan):** /FAN_GATE Rg1/Rgs1→Q2.1 · /FAN_RET D2.2→Q2.3 · **Q4:** /LED_K D1.1→Q4.3
|
||||
- **Ethernet:** /3V3_ETH (3 islas), /ETH_RXP, /ETH_RXN, /ETH_RCT, /ETH_TXP, /ETH_ACTLED
|
||||
- **LTC4040:** /VEH5V (3 islas, incluye JP1.2), /MAIN5V (isla trk), /CHRG_FAULT, /PROG→R50,
|
||||
/F2_SEL→JP2.2, /SYS5V (3 islas: R2.2 | U12 | F1.1/D6.1)
|
||||
- **Varios:** /BATLED1 U13.4→RB1.1 · /BYP_CTRL · /DFU_DM, /DFU_VBUS · /KCOIL D6.2 · /MODEM_RST
|
||||
SW4G1 · /NRST · /RS485_A, /RS485_B · /DIN_4G, /USB_DP, /USB_VBUS (colas sueltas) ·
|
||||
los 3 clearance de 0.14 mm cerca de C31/RS1/JP2 (mueve la pista 10 µm)
|
||||
|
||||
## Notas técnicas (para la próxima sesión)
|
||||
|
||||
- El MCP de KiCAD se cayó una vez al pedirle refill de zonas (riesgo documentado del backend
|
||||
SWIG) — se recuperó solo; el archivo nunca se corrompió. Refill mejor en la GUI.
|
||||
- La rotación de huellas en `.kicad_pcb` es **horaria** con Y hacia abajo (transformada R(−θ)):
|
||||
calibrado contra 327 pads (200 aciertos vs 129). Los reportes de esta mañana tenían posiciones
|
||||
especulares para huellas a ±90° — el censo de arriba ya está corregido.
|
||||
171
docs/historia/v5-pre-auditoria.md
Normal file
171
docs/historia/v5-pre-auditoria.md
Normal file
@ -0,0 +1,171 @@
|
||||
# EinarBoard v5 (`hbox-watchdog-v5`) — pre-audit checks
|
||||
|
||||
**Fecha:** 2026-08-20 · **Archivos analizados:** `hbox-watchdog-v5.kicad_sch` (guardado 18-Ago 04:22)
|
||||
y `hbox-watchdog-v5.kicad_pcb` (guardado 20-Ago 02:43) · **Placa:** 155.0 × 100.1 mm, 229 huellas,
|
||||
1852 segmentos, 199 vías, 5 zonas.
|
||||
|
||||
> Alcance: sólo las dudas planteadas (Q1/Q2/Q4/Q10, Q2 sin señales, capacitores sueltos,
|
||||
> los 12 errores de DRC, U13 en 3D). La auditoría completa va aparte.
|
||||
|
||||
---
|
||||
|
||||
## 1. Q1, Q2, Q4, Q10 — pads sin red: causa raíz
|
||||
|
||||
**No es un problema de ruteo. Es un desajuste símbolo↔huella.**
|
||||
|
||||
Los cuatro usan `Device:Q_PMOS` / `Device:Q_NMOS`, cuyos pines están numerados con **letras**
|
||||
(`G`, `D`, `S`). Las huellas `SOT-23` y `TO-252-2` tienen pads **`1`, `2`, `3`**.
|
||||
Ningún número coincide → KiCad no puede mapear ningún pin a ningún pad → **los tres pads quedan
|
||||
sin red y sin `pinfunction`**. Por eso aparecen "unconnected" y sin etiqueta.
|
||||
|
||||
Q5/Q6 (`Transistor_BJT:Q_NPN_BEC`, pines 1/2/3) y Q7 (`Drogheda-sym:AO4406A`) sí mapean bien.
|
||||
|
||||
### El esquemático SÍ está correcto — estas son las redes que les tocan
|
||||
|
||||
| Ref | Parte | Encapsulado | Gate | Source | Drain |
|
||||
|-----|-------|-------------|------|--------|-------|
|
||||
| **Q1** | AO3401A (P-ch) | SOT-23 | `/GATE_LS` | `/SW_IN` | `/SW_5V` |
|
||||
| **Q2** | AO3400A (N-ch) | SOT-23 | `/FAN_GATE` | `GND` | `/FAN_RET`|
|
||||
| **Q4** | AO3400A (N-ch) | SOT-23 | `/EXT_LED` | `GND` | `/LED_K` |
|
||||
| **Q10** | IRFR5305 (P-ch)| TO-252-2 | `/Q10_GATE` | `/24V` | `/24V_IN` |
|
||||
|
||||
*(Q10 D=24V_IN / S=24V es lo correcto para protección de polaridad inversa: el diodo de cuerpo
|
||||
conduce con polaridad buena y bloquea con polaridad invertida.)*
|
||||
|
||||
### Corrección
|
||||
|
||||
Los símbolos de reemplazo tienen **la misma geometría de pines** que los actuales, así que es un
|
||||
cambio "drop-in": los cables y las etiquetas del esquemático se quedan donde están.
|
||||
|
||||
| Ref | Cambiar de | Cambiar a | Numeración resultante |
|
||||
|-----|-----------|-----------|----------------------|
|
||||
| Q1 | `Device:Q_PMOS` | **`Transistor_FET:Q_PMOS_GSD`** | 1=G, 2=S, 3=D ✔ AO3401A |
|
||||
| Q2 | `Device:Q_NMOS` | **`Transistor_FET:Q_NMOS_GSD`** | 1=G, 2=S, 3=D ✔ AO3400A |
|
||||
| Q4 | `Device:Q_NMOS` | **`Transistor_FET:Q_NMOS_GSD`** | 1=G, 2=S, 3=D ✔ AO3400A |
|
||||
| Q10 | `Device:Q_PMOS` | **`Transistor_FET:Q_PMOS_GDS`** | 1=G, 2=D(tab), 3=S ✔ IRFR5305 DPAK |
|
||||
|
||||
Después: *Tools → Update PCB from Schematic*. **Esto también elimina 6 de los 12 errores de DRC**
|
||||
(ver §5).
|
||||
|
||||
---
|
||||
|
||||
## 2. DzG1 (BZX84C15) — el clamp de gate NO existe físicamente ⚠ CRÍTICO
|
||||
|
||||
`DzG1` usa `Device:D_Zener` (2 pines: 1=K, 2=A) sobre `Package_TO_SOT_SMD:SOT-23` (3 pads).
|
||||
KiCad mapeó K→pad 1 y A→pad 2, dejando el pad 3 sin red.
|
||||
|
||||
**El BZX84C15 real en SOT-23 es: pad 1 = Ánodo · pad 2 = NC · pad 3 = Cátodo.**
|
||||
|
||||
Consecuencia en la placa fabricada:
|
||||
|
||||
- `/24V` llega al **ánodo** (pad 1).
|
||||
- `/Q10_GATE` llega a un **pin NC** (pad 2) → no va a ningún lado.
|
||||
- El **cátodo** (pad 3) queda flotando.
|
||||
- → **El zener no clampa nada.**
|
||||
|
||||
Y eso importa: `Rrp1` (10 k) jala `/Q10_GATE` a GND mientras el source de Q10 está en 24 V.
|
||||
Sin clamp, **Vgs = −24 V** contra el máximo del IRFR5305 de **±20 V** → ruptura de óxido de compuerta
|
||||
en cuanto se energice en una máquina de 24 V (RTG, Reach Stacker, Empty Handler son todas 24 V).
|
||||
|
||||
**Corrección:** cambiar el símbolo a **`Diode:BZX84Cxx`** (300 mW zener SOT-23; pin 1=A, 2=NC, 3=K)
|
||||
y conectar **pin 3 (K) → `/24V`**, **pin 1 (A) → `/Q10_GATE`**.
|
||||
|
||||
---
|
||||
|
||||
## 3. U13 (PCF8574) — el 3D tiene razón, la huella está mal
|
||||
|
||||
- Huella en la placa: `Package_SO:SOIC-16W_7.5x10.3mm_P1.27mm` (cuerpo **ancho, 7.5 mm**).
|
||||
- Modelo 3D asignado: `SOIC-16_3.9x9.9mm_P1.27mm.step` (cuerpo **angosto, 3.9 mm**).
|
||||
- Parte real: **LCSC C7605 = NXP PCF8574T/3,518**, encapsulado NXP **SOT109-1 = SO16, ancho de
|
||||
cuerpo 3.9 mm, largo 9.9 mm, paso 1.27 mm**.
|
||||
|
||||
LCSC lo etiqueta "SOIC-16-300mil", que es lo que engañó; la hoja de datos de NXP y la huella
|
||||
sugerida por la base JLCPCB coinciden en el **angosto**.
|
||||
|
||||
**Corrección:** U13 → `Package_SO:SOIC-16_3.9x9.9mm_P1.27mm`.
|
||||
Es un cambio de huella, así que hay que **volver a rutear la zona de U13** (el ancho de pads a pads
|
||||
baja de ~10.3 mm a ~6.0 mm).
|
||||
|
||||
---
|
||||
|
||||
## 4. MP1584EN ×3 (U2, U7, U12) — pad expuesto sin red ⚠
|
||||
|
||||
El símbolo `Drogheda-sym:MP1584EN` tiene 8 pines; la huella `SOIC-8-1EP_3.9x4.9mm_P1.27mm_EP2.29x3mm`
|
||||
tiene **pad 9 (exposed pad)**. En los tres reguladores el pad 9 está **sin red**.
|
||||
|
||||
En el MP1584 el EP es el camino térmico a GND. Tres bucks de 3 A sin disipación, dentro de una
|
||||
caja ASA sellada, dentro de un gabinete ABS IP67 sellado — es literalmente el modo de falla del
|
||||
RCA de junio.
|
||||
|
||||
Comparar: U6 (LTC4040, QFN-24-1EP) **sí** tiene su pad 25 = `GND_EP` correctamente conectado.
|
||||
|
||||
**Corrección:** agregar pin 9 (`EP`, tipo `power_in`) al símbolo `Drogheda-sym:MP1584EN`, atarlo a
|
||||
GND en el esquemático, y poner un campo de vías térmicas bajo cada EP.
|
||||
|
||||
---
|
||||
|
||||
## 5. Los 12 errores de DRC (del archivo guardado)
|
||||
|
||||
| # | Tipo | Ubicación (mm) | Causa |
|
||||
|---|------|----------------|-------|
|
||||
| 1–3 | `clearance` 0.140 vs 0.150 mm | (161.65, 120.39) · (150.21, 120.39) · (150.21, 115.13) | Cerca de C31 / RS1 / JP2 — faltan **10 µm**. Empujar la pista. |
|
||||
| 4–6 | `shorting_items` `/EXT_LED` ↔ *(sin red)* | (124.04, 51.44) · (123.62, 51.86) ×2 | **Q4** — es el §1. Se corrige solo. |
|
||||
| 7–9 | `solder_mask_bridge` | (123.62, 51.86) · (124.04, 51.44) · (122.25, 53.23) | Misma causa: **Q4**. Se corrige solo. |
|
||||
| 10–12 | `npth_inside_courtyard` | MH9 (77.90, 81.64) · MH7 (76.63, 129.90) · MH10 (128.70, 96.88) | Los barrenos M2.5 caen dentro del courtyard de **MOD1** (`Module:Arduino_UNO_R3`). Es **por diseño** (bolt-down del shield). Excepcionar la regla o recortar el courtyard de MOD1. |
|
||||
|
||||
**Neto: 6 de 12 desaparecen al arreglar Q4. Los 3 de MH son waivables. Sólo 3 son ruteo real.**
|
||||
|
||||
Advertencias que valen la pena (no errores): `track_dangling` 27, `via_dangling` 20,
|
||||
`lib_footprint_mismatch` 85, `text_height` 144, `silk_over_copper` 11.
|
||||
|
||||
---
|
||||
|
||||
## 6. Planos internos — nombre y red intercambiados
|
||||
|
||||
| Zona (nombre) | Capa | Red asignada |
|
||||
|---|---|---|
|
||||
| `P3V3_PLANE_IN1` | In1.Cu | **GND** |
|
||||
| `GND_PLANE_IN2` | In2.Cu | **/3V3** |
|
||||
|
||||
Los nombres y las redes están cruzados. Si las redes son las buenas, sólo hay que renombrar las
|
||||
zonas; si los nombres son los buenos, el stackup está invertido. Cualquiera de las dos, hay que
|
||||
resolverlo antes de generar plots — quien lea `In1`/`In2` va a asumir lo que dice el nombre.
|
||||
|
||||
---
|
||||
|
||||
## 7. Rig2 existe sólo en el PCB (no está en el esquemático)
|
||||
|
||||
`Rig2` (22 k, `/IGN_SENSE` → `GND`) es una huella **sin símbolo** y con redes asignadas a mano
|
||||
(no tiene `path` al esquemático). Es la única de las 9 huellas huérfanas que no es un barreno.
|
||||
|
||||
Efectos:
|
||||
|
||||
- No aparece en el BOM.
|
||||
- El próximo *Update PCB from Schematic* con «delete extra footprints» **la borra**.
|
||||
- La pata baja del divisor de ignición queda entonces sólo con `Rigb1` (3.3 k).
|
||||
|
||||
Cálculo del divisor a `/IGN_SENSE` → PA5 (pin 21 del F411RET6), con `Rig1` = 22 k arriba:
|
||||
|
||||
| Escenario | Pata baja | 24 V → PA5 | 12 V → PA5 |
|
||||
|---|---|---|---|
|
||||
| Con Rig2 | 22 k ∥ 3.3 k = 2.87 k | 2.77 V | 1.39 V |
|
||||
| Sin Rig2 | 3.3 k | 3.13 V | 1.57 V |
|
||||
|
||||
Ambos quedan bajo 3.3 V (y `D5` BAT54 clampa de todos modos), así que **no es un paro de línea** —
|
||||
pero sí es una discrepancia de BOM y contra la documentación, que dice divisor **22k/10k**.
|
||||
Decidir cuál es el valor bueno y meterlo al esquemático.
|
||||
|
||||
---
|
||||
|
||||
## 8. Pendiente: la lista exacta de "unconnected"
|
||||
|
||||
El `.kicad_pcb` en disco es de las **02:43 de hoy**. Contra ese archivo cuento ~100 pads de señal
|
||||
sin cobre encima — no 28. Además **las 5 zonas tienen el relleno viejo**: los pads de GND quedan a
|
||||
~0.62 mm del pour, sin thermal spokes.
|
||||
|
||||
Es decir: **el archivo guardado va atrás de tu sesión viva de KiCad.**
|
||||
|
||||
Para darte la lista exacta de los 28 y de cada capacitor suelto con su red de destino:
|
||||
**Ctrl+S en pcbnew** (KiCad rellena las zonas al guardar / al correr DRC) y vuelvo a correr el
|
||||
análisis. Ningún capacitor de la placa está *sin red asignada* — todos tienen su net; es puramente
|
||||
cuestión de ruteo, así que en cuanto vea el archivo fresco te doy pad por pad a dónde va cada uno.
|
||||
122
docs/historia/v57-eco-fuente-v01.md
Normal file
122
docs/historia/v57-eco-fuente-v01.md
Normal file
@ -0,0 +1,122 @@
|
||||
# EinarBoard V5.7 — ECO: fuente conmutada 24 V → 5 V a bordo (sin SD-15B externa) · borrador v0.1
|
||||
|
||||
!!! warning "Designadores de antes del 22-oct (T43)"
|
||||
Este documento conserva los designadores que tenía la tarjeta en su fecha. Desde el 22-oct (T43) la V5.6, la V5.7 y la v6 traen otros, y varios nombres viejos ahora son otra pieza: en la V5.6 y la V5.7 el LTC4040 es ahora U14 (antes U6) y J7 es el USB-C de DFU (antes J11). Para leerlo contra la tarjeta de hoy, usa la [tabla viejo → nuevo](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo); lo que ya no existía el 22-oct no está en la tabla.
|
||||
|
||||
**Fecha:** 12-oct-2026 · **Base:** V5.6 (`hbox-watchdog-v5.*` con acelerómetro y sin P12, 09-oct) · **Referencia:** bloque de entrada de la v6 (`hbox-watchdog-v6.kicad_sch` 30-sep, netlist verificada 08-oct) · **Autor:** Claude para Leo / Michelle · **Estado:** borrador para decidir alcance; no se ha tocado ningún archivo KiCad de V5.7.
|
||||
|
||||
## 0. Qué cambia y por qué
|
||||
|
||||
Hoy el HBOX recibe **24 V del gabinete** por el GX16 (P1) y un módulo **Mean Well SD-15B-05** dentro del HBOX lo convierte a 5 V, que entra a la tarjeta por **P2**. Los 24 V en la tarjeta solo alimentan el buck del ventilador (U2).
|
||||
|
||||
V5.7 trae esa conversión a la tarjeta, con el mismo bloque que ya está diseñado y auditado en la v6 (U25 TPS54360B). Ventajas concretas:
|
||||
|
||||
| | Hoy (V5.6) | V5.7 |
|
||||
|---|---|---|
|
||||
| Módulos en el HBOX | tarjeta + SD-15B + paquete | tarjeta + paquete — **el hueco del SD-15B queda para el paquete 26650 LFP** |
|
||||
| Cable interno 5 V (SD-15B → P2) | sí — el cruce de GX16/arnés destruyó la V5-006 | no; un solo par 24 V + ignición en el GX16 |
|
||||
| Riel 5 V | 5.3 V ajustado a mano con el potenciómetro del SD-15B; 5.57 V y 5.7 V medidos en campo (fuera de rango del LTC4040) | **5.32 V fijos** por divisor (R121/R122), sin ajuste |
|
||||
| Plegado de la fuente con el cargador (incidente 10-ago) | mitigado por RS1 10 mΩ en V5.6 | igual; además el buck tiene límite de corriente propio |
|
||||
| Costo | ~US$12 el SD-15B + conector + cable | ~US$4 en partes LCSC |
|
||||
| Calor dentro del HBOX | SD-15B (~82 % eficiencia) | buck síncrono-no (~88–90 %): ~0.3 W menos |
|
||||
|
||||
Lo que **no** cambia: LTC4040 y paquete, RS1/R50/R55 de V5.6, modem buck U7, MCU, puertos, contorno 140 × 100 mm.
|
||||
|
||||
## 1. Bloque a copiar de la v6 (valores verificados en la netlist v6)
|
||||
|
||||
```
|
||||
P1 GX16 pin 1 (24V_IN) ─ F2 fusible NANO2 3 A en portafusible ─ VIN_FUSED ─ D10 SMBJ33A a GND
|
||||
─ Q10 IRFR5305 (polaridad inversa; DzG1 15 V + Rrp1 10 k en compuerta, ya existen) ─ VIN_PROT
|
||||
VIN_PROT: C149, C150 2.2 µF/100 V 1210 · C151 100 µF/50 V 8×10 · D32 SMCJ33A
|
||||
U25 TPS54360B (HSOP-8 EP): VIN=VIN_PROT · EN flotante (R124/R125 DNP → UVLO interno)
|
||||
RT/CLK R120 162 k → f ≈ 600 kHz · BOOT C152 100 nF a SW
|
||||
SW ─ D34 B560C (SMC) a GND ─ L22 10 µH ─ VEH5V
|
||||
FB: R121 57.6 k (VEH5V) / R122 10.2 k (GND) → 0.8 V × (1 + 57.6/10.2) = 5.32 V
|
||||
COMP: R123 13 k + C155 6.8 nF a GND, C156 39 pF a GND
|
||||
VEH5V: C153, C154 47 µF/16 V 1210 (+ C78 220 µF polímero, C30, C31 que ya están)
|
||||
```
|
||||
|
||||
| Ref (v6) | Parte | Huella | LCSC | Nota V5.7 |
|
||||
|---|---|---|---|---|
|
||||
| U25 | TPS54360BDDAR | SO-8 EP (igual que U7/U20 de v6) | C524806 | 3.5 A continuos, 60 V entrada. Ver §3 corriente. |
|
||||
| L22 | 10 µH SRP1265A | 12.5 × 13.5 mm | C840531 | Isat 11 A. Si falta espacio: 10 µH ≥ 4.5 A Isat en 10 × 10 mm; **no** la 8 × 8 MWSA0804S (rizo 0.7 A p-p + 3.2 A pico la deja sin margen). |
|
||||
| D34 | B560C | SMC | C85100 | Diodo de recirculación: ~1 W a 2.8 A → SMC con cobre, no SMA. |
|
||||
| D32 | SMCJ33A | SMC | C699039 | TVS tras el FET; D10 SMBJ33A queda antes. |
|
||||
| F2 + fusible | portafusible NANO2 + 0154002.DR 2 A (v6) | Littelfuse 154 | C206912 / C206910 | Para V5.7 con cargador + ventilador: fusible **3 A** (0154003.DR). |
|
||||
| C149, C150 | 2.2 µF/100 V | 1210 | C153036 | |
|
||||
| C151 | 100 µF/50 V | electrolítico 8 × 10 | C116241 | |
|
||||
| C152 | 100 nF | 0603 | C14663 | |
|
||||
| C153, C154 | 47 µF/16 V | 1210 | C5440143 | |
|
||||
| C155 | 6.8 nF | 0402 | C1542 | |
|
||||
| C156 | 39 pF | 0402 | C1563 | |
|
||||
| R120 | 162 k 1 % | 0603 | C165760 | |
|
||||
| R121 / R122 | 57.6 k / 10.2 k 1 % | 0603 | C23082 / C22772 | 5.32 V |
|
||||
| R123 | 13 k | 0603 | C22797 | |
|
||||
| R124 / R125 | 523 k / 84.5 k | 0603 | — | **DNP** (UVLO interno 4.3 V). Poblar solo si se quiere apagar bajo ~15 V. |
|
||||
|
||||
## 2. Qué sale de la V5.6
|
||||
|
||||
| Sale | Por qué |
|
||||
|---|---|
|
||||
| **P2** (JST-XH 2, entrada 5 V) | Ya no hay 5 V externo. Opción: dejar la huella como entrada de banco (v6 la conserva). Recomendación: **quitarla**, el banco usa P1 con 24 V. |
|
||||
| **U2 MP1584 + L2 + C10/C11/C22/C23 + Cbst1 + Dbk1 + R20/R21 + Rcomp1 + Rfreq1 + Ren1/Ren2** (buck del ventilador, 17 piezas) | El ventilador pasa a VEH5V por JP1 (ya existe, 0 Ω) — decisión tomada en la v6. Libera la franja x 179–195 / y 50–72 mm, que es donde entra el buck nuevo. |
|
||||
| **D2** SS34 (recirculación del ventilador) y Q2 | Se quedan (el ventilador sigue conmutado por PA6). |
|
||||
| SD-15B-05, su cable y el pin 5 V del arnés | Fuera del HBOX y de la BOM mecánica. |
|
||||
|
||||
Netas: `/24V` desaparece (U2 era su único consumidor); `/24V_IN` → F2 → `/VIN_FUSED` → Q10 → `/VIN_PROT` → U25 → `/VEH5V`.
|
||||
|
||||
## 3. Corriente y térmico
|
||||
|
||||
- Todo lo que cuelga del LTC4040 (etapa, módem, cargador, lógica) pasa por **RS1 10 mΩ → límite 2.5 A**. El ventilador (≤ 0.3 A) cuelga de VEH5V antes de RS1. **Carga continua máxima del buck ≈ 2.8 A** < 3.5 A del TPS54360B. Las ráfagas LTE (2 A, ms) las absorben C78 220 µF + C153/C154.
|
||||
- Si en el banco el buck entra en límite de corriente con el cargador a 1.4 A + ventilador, el **TPS54560B** (5 A) es el mismo encapsulado y pinout: cambio de BOM, no de layout. Dejarlo anotado en el esquemático.
|
||||
- Disipación a 2.8 A / 24 V: U25 ≈ 0.6 W, D34 ≈ 1.1 W, L ≈ 0.2 W. EP de U25 con ≥ 6 vías a In1; D34 con ≥ 1 cm² de cobre en el cátodo. **Aplica la regla DFM del LTC4040**: pasta del EP al 50 %, vías rellenas y tapadas (ya es regla de pedido).
|
||||
- 24 V del gabinete es regulado (DDR-60G-24); la entrada real es 18–36 V. D10 + D32 cubren transitorios; el FET Q10 la polaridad.
|
||||
|
||||
## 4. Espacio en la tarjeta (el problema real)
|
||||
|
||||
- Lo que libera: franja del buck del ventilador (~15 × 22 mm, borde derecho), P2 (6 × 8 mm, arriba a la izquierda), P12/R42/C24 (ya liberado en V5.6).
|
||||
- Lo que necesita el bloque: ~22 × 26 mm (U25 + L 12.5 mm + 2 × 1210 + elco 8 mm + SMC × 2 + portafusible 12 × 5 mm).
|
||||
- **No cabe en la franja sola.** Opciones, en orden:
|
||||
1. Inductor 10 × 10 y elco 6.3 × 7.7 (C151 → 100 µF/50 V 6.3 × 11 o 2 × 47 µF/50 V 1210): el bloque baja a ~18 × 22 mm y cabe en la franja + lo que hay junto a P1/Q10 (x 175–198, y 115–130) si se mueven D10/Q10 al nuevo flujo.
|
||||
2. Portafusible fuera de la tarjeta (el gabinete ya tiene portafusible en el riel DIN): quitar F2 y ganar 12 × 5 mm. Decisión de Leo.
|
||||
3. **Cara inferior.** B.Cu solo tiene MOD1 (zócalo UNO). Montar el bloque abajo da todo el espacio, pero: ensamble a dos caras en JLC (costo), altura bajo la tarjeta en el HBOX (verificar con el respaldo mecánico), y el inductor queda del lado opuesto al plano de masa. Última opción.
|
||||
- Mientras no se resuelva el espacio, **no mover nada más**: V5.7 es exactamente V5.6 + este bloque − U2 − P2.
|
||||
|
||||
## 5. Firmware
|
||||
|
||||
- Ninguna línea nueva al MCU. `5V_SENSE` (PA4) sigue leyendo VEH5V. El umbral del gobernador (4.90 V) queda 0.42 V por debajo de 5.32 V; con V5.5 era la razón de los cortes en H0027.
|
||||
- Opcional: `PG`/`PGOOD` no existe en el TPS54360B; no hay señal nueva.
|
||||
- `board_v57.h` = `board_v56.h`. El ventilador ya es PWM por PA6/Q2 desde 5 V; quitar la política «ventilador solo con 24 V» del firmware (en batería el ventilador queda apagado por software, como hoy).
|
||||
|
||||
## 6. Validación del primer lote V5.7
|
||||
|
||||
1. Entrada 18, 24 y 36 V sin carga y con 2.8 A: VEH5V 5.32 ± 0.1 V, rizo < 50 mV.
|
||||
2. Arranque con el paquete a 3.0 V y el cargador habilitado: sin oscilación entre buck y LTC4040 (observar VEH5V con el osciloscopio durante `CARGA ON`).
|
||||
3. Ráfaga LTE con cargador a 1.4 A: VEH5V no baja de 5.05 V (umbral de arranque del gobernador) → `cortes=0` en 2 h.
|
||||
4. Térmico: U25, D34 y L a 2.8 A durante 1 h en caja cerrada a 45 °C; ninguno > 100 °C.
|
||||
5. Polaridad inversa en P1 (−24 V, 10 s): nada se daña; D10/D32 no conducen en operación normal.
|
||||
6. Fusible: cortocircuito en VEH5V abre F2 antes de que falle Q10/U25.
|
||||
|
||||
## 7. Decisiones pendientes (Leo)
|
||||
|
||||
1. ¿Quitar P2 o dejarla DNP como entrada de banco?
|
||||
2. ¿Fusible en tarjeta (F2) o solo el del gabinete?
|
||||
3. ¿Se acepta montar el bloque en la cara inferior si no cabe arriba?
|
||||
4. ¿V5.7 se ordena junto con V5.6 (dos versiones en una tanda) o después del banco de V5.6? Recomendación: **después** — V5.6 prueba el módem en batería y el ciclo de 10 min; V5.7 solo cambia la entrada.
|
||||
5. Mecánica del HBOX: el hueco del SD-15B pasa al paquete 26650 → nueva pieza impresa.
|
||||
|
||||
## 7b. Proyecto V5.7 creado (12-oct 14:51) — decisión de Leo: proyecto separado, conviven V5.6 y V5.7
|
||||
|
||||
`kicad/hbox-watchdog-v57.{kicad_sch,kicad_pcb,kicad_pro,kicad_prl,kicad_dru}` = copia de `hbox-watchdog-v5.*` tal como estaba a las 14:46 (U26 colocado, sin P12). Renombrados los 259 `(project …)` del esquemático y las 240 referencias `Sheetfile` del PCB; cajetines a «EINARBoard HBOX V5.7», rev 5.7, 2026-10-12; serigrafía «HBOX v5.7 - 2026-10» / «EINARBoard 5.7». Mismas tablas de librerías (`${KIPRJMOD}`), misma carpeta. Verificado con KiCad: ERC 0 errores (los mismos 25 avisos); netlist V5.7 = V5.6 nodo a nodo (253 componentes, 0 diferencias); DRC solo marca los pads de U26/C126–C128 sobre la colada GND sin rellenar (se va con B) y la vía suelta de 24V heredada.
|
||||
|
||||
**Regla de trabajo:** el esquemático V5.7 se edita ya (bloque U25, fuera U2 y P2). El **PCB V5.7 se vuelve a tomar del PCB V5.6 final de Michelle** (ruteo de U26, NTC_CELL, vía) cuando ella haga push, y entonces se corre Update PCB para el bloque nuevo. Así no se duplica su trabajo ni se divergen las dos tarjetas en lo que comparten.
|
||||
|
||||
## 8. Plan de trabajo
|
||||
|
||||
| Paso | Quién | Qué |
|
||||
|---|---|---|
|
||||
| 1 | Leo | Decisiones §7. |
|
||||
| 2 | Claude | Esquemático V5.7 por script sobre V5.6 (copia `hbox-watchdog-v57.*`? → **no**: misma regla que V5.6, se edita `hbox-watchdog-v5.*` en sitio con tag `v5.6-as-ordered` antes). Bloque U25 copiado de v6 con las mismas refs; quitar U2 y P2; ERC 0; diff de netlist = solo lo previsto. |
|
||||
| 3 | Michelle | Update PCB; retirar el buck del ventilador; colocar el bloque (§4); polígonos SW/VEH5V; DRC 0; paridad 0. |
|
||||
| 4 | Claude | Auditoría pad a pad + BOM/CPL; regla DFM U6/U25. |
|
||||
| 5 | Leo | Pedido (5 prototipos primero). |
|
||||
104
docs/historia/v6-auditoria-completa-brief.md
Normal file
104
docs/historia/v6-auditoria-completa-brief.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Agent brief — Full audit of EinarBoard v6 (with V5.5/V5.6 lessons learned)
|
||||
|
||||
!!! warning "Designators from before 22-Oct (T43)"
|
||||
This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the v6, U6 is now the A7670SA modem (was U19) and the LTC4040 is U17 (was U6). To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it.
|
||||
|
||||
**Owner:** Leo (Helgen Technologies) · **Date:** 2026-09-28 · **For:** a fresh Claude session/agent with the Drogheda folder mounted (`$HOME/mnt/Projects/Drogheda`) and the KiCad MCP tools available on Leo's Mac.
|
||||
**Language:** write the report in English with a Spanish executive summary (same convention as `2026-09-28_Drogheda_EinarBoard-v6_Review-vs-V5.5-Lessons_v1.0.md`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Audit the EinarBoard **v6** design (`03-Architecture-Hardware/kicad/hbox-watchdog-v6.kicad_sch` + `.kicad_pcb`, current state on disk) **completely**, not only against the nine V5.5 lessons: power tree, modem, GNSS, MCU pin map vs. firmware, Ethernet, thermal/EMC, connectors, BOM/JLC, identity/telemetry, DRC/ERC. Output a single ranked report with evidence and concrete fixes so Leo can decide what to change **before ordering v6**. Read-only by default; apply fixes only on a copy and only if Leo asks (§7).
|
||||
|
||||
## 2. Inputs (read all before starting)
|
||||
|
||||
Design files (do not modify in place):
|
||||
- `03-Architecture-Hardware/kicad/hbox-watchdog-v6.{kicad_sch,kicad_pcb,kicad_pro}` — the target. `hbox-watchdog-v6 respaldo.*` is an older backup, ignore unless comparing.
|
||||
- `03-Architecture-Hardware/kicad/hbox-watchdog-v5.*` — V5.5 **as fabbed** (frozen; never touch). `hbox-watchdog-v56.*` — V5.6 ECO copy (reference for what was fixed on V5).
|
||||
- Libraries: `Drogheda-v6.kicad_sym`, `Drogheda.pretty` in the kicad folder.
|
||||
|
||||
Lessons and prior reviews (the audit must cite these):
|
||||
- `03-Architecture-Hardware/2026-09-28_Drogheda_EinarBoard-v6_Review-vs-V5.5-Lessons_v1.0.md` — lesson-by-lesson review F1–F9 (✅ F1 F2 F3 · ⚠ F4 F6 F8 F9 · ❌ F5 F7). Start from its §4 risk checklist; **re-verify every item against the current files** (it was written on the 25-Sep state).
|
||||
- `03-Architecture-Hardware/2026-09-27_Drogheda_EinarBoard-V5.6_ECO_ES_v1.0.md` — the V5.6 ECO: sections A (modem power), B (MCU lines), C (charger RS1/R50), D (temperature), E (EMC list), F (misc), H (firmware 0.7.0 needs), I (validation).
|
||||
- `03-Architecture-Hardware/2026-09-28_Drogheda_EinarBoard-V5.6_Schematic-Change-Report_v1.0.md` — how V5.6 was implemented, open items §6.
|
||||
- `03-Architecture-Hardware/2026-08-24_Drogheda_EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`, `..._Phase-2_Schematic-Delta_v1.0.md`, `2026-09-02_Drogheda_v6-Copper-Plan_Placement-EMC-Guide.html`, `2026-08-27_Drogheda_v6-Antenna-Chain_Diagram.html`.
|
||||
- Project docs (claude.ai project "Drogheda"): `2026-09-25_HBox-v6_Pre-Fab-Audit_EN.md`, `2026-09-26_HBOX-V4-V5_Pinmap-Audit-FW-0.5.4.md`, `2026-09-27_HBOX-V5.5-V4_Modem-on-battery_Diagnosis-and-0.5.8-Field-Strategy.md`, `2026-09-28_Drogheda_Status-and-Next-Steps.md`.
|
||||
- Obsidian vault `Second Brain/03 Projects/Helgen/Project-Drogheda/`: "EinarBoard — Lecciones aprendidas V5.5 (sep 2026)", "EinarBoard v6 — Revisión contra lecciones V5.5", "Drogheda — Decision Log", "Drogheda — Risk Register".
|
||||
- Test evidence: `06-Testing-Validation/2026-09-28_H0008_charging-overnight-log.md` (charger works; average charge current ≈ 1.3 A vs 2.4 A programmed; 8.5 h to full; rail 5.25 V; MODEM watch threshold flapping).
|
||||
- Firmware: `04-Source-Code/firmware/hbox_v5_bringup/hbox_v5_bringup.ino` (0.6.0) and `CHANGELOG.md` — the pin map and every GPIO/ADC/UART/SPI/I²C the firmware expects. v6 will need a new pin map; the audit must produce it.
|
||||
- Memory files in this project (read: `drogheda-einarboard-v6-brief`, `drogheda-v6-zed-f9p-integration`, `drogheda-v6-lte-island`, `drogheda-v6-resync-protocol`, `drogheda-einarboard-v5-audit`, `drogheda-battery-hightemp`, `drogheda-backup-lowpower`, `drogheda-einarboard-harness`, `drogheda-einarboard-modem-rail`, `drogheda-einarboard-short-incident`, `drogheda-mff2-footprint`, `drogheda-power-cost`, `preferences`).
|
||||
|
||||
## 3. Ground rules (Leo's standing rules — non-negotiable)
|
||||
|
||||
1. **Never edit the v6 files in place.** Work read-only; if a fix phase is requested, copy to `hbox-watchdog-v6-audit.*`, keep backups of every intermediate state under `kicad/_to_delete/v6-audit-<date>/`, preserve UUIDs, and prove every change with a **netlist diff** (`kicad-cli sch export netlist`) before and after.
|
||||
2. **Never run bare numeric regex over KiCad files** (values like `0.3`, `825` appear everywhere). Parse S-expressions (python `sexpdata`/kiutils or the KiCad MCP tools) and match by ref/UUID/net.
|
||||
3. **No git from the mounted folder** — give Leo the commands. The mount cannot delete (use `_to_delete/`).
|
||||
4. Hardware bodges on existing boards are rejected — every fix is a design change for the next fab.
|
||||
5. Look things up yourself (netlist, datasheets, JLC parts DB via the KiCad MCP `search_jlcpcb_parts`/`get_jlcpcb_part`) — do not ask Leo for values you can derive from the files.
|
||||
6. Verify with tools, not by reading: `kicad-cli sch erc`, `kicad-cli pcb drc`, netlist export, pad→net dump, track width/length per net, courtyard overlaps (`check_courtyard_overlaps`), clearance (`check_clearance`).
|
||||
|
||||
## 4. Audit scope — do every block, in this order
|
||||
|
||||
**A. Power tree (24 V → 5 V → rails), incl. lessons F5/F7/F4**
|
||||
- U25 TPS54360B: output set-point (R121/R122) vs. the 0.6.0 charge governor (CHG_RAIL_ON 5.05 / CHG_RAIL_OFF 4.90 V) and LTC4040 PFI (4.44 V on V5). Required: ≥ 5.30 V nominal (R121 57.6k proposal) — confirm with the datasheet formula, check FB divider tolerance, output ripple, Cout, and that the 5 V rail can source F9P + modem peak + 2 A charge with margin. Input protection on 24 V (TVS, reverse, inrush) and the 24V/5V inner-layer clearance lesson from the 23-Jul short incident (`drogheda-einarboard-short-incident`): report every 24 V-to-other-net clearance < 0.5 mm on inner layers.
|
||||
- LTC4040 U6 block: RS1 (input limit 25 mV/RS vs. what U25 can supply), R50 (825 Ω = 2.42 A → 1.5 k = 1.33 A per ECO C; justify with the H0008 finding), PROG/NTC/RNTB/JP2 chemistry (Li-ion vs LFP-HT — `drogheda-battery-hightemp`), BSTFB divider (backup V), PFI divider, safety timer, CHRG/FAULT pull-ups to the MCU. **Thermal:** SW_BAT/BAT_PLUS track widths (target ≥ 2 mm / 1.5 mm), U6 EP thermal vias (≥ 9), L6 distance, copper area, a temperature sensor within 10 mm of U6 (TMP102 or NTC) so the firmware can see charger die heating.
|
||||
- Every buck/LDO on the board: input/output caps location (< 3 mm), BST cap, compensation network vs datasheet application table for the actual Vout/L/Cout, EN pull-downs (default OFF where the firmware expects OFF), UVLO vs the minimum battery voltage in backup (3.5 V boost floor).
|
||||
- Backup path: what runs on battery, what the boost can deliver (VSYS backup 4.79 V on V5) and whether F9P + modem + W5500 cold-start on battery is within the LTC4040 boost limit — this is the root cause of the V5.5 "modem dark on battery" failure (`2026-09-27_..._Modem-on-battery_Diagnosis`). Produce a worst-case current budget table (per rail, cold start and steady state).
|
||||
|
||||
**B. Modem (A7670SA) — lessons F1/F2/F3, ECO A/B**
|
||||
- U20 AP64350 3.80 V: source (D21/MAIN5V vs D20/24 V), EN = MODEM_PWR_EN (PC10) with 100k pull-down (R113), compensation/frequency network vs datasheet for 3.8 V, Cout, bulk ≥ 220 µF polymer within 10–20 mm of VBAT_MODEM pins, track width ≥ 0.25 mm (2 A bursts), ground return.
|
||||
- Control lines: PWRKEY (PC8), STATUS (PC9), RST (PC11), level shifting to 1.8 V logic where the module requires it, series resistors, pull-ups/downs at power-up (module must not be back-powered through UART/GPIO while its rail is off).
|
||||
- UART USART6 PC6/PC7 with the R106 100 Ω RTCM tap: confirm direction, no contention with the ArduSimple/F9P path, and that the firmware can read RTCM age (`$GxGGA` field 13) or raw RTCM.
|
||||
- SIM/eSIM (U22 MFF2 footprint — `drogheda-mff2-footprint`), SIM ESD, antenna path (matching, DC block, ESD), LTE island copper rules (`drogheda-v6-lte-island`).
|
||||
- Cold-start on battery test case: state explicitly whether v6 can bring the modem up from the LTC4040 boost alone; if not, which sequencing the firmware must enforce (this is requirement R1 of the ECO: "10 minutes off, come back to life, report, while on battery").
|
||||
|
||||
**C. GNSS (ZED-F9P) — `drogheda-v6-zed-f9p-integration`**
|
||||
- VCC ramp window (20–8000 µs/V), RTK_STAT, TIMEPULSE, SAFEBOOT/RESET_N handling, backup battery/V_BCKP, antenna chain (LNA bias, DC block, ESD, 50 Ω trace geometry on the stack-up), UART/I²C/SPI to MCU, u-center passthrough via J11 (P17 removed).
|
||||
- Confirm no `$GPTXT H00xx` source exists on v6 → identity must be `$PHID` from the MCU (lesson F9); check the MCU has a UID-based ID path and the monitor/firmware plan (`2026-09-25_HBox-v6_Pre-Fab-Audit_EN.md`).
|
||||
|
||||
**D. MCU pin map vs firmware 0.6.0 (STM32F411RET6)**
|
||||
- Build the full table: every net on U16 (or whatever the MCU ref is on v6) → function → firmware macro in `hbox_v5_bringup.ino` (PIN_*). Flag: pins used by firmware that are unconnected on v6; v6 nets with no firmware counterpart; ADC pins on non-ADC-capable pads; UART/SPI/I²C alternate-function conflicts; PWM (fan) timer availability; 5 V-tolerant requirement on any 5 V signal; BOOT0/BOOT1/DFU trap (`drogheda-einarboard-30-mcu-integration`); NRST routing (SW1, P10, C104); SWD header; the CHRG_STAT/FAULT/PFO inputs; IGN_SENSE divider (Rig 22k) and its junction (the V5.6 tool bug removed a junction there — check v6 too).
|
||||
- Deliverable: `pinmap-v6.md` table ready to become `#ifdef HBOX_BOARD_V6` in firmware.
|
||||
|
||||
**E. Ethernet W5500 — lesson F8**
|
||||
- SPI2 wiring, CS/INT/RST, RST pull-down (100k), 3V3_ETH source and whether it should be switchable (always-on on V5 wastes battery), magnetics/HR911105A, ESD on J9 (USBLC6-2SC6 or equivalent), no PoE (state it), 25 MHz crystal placement ≤ 5 mm.
|
||||
|
||||
**F. Thermal and EMC (ECO section E, copper plan) — lesson F5/F6**
|
||||
- Fan driver/FB divider location, fan tach, temperature sensors: count and placement (near modem, near U6, board ambient, P12 NTC for the battery pack), and whether the firmware's thermal inhibit (CHG_CELL_HOT 45 °C) has a real cell temperature source.
|
||||
- Crystals ≤ 5 mm; via stitching under the modem; DFU ESD (D14/D15/D16) at J11; return paths across splits; 24 V in inner layers; courtyard overlaps; DRC clean.
|
||||
|
||||
**G. Connectors and harness — `drogheda-einarboard-harness`, `drogheda-einarboard-j1-mirror`**
|
||||
- P1/P2 power entry, ignition GND return, JP_CUT across the P11 equivalent, pin-1 marking and mirrored-socket check on every header (the J1 mirror errata), silkscreen names (RESET MÓDEM / RESET MCU / F2), mounting holes and case fit.
|
||||
|
||||
**H. BOM / JLC manufacturability**
|
||||
- Every part with an `LCSC` property: exists, in stock, Basic vs Extended, footprint matches the JLC part (e.g. the L7 SRU8028 vs MWSA0804S question), polarity marks, DNP list explicit, no `TBD`. Export BOM + CPL with `kicad-cli` and check with `get_jlcpcb_part`. Estimate the unit BOM cost delta vs V5.6.
|
||||
|
||||
**I. Requirements traceability (from the ECO §0 R1–R5)**
|
||||
- R1 10-min off/on on battery with report; R2 modem never left dark (MCU can power-cycle it); R3 report while on battery; R4 temperature management (sensors + fan + charger inhibit); R5 charge at ≥ 1.3 A without thermal cycling. For each: satisfied by v6 as-is / needs change X / needs firmware only.
|
||||
|
||||
**J. ERC/DRC** — run both with `kicad-cli`; list every error and warning, classify pre-existing vs real.
|
||||
|
||||
## 5. Method and evidence standard
|
||||
|
||||
- Every finding: **ID, severity (Blocker / Major / Minor / Note), block, evidence (ref, net, coordinates, measured width/length/distance, datasheet page), consequence, concrete fix (part value + LCSC, or layout instruction with numbers), effort (schematic-only / layout / both)**.
|
||||
- "Pass" items need evidence too (one line each) — the report must show what was checked, not only what failed.
|
||||
- Compute, don't eyeball: track widths via pcb parsing, distances from footprint positions, divider outputs from resistor values, current budgets from datasheets.
|
||||
- If a fact is not derivable from the files (e.g. enclosure dimensions), list it under "Inputs needed from Leo" at the end — do not stop the audit for it.
|
||||
|
||||
## 6. Deliverables
|
||||
|
||||
1. `03-Architecture-Hardware/2026-MM-DD_Drogheda_EinarBoard-v6_Full-Audit_v1.0.md` — sections in the order of §4, then §4-I traceability table, §4-D pin map, a **ranked change list** (Blockers first) and a "ready to order? YES/NO + conditions" verdict.
|
||||
2. `03-Architecture-Hardware/pinmap-v6.md` (or inside the report) — the MCU pin table.
|
||||
3. Same report as a project doc `claude/…_EinarBoard-v6_Full-Audit.md` (claude.ai project "Drogheda") and a vault note `EinarBoard v6 — Auditoría completa (fecha).md` in `Second Brain/03 Projects/Helgen/Project-Drogheda/`, linked from `Project Drogheda MOC.md`; append decisions to `Drogheda — Decision Log.md` and new risks to `Drogheda — Risk Register.md`.
|
||||
4. Update `2026-09-28_Drogheda_Status-and-Next-Steps.md` (project) with a v6 line.
|
||||
5. Git commands for Leo (two-step: kicad submodule first, then the parent gitlink — `drogheda-kicad-submodule`).
|
||||
|
||||
## 7. Optional phase 2 (only if Leo says "apply")
|
||||
|
||||
Apply the Blocker/Major schematic fixes on a copy `hbox-watchdog-v6-audit.*` following the V5.6 method (`2026-09-28_..._Schematic-Change-Report`): copy → backups per step → KiCad MCP edits by ref/UUID → ERC → netlist diff = only the intended pins → PDF export → change report. Layout changes are listed for Michelle, not executed, unless explicitly requested.
|
||||
|
||||
## 8. Acceptance
|
||||
|
||||
The audit is done when: every §4 block has a pass/fail table with evidence; ERC/DRC outputs are attached; the R1–R5 traceability table is complete; the pin map covers 100 % of MCU pins; every proposed part has an LCSC number; the verdict states what blocks the order and what can follow in v6.1.
|
||||
340
docs/historia/v6-fase1-ntrip.md
Normal file
340
docs/historia/v6-fase1-ntrip.md
Normal file
@ -0,0 +1,340 @@
|
||||
---
|
||||
title: EinarBoard v6 — Phase 1 — NTRIP Client in the STM32 — Bench Spec & Execution Prompt
|
||||
tags: [project-drogheda, firmware, v6, ntrip, a7670sa, 4g, rtcm, bench, design-brief, prompt]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO + Claude
|
||||
date: 2026-08-24
|
||||
status: Active — THE spec for the Phase 1 bench spike. Execute before any v6 layout exists.
|
||||
authority: implements Phase 1 of `2026-08-24_…EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`.
|
||||
Firmware baseline = `04-Source-Code/firmware/blackpillrtkcode_v3` (v3.0.3). Where this
|
||||
conflicts with the v6 brief on Phase 1 mechanics, THIS file wins.
|
||||
---
|
||||
|
||||
# Phase 1 — NTRIP client in the STM32 (bench spike)
|
||||
|
||||
!!! warning "Designators from before 22-Oct (T43)"
|
||||
This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the v6, U6 is now the A7670SA modem (was U19) and the LTC4040 is U17 (was U6). To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it.
|
||||
|
||||
> **Why this exists.** The Ardusimple 4G NTRIP Master runs the NTRIP client *on the module*.
|
||||
> A bare A7670SA does not. v6 therefore requires a piece of firmware Helgen has never
|
||||
> shipped, sitting directly on the correction path — the exact path the June RCA already
|
||||
> proved you cannot see into. **This spec retires that risk on a $40 bench rig, before a
|
||||
> single v6 trace is drawn.**
|
||||
|
||||
---
|
||||
|
||||
## 0. ⚠ Correction to the v6 brief §10
|
||||
|
||||
The brief said to run this spike **on a v5.5 board through its XBee socket**. After extracting
|
||||
the actual v5 netlist, **that is wrong on two counts** and the plan is changed:
|
||||
|
||||
1. **The socket cannot power the modem.** `J7.1 = /3V3_MODEM`, generated by **U7 (MP1584)
|
||||
→ L7 (10 µH/2 A) → 3V3_MODEM**, C41 22 µF + C43 100 µF. The A7670SA needs
|
||||
**VBAT 3.4–4.2 V at up to 2.8 A**. Wrong voltage, wrong current, wrong rail. Feeding it
|
||||
from there would brown out `SW_5V` and reproduce the 10-Ago boot loop on the bench.
|
||||
2. **The UART nets collide.** `/USART1_RX` (PA10) carries **three** loads: `U16.43`,
|
||||
`K1.1` (relay NC) and `MOD1.23` (the F9P's TX2). There is no free RX pin on that net for
|
||||
the modem, and with K1 de-energised the NC contact ties `/USART1_RX` to `/DIN_4G`
|
||||
(modem RX) — driving the modem's TX into that net creates a **self-loop**.
|
||||
|
||||
**Phase 1 therefore runs on a standalone bench rig** (§1) that mirrors the v6 topology exactly,
|
||||
using pins that are free in v6. Nothing is soldered to the 15 boards in fab. An optional
|
||||
confirmation run on a real v5.5 board is described in §9 — **after** G1 passes.
|
||||
|
||||
---
|
||||
|
||||
## 1. Bench rig
|
||||
|
||||
### 1.1 Bill of materials (~US$45, all obtainable in MX in days)
|
||||
|
||||
| Item | Note |
|
||||
|---|---|
|
||||
| **STM32F411CEU6 "Blackpill"** | already in stock. Same family as the F411RET6 — USART6 lives on PB6/PB7 in the 48-pin package (see §1.3) |
|
||||
| **A7670SA core board** with USB, nano-SIM holder, UART header, u.FL/SMA | ~US$12–18 (MercadoLibre / AliExpress). **⚠ Confirm the variant is `-SA`.** An `-E`/`-C` board has no B4/B66 and will never attach on Telcel |
|
||||
| *(fallback)* LILYGO **T-A7670SA** | ~US$33; has its own LiPo + 5 V regulation. Requires flashing the on-board ESP32 with a transparent UART bridge, or tapping the modem pins directly |
|
||||
| **simpleRTK2B Pro** + multiband antenna | already owned |
|
||||
| **LTE antenna** + pigtail | ~US$5 |
|
||||
| **M2M SIM**, Telcel or AT&T MX, real data plan | ⚠ **not** a flat-rate sensor SIM — see v6 brief §6.4 |
|
||||
| Bench PSU capable of **4.0 V / 3 A**, or a 1S LiPo | powers the modem *only* |
|
||||
| **SN74LVC2T45DCUR** ×2 (or a TXS0108E breakout) | only if the core board exposes raw **1.8 V** I/O |
|
||||
|
||||
### 1.2 Power — the rule that keeps this honest
|
||||
|
||||
> **The modem gets its own supply. Never off the Blackpill's 3V3 or a USB 5 V pin.**
|
||||
> 4.0 V, ≥3 A, with **≥470 µF of low-ESR bulk within 20 mm of the module's VBAT pins**, and
|
||||
> **all three VBAT pins tied**. Grounds common, single star point at the modem.
|
||||
>
|
||||
> Instrument this: put a scope on VBAT during the first LTE attach and **record the droop**.
|
||||
> **>300 mV means the v6 rail is undersized** and you have learned it for $0. This measurement
|
||||
> is a Phase 1 deliverable, not an optional extra — it is the empirical check on the
|
||||
> `AP64350`-class buck chosen in v6 Block P.
|
||||
|
||||
### 1.3 Wiring — mirrors v6 exactly
|
||||
|
||||
| Function | v6 net | v6 MCU pin | Bench (Blackpill F411CE) |
|
||||
|---|---|---|---|
|
||||
| MCU → modem UART (AT + GGA + `$PHLG`) | `/USART6_TX` | **PC6** | **PB6** (USART1_TX, AF7) |
|
||||
| modem → MCU UART (responses + RTCM tap) | `/USART6_RX` | **PC7** | **PB7** (USART1_RX, AF7) |
|
||||
| modem UART TX → GNSS UART2 RX (RTCM, hardware path) | `/DOUT_4G` | — | direct wire to the Pro's **RX2** |
|
||||
| GNSS UART1 ↔ MCU | `/GNSS_TX`, `/GNSS_RX` | PA2 / PA3 | PA2 / PA3 (unchanged, `HardwareSerial GNSS(PA3, PA2)`) |
|
||||
| GNSS UART2 TX (uplink NMEA tap) | `/USART1_RX` | PA10 | **not wired on the bench** — see the note below |
|
||||
| modem PWRKEY | `/MODEM_PWRKEY` | **PC8** | **PB8**, via 2N7002 open-drain |
|
||||
| modem STATUS | `/MODEM_STATUS` | **PC9** | **PB9**, level-shifted |
|
||||
| modem RESET | `/MODEM_RST` | **PC11** | **PB5**, via 2N7002 open-drain |
|
||||
| modem rail enable | `/MODEM_PWR_EN` | **PC10** | **PB4** → bench PSU inhibit / load-switch |
|
||||
|
||||
> ⚠ **The 48-pin Blackpill has only two usable UARTs, and this matters.** On the F411**CE**
|
||||
> (UFQFPN48) there is no PC6/PC7, so USART6 is only available on PA11/PA12 — **the USB pins**.
|
||||
> That leaves USART2 (PA2/PA3, the receiver) and USART1 (PA9/PA10 **or** PB6/PB7, the modem).
|
||||
> Remapping USART1 to PB6/PB7 for the modem means **PA10 cannot simultaneously be
|
||||
> USART1_RX**, so the UART2 uplink tap is not available on the bench rig.
|
||||
>
|
||||
> **This costs nothing.** The MCU already receives the full NMEA stream — GGA included — from
|
||||
> the receiver's UART1 on USART2. Phase 1 sources the caster GGA from there. The PA10 tap
|
||||
> exists on v6 only to relay the receiver's UART2 output byte-for-byte for Modo B, which is a
|
||||
> v6 concern, not a Phase 1 one. **The F411RET6 on v6 has PC6/PC7 and keeps all three.**
|
||||
|
||||
> **Level shifting.** A7670SA I/O is **1.8 V**. `MODEM_TX` needs 3.3 → 1.8; `MODEM_RX` and
|
||||
> `MODEM_STATUS` need 1.8 → 3.3 (the STM32's V_IH ≈ 2.31 V — **1.8 V will not read as HIGH**).
|
||||
> `PWRKEY` and `RESET` have internal 50 kΩ pull-ups to VBAT: drive them **open-drain** with a
|
||||
> 2N7002, never push-pull, so an absent MCU leaves them unasserted.
|
||||
> Many A7670SA core boards already carry a shifter — **verify with a meter before connecting
|
||||
> anything**, and never drive 3.3 V into a raw 1.8 V pin.
|
||||
|
||||
### 1.4 Topology note — the fail-safe survives
|
||||
|
||||
On v6 the modem's UART TX fans out to **both** the GNSS UART2 RX (hardware RTCM path) **and**
|
||||
the MCU. K1 keeps its role: **NC = F9P TX2 → modem RX** (MCU dead ⇒ the receiver's own GGA
|
||||
still reaches the caster ⇒ corrections keep flowing), **NO = PC6** (MCU in line, forwarding GGA
|
||||
and injecting `$PHLG`). The only schematic change is that **K1 pin 10 moves from PA9 to PC6**
|
||||
(v6 Block T). Replicate this on the bench with a jumper: for Phase 1, run permanently in the
|
||||
"MCU in line" position.
|
||||
|
||||
**Consequence, stated plainly: v6 makes Modo B the normal operating mode, not a bench special.**
|
||||
That is consistent with the EPCT decision already on record (`$PHLG` in-band via Modo B).
|
||||
|
||||
---
|
||||
|
||||
## 2. Firmware architecture
|
||||
|
||||
### 2.1 Placement and guards
|
||||
|
||||
New block in `blackpillrtkcode_v3.ino` (Phase 1) → promoted to `blackpillrtkcode_v4/` for v6.
|
||||
Follow the file's existing idiom exactly:
|
||||
|
||||
```c
|
||||
#define ENABLE_NTRIP_CLIENT 0 // 0 = fleet (modem owns NTRIP) · 1 = MCU owns NTRIP [v6]
|
||||
#define ENABLE_NTRIP_DEBUG 1 // NTRIP/AT trace lines on the USB STREAM log
|
||||
```
|
||||
|
||||
`#ifndef`-guarded so `-DENABLE_NTRIP_CLIENT=1` works as an `arduino-cli` build flag without
|
||||
editing the file — same pattern as `ENABLE_MODOB_BENCH`. FW version string becomes
|
||||
**`"3.1.0-ntrip"`** so a stray flash is detectable in `$PHLG` field 1.
|
||||
|
||||
### 2.2 Hard rules
|
||||
|
||||
1. **No `delay()`. No blocking reads.** Everything is a millis()-timed state machine serviced
|
||||
from `loop()`, like `chargeGovernor()` and `modoBService()`. The IWDG is on
|
||||
(`ENABLE_IWDG 1`) — a blocking AT wait will reset the board.
|
||||
2. **No `String`, no heap.** Fixed buffers, `snprintf`, exactly as `buildPHLG()` does.
|
||||
3. **The NTRIP client never gates the safety loop.** Power, charge governor, thermal and fan
|
||||
services keep running at their existing cadence regardless of modem state.
|
||||
4. **Secrets never enter git** (invariant #6). APN and caster credentials come from
|
||||
`config/ntrip.local.h` (git-ignored) or `-D` build flags. Ship
|
||||
`config/ntrip.example.h` alongside the existing `4G_NTRIP_HelgenSRV-NMEA.example.txt`.
|
||||
|
||||
### 2.3 State machine
|
||||
|
||||
```
|
||||
┌──────────────────────────── beginBackoff(reason) ────────────────────┐
|
||||
│ │
|
||||
MODEM_OFF ─► RAIL_UP ─► PWRKEY_PULSE ─► WAIT_READY ─► CONFIG ─► ATTACH ─► │
|
||||
▲ │ │
|
||||
│ ▼ │
|
||||
│ SOCKET_OPEN ─► NTRIP_GET ──────┤
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
└──────────────── HARD_CYCLE ◄── BACKOFF ◄──────────────── STREAMING ──┘
|
||||
```
|
||||
|
||||
| State | Action | Timeout → next |
|
||||
|---|---|---|
|
||||
| `MODEM_OFF` | `MODEM_PWR_EN` low; all modem GPIO released | — |
|
||||
| `RAIL_UP` | `MODEM_PWR_EN` high | 100 ms → `PWRKEY_PULSE` |
|
||||
| `PWRKEY_PULSE` | PWRKEY low **≥ 50 ms** *(SIMCom; **never** copy Quectel's ≥2 s)*, release | → `WAIT_READY` |
|
||||
| `WAIT_READY` | wait for `RDY`/`+CPIN: READY`; UART ready ≈ 8 s, STATUS asserts ≈ 11.2 s | **20 s** → `BACKOFF` |
|
||||
| `CONFIG` | `ATE0` · `AT+CMEE=2` · `AT+CPIN?` · `AT+CGDCONT=1,"IP","<APN>"` · `AT+CSQ` · `AT+CREG?` | 10 s → `BACKOFF` |
|
||||
| `ATTACH` | `AT+CGATT?` poll until `1`; then `AT+NETOPEN` | **90 s** → `BACKOFF` |
|
||||
| `SOCKET_OPEN` | `AT+CIPOPEN=0,"TCP","<caster>",<port>` | 30 s → `BACKOFF` |
|
||||
| `NTRIP_GET` | send the §3 request; await `ICY 200 OK` / `HTTP/1.x 200` | 15 s → `BACKOFF` |
|
||||
| `STREAMING` | transparent RTCM in; GGA + `$PHLG` out; watchdogs of §5 | see §5 |
|
||||
| `BACKOFF` | rail stays up, socket closed, exponential wait | → `PWRKEY_PULSE` |
|
||||
| `HARD_CYCLE` | `MODEM_PWR_EN` low **≥ 2 s**, then `MODEM_OFF` | invariant #3's recovery path |
|
||||
|
||||
**Backoff ladder (mirror `chargeGovernor()`'s cooldown idiom):**
|
||||
`5 s → 10 → 20 → 40 → 80 → 160 → 300 s cap`. **Reset to 5 s after 10 minutes of healthy
|
||||
`STREAMING`.** After **3 consecutive** `BACKOFF` entries without ever reaching `STREAMING`,
|
||||
escalate to `HARD_CYCLE`.
|
||||
|
||||
> **Invariant #2 — never vanish, never loop.** The backoff ladder plus the 3-strike escalation
|
||||
> is what stops this becoming a fast-reboot loop. It is not decoration; it is the invariant.
|
||||
|
||||
---
|
||||
|
||||
## 3. NTRIP v1 handshake — exact bytes
|
||||
|
||||
```
|
||||
GET /<MOUNTPOINT> HTTP/1.0\r\n
|
||||
User-Agent: NTRIP HelgenHBOX/3.1\r\n
|
||||
Authorization: Basic <base64(user:pass)>\r\n
|
||||
Accept: */*\r\n
|
||||
Connection: close\r\n
|
||||
\r\n
|
||||
```
|
||||
|
||||
- Accept **either** `ICY 200 OK` (NTRIP v1) or `HTTP/1.0 200 OK` / `HTTP/1.1 200 OK`.
|
||||
- `401` → **do not retry blind.** Latch `NTRIP_AUTH_FAIL`, emit an `EVT`, and back off to the
|
||||
300 s cap. Hammering a caster on bad credentials gets you IP-banned — and rtk2go does ban.
|
||||
- `SOURCETABLE 200 OK` means **the mountpoint does not exist**. Distinct latch, distinct EVT.
|
||||
This is a real failure mode and the June RCA would have caught the caster problem months
|
||||
earlier if the firmware had reported it.
|
||||
- Base64 is computed **at build time** from the credentials header — no runtime encoder, no
|
||||
credentials in RAM as plaintext longer than necessary.
|
||||
|
||||
**GGA uplink.** Send one `$GPGGA`/`$GNGGA` to the caster **every 10 s** while `STREAMING`,
|
||||
**byte-exact, never rebuilt** — copy the sentence verbatim from the receiver's own output
|
||||
(bench: the USART2 NMEA stream; v6: the PA10 UART2 tap). VRS mountpoints require it;
|
||||
single-base mountpoints ignore it harmlessly. A rebuilt GGA with a recomputed checksum is the
|
||||
classic way to get silently dropped by a caster — `nmeaChecksumOk()` already exists in the
|
||||
firmware for validating it, but the bytes sent must be the originals.
|
||||
|
||||
---
|
||||
|
||||
## 4. RTCM handling and the telemetry that closes the June RCA
|
||||
|
||||
RTCM3 bytes flow **modem → GNSS UART2 in hardware**. The MCU sees the same bytes on its RX and
|
||||
**parses them for telemetry without being in the data path's critical timing**.
|
||||
|
||||
Frame walker (no buffering of payloads — count only):
|
||||
|
||||
- Preamble `0xD3`, then 6 reserved bits + **10-bit big-endian length**, then payload, then
|
||||
**3-byte CRC-24Q**.
|
||||
- Message type = first 12 bits of the payload.
|
||||
- Maintain: `rtcmBytes` (u32), `rtcmMsgs` (u32), `lastRtcmMs`, `rtcmCrcBad` (u16), and a
|
||||
small bitmap of **seen message types** (1005/1006 station, 1074–1077 GPS MSM,
|
||||
1084–1087 GLO, 1094–1097 GAL, 1124–1127 BDS).
|
||||
|
||||
> **This is the single most valuable thing Phase 1 produces.** On 10-Ago the fleet showed
|
||||
> *zero corrections applied anywhere* and there was no way to tell whether the caster, the
|
||||
> modem, the wiring or the receiver was at fault. After this, `$PHLG` answers it directly:
|
||||
> RTCM arriving but no fix ⇒ receiver/config or base-band mismatch. No RTCM ⇒ caster or link.
|
||||
> **Build the observability before you build the board.**
|
||||
|
||||
**`STREAMING` health watchdogs:**
|
||||
|
||||
| Condition | Action |
|
||||
|---|---|
|
||||
| no RTCM byte for **30 s** | → `BACKOFF` ("rtcm stall") |
|
||||
| no TCP data at all for **60 s** | → `BACKOFF` |
|
||||
| `+CIPCLOSE`/`+IPCLOSE` URC | → `BACKOFF` immediately |
|
||||
| `CSQ` = 99 for **60 s** | → `BACKOFF` |
|
||||
| 3 backoffs, never streamed | → `HARD_CYCLE` |
|
||||
|
||||
---
|
||||
|
||||
## 5. `$PHLG` extension — append-only
|
||||
|
||||
`buildPHLG()` is documented append-only and the backend decoder is already deployed. v3.0.3
|
||||
emits 25 fields. **Append six, in this order**, and change nothing before them:
|
||||
|
||||
| # | Field | Type | Meaning |
|
||||
|---|---|---|---|
|
||||
| 26 | `ntrip` | u8 | modem state enum (0=OFF … 8=STREAMING, 9=AUTH_FAIL, 10=NO_MOUNT) |
|
||||
| 27 | `rtcm_age` | u32 | ms since last RTCM byte (`0xFFFFFFFF` = never) |
|
||||
| 28 | `rtcm_msgs` | u32 | RTCM3 frames accepted this power session |
|
||||
| 29 | `rtcm_bad` | u16 | CRC-24Q failures |
|
||||
| 30 | `csq` | u8 | last `AT+CSQ` RSSI index (99 = unknown) |
|
||||
| 31 | `nret` | u16 | backoff entries this power session |
|
||||
|
||||
Also extend the `COLS,` header in `logBoot()` and the `TLM,` record in `logTelemetry()` with the
|
||||
same six, same order. **Backend:** `retranslator`'s `$PHLG` decoder must tolerate both 25- and
|
||||
31-field sentences — mixed-firmware fleets are guaranteed during rollout.
|
||||
|
||||
Update `docs/LOGGING-SPEC.md` to **v2.2** in the same commit.
|
||||
|
||||
---
|
||||
|
||||
## 6. hostsim
|
||||
|
||||
`tools/hostsim` already carries `sim`, `sim_mb`, `rollover` and a `run.sh` driver. Add:
|
||||
|
||||
- **scenario `ntrip`** — a fake modem behind a pipe that can be scripted to: attach normally ·
|
||||
never attach · return `401` · return `SOURCETABLE` · accept then go silent mid-stream ·
|
||||
drop TCP · emit malformed RTCM (bad CRC).
|
||||
- Assertions: the ladder is exponential and capped; it **resets after 10 min healthy**;
|
||||
`HARD_CYCLE` fires on the 3rd fruitless backoff and **not** before; `$PHLG` stays ≤ 160 bytes
|
||||
with all 31 fields; the XOR checksum is right; **no path ever calls `delay()`**.
|
||||
- `run.sh` builds and runs both `ENABLE_NTRIP_CLIENT` variants, as it already does for `modob`.
|
||||
|
||||
> Green hostsim is a **precondition** for touching the bench rig, not a post-hoc check.
|
||||
|
||||
---
|
||||
|
||||
## 7. G1 — acceptance gate
|
||||
|
||||
**G1 passes only when all of these hold on the bench rig, in one unbroken run:**
|
||||
|
||||
1. **24 h continuous**, `fixQuality ≥ 4` (RTK-fixed or float) for **≥ 95 %** of samples.
|
||||
2. **≥ 1 forced reconnect survived** — pull the LTE antenna for 60 s, restore. RTK must return
|
||||
**without human intervention** and `nret` must increment.
|
||||
3. **≥ 1 `HARD_CYCLE` exercised** — hold the modem in a fruitless state 3 backoffs running;
|
||||
confirm the rail drops ≥2 s and the unit recovers.
|
||||
4. **`rtcm_msgs` monotonically increasing** and the message-type bitmap shows the expected
|
||||
MSM sets for the base in use.
|
||||
5. **VBAT droop < 300 mV** at LTE attach, scope-captured (§1.2), with the capture archived to
|
||||
`06-Testing-Validation/`.
|
||||
6. **No IWDG reset** across the whole run (`g_resetCause` stays as booted).
|
||||
7. Auth/mountpoint failures produce the **correct distinct latches**, verified by deliberately
|
||||
using bad credentials and a bad mountpoint.
|
||||
|
||||
**Deliverables:** the 24 h `TLM` log, the scope capture, the hostsim run output, and a one-page
|
||||
result note in `06-Testing-Validation/`.
|
||||
|
||||
---
|
||||
|
||||
## 8. ⚠ Phase 1 depends on Phase −1
|
||||
|
||||
**Gate G0 first.** The 10-Ago fleet truth is *zero corrections applied anywhere* → check
|
||||
`HelgenTestStation` @ rtk2go:2101. If the caster or base is broken, this bench rig will
|
||||
faithfully report `STREAMING` with `rtcm_msgs = 0` and you will spend a week debugging
|
||||
firmware that is working correctly. **Prove the correction source on existing hardware
|
||||
before starting.**
|
||||
|
||||
---
|
||||
|
||||
## 9. Optional confirmation on a real v5.5 board (after G1)
|
||||
|
||||
Only once G1 has passed, and only if you want the extra confidence:
|
||||
|
||||
1. **Remove the simpleRTK2B Pro shield** from MOD1 — otherwise the F9P's TX2 contends with the
|
||||
modem on `/USART1_RX`.
|
||||
2. **Energise K1** (`PIN_BYP_CTRL` HIGH) so COM–NO is closed and `/DIN_4G` is isolated from
|
||||
`/USART1_RX`.
|
||||
3. Solder to the **THT relay pins** (accessible, no fine-pitch work): **K1 pin 10 = PA9**,
|
||||
**K1 pin 1 = PA10**, **K1 pins 5/6 = `/DIN_4G`**. `/DOUT_4G` is available at **J7 pin 2**.
|
||||
4. The modem still runs from its **own** 4.0 V supply. `3V3_MODEM` stays unloaded.
|
||||
|
||||
This confirms the connector and relay mechanics only. It does **not** validate the v6 pin map
|
||||
(PC6/PC7 have no copper on v5.5) — that is what Phase 6 first-article bring-up is for.
|
||||
|
||||
---
|
||||
|
||||
## 10. Cross-references
|
||||
|
||||
- v6 brief: `2026-08-24_Drogheda_EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`
|
||||
- Schematic delta: `2026-08-24_Drogheda_EinarBoard-v6_Phase-2_Schematic-Delta_v1.0.md`
|
||||
- Firmware baseline: `04-Source-Code/firmware/blackpillrtkcode_v3/` (v3.0.3), `docs/LOGGING-SPEC.md`
|
||||
- Modo B / `$PHLG` decision: `2026-08-08_Drogheda_Firmware-v3.1_ModoB-PHLG_Design-Brief_v1.0.md`
|
||||
- Ardusimple config being replaced: `04-Source-Code/firmware/config/4G_NTRIP_HelgenSRV-NMEA.example.txt`
|
||||
- Invariants: **#1** power-off safe · **#2** never vanish, never loop (§2.3 ladder) ·
|
||||
**#3** corrections ride the 4G, recover by power-cycling (`HARD_CYCLE`) · **#6** no secrets (§2.2)
|
||||
480
docs/historia/v6-fase2-esquematico.md
Normal file
480
docs/historia/v6-fase2-esquematico.md
Normal file
@ -0,0 +1,480 @@
|
||||
---
|
||||
title: EinarBoard v6 — Phase 2 — Schematic Delta (v5 → v6) — Build Spec
|
||||
tags: [project-drogheda, hardware, einarboard, v6, schematic, kicad, gnss, 4g, rf, delta]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO + Claude
|
||||
date: 2026-08-24
|
||||
status: Active — THE schematic spec for v6. Every net/pin below is verified against the
|
||||
as-drawn `hbox-watchdog-v5.kicad_pcb` netlist (extracted 2026-08-24), not assumed.
|
||||
authority: implements Phase 2 of `2026-08-24_…EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`.
|
||||
Supplements `watchdog-electrical-design-state.md` (canonical) and
|
||||
`watchdog-production-schematic-delta.md` (the v3 precedent for this document's format).
|
||||
---
|
||||
|
||||
# EinarBoard v6 — Schematic Delta
|
||||
|
||||
!!! warning "Designators from before 22-Oct (T43)"
|
||||
This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the v6, U6 is now the A7670SA modem (was U19) and the LTC4040 is U17 (was U6). To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it.
|
||||
|
||||
> **Base:** `kicad/hbox-watchdog-v5` (EinarBoard 3.0 / v5.5, 15 pcs in fab since 22-Ago,
|
||||
> git checkpoint `c3e8e48`). **Target:** `kicad/hbox-watchdog-v6`.
|
||||
>
|
||||
> **Method rules, unchanged and non-negotiable:** file-level KiCad edits only (the MCP backend
|
||||
> clobbered disk state on 24-Jul — read-only MCP queries are fine); git checkpoint after every
|
||||
> block; every structural edit verified by an independent parse; `%.10g` coordinates; footprint
|
||||
> rotation = R(−θ); zone refills in the GUI only.
|
||||
>
|
||||
> ⚠ **Before starting:** clear the stale `.git/*.lock` files and confirm the v5.5 fix commit
|
||||
> actually landed. This has bitten two sessions running.
|
||||
|
||||
---
|
||||
|
||||
## 0. ⚠ Two corrections to the v6 brief, from the extracted netlist
|
||||
|
||||
The brief was written before the netlist was pulled. Both of these were wrong:
|
||||
|
||||
1. **"Spare PCF8574 pins" — there is only ONE.** `U13` (PCF8574T, SOIC-16 **WIDE**) has
|
||||
P0–P3 on `/BATLED1–4`, P4–P6 on `/RGB_R/G/B`. **Only `P7` (pin 12) and `~INT` (pin 13)
|
||||
are free.** All new control signals therefore go on **MCU pins**, of which there are
|
||||
plenty (§3).
|
||||
2. **`/MODEM_RST` is not MCU-controlled today.** It connects only `J7.5` and `SW4G1.1`
|
||||
(a pushbutton). The firmware has **never** been able to reset the modem in software — its
|
||||
only tool is killing `SW_5V` via `PWR_CTRL`. **v6 fixes this** (Block T).
|
||||
|
||||
A third finding worth recording: **`/3V3_MODEM` is made by `U7` (MP1584) → `L7` → `3V3_MODEM`**,
|
||||
input `/SW_5V`. `U7`'s job changes completely in v6 (§Block P).
|
||||
|
||||
---
|
||||
|
||||
## 1. Verified v5 baseline (extracted 2026-08-24)
|
||||
|
||||
### 1.1 STM32F411RET6 (`U16`) — as drawn
|
||||
|
||||
| Pin | Port | Net | Pin | Port | Net |
|
||||
|---|---|---|---|---|---|
|
||||
| 2 | PC13 | `/LED_C13` | 41 | PA8 | `/CHRG_EN` |
|
||||
| 8 | PC0 | `/ETH_INT` | 42 | PA9 | `/USART1_TX` |
|
||||
| 14 | PA0 | `/BAT_SENSE` | 43 | PA10 | `/USART1_RX` |
|
||||
| 15 | PA1 | `/PWR_CTRL` | 44/45 | PA11/12 | `/DFU_DM` `/DFU_DP` |
|
||||
| 16 | PA2 | `/GNSS_TX` | 46/49 | PA13/14 | `/SWDIO` `/SWCLK` |
|
||||
| 17 | PA3 | `/GNSS_RX` | 50 | PA15 | `/ETH_RST` |
|
||||
| 20 | PA4 | `/5V_SENSE` | 55 | PB3 | `/CHRG_STAT` |
|
||||
| 21 | PA5 | `/IGN_SENSE` | 56 | PB4 | `/CHRG_FAULT` |
|
||||
| 22 | PA6 | `/FAN_PWM` | 57 | PB5 | `/BYP_CTRL` |
|
||||
| 26 | PB0 | `/EXT_LED` | 58/59 | PB6/PB7 | `/I2C_SCL` `/I2C_SDA` |
|
||||
| 27 | PB1 | `/NTC_TEMP` | 61 | PB8 | `/SER_IRQ` |
|
||||
| 28 | PB2 | `/BOOT1_STRAP` | 33–36 | PB12–15 | `/ETH_CS/SCK/MISO/MOSI` |
|
||||
|
||||
**Free (unconnected in v5): PC1, PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC10, PC11, PC12,
|
||||
PD2, PA7, PB9, PB10** — 16 pins. **USART6 = PC6/PC7 is free**, which is what makes v6 possible
|
||||
without disturbing anything.
|
||||
|
||||
### 1.2 The three nets that carry the whole GNSS/4G architecture
|
||||
|
||||
| Net | v5 members | Meaning |
|
||||
|---|---|---|
|
||||
| `/USART1_RX` | `U16.43` (PA10) · `Rdfu1.1` (22k) · **`K1.1` (NC)** · **`MOD1.23`** (F9P TX2) | receiver's UART2 uplink; MCU taps it passively |
|
||||
| `/DIN_4G` | **`K1.5/6` (COM)** · `J7.3` | modem's data-**in**; fed from the relay |
|
||||
| `/DOUT_4G` | `J7.2` · **`MOD1.17`** (F9P RX2) | **RTCM downlink, hardwired, never switched** |
|
||||
|
||||
`K1` (G5V-1 DC5): `1`=NC=`/USART1_RX` · `5/6`=COM=`/DIN_4G` · `10`=NO=`/USART1_TX` (PA9) ·
|
||||
`2`=`/SYS5V` · `9`=`/KCOIL` ← `BYP_CTRL` (PB5) via `R40`→`Q6`.
|
||||
|
||||
### 1.3 Power tree (verified)
|
||||
|
||||
```
|
||||
P1.1 ─/24V_IN─┬─ D10 (SMBJ33A)
|
||||
└─ Q10 (IRFR5305, rev-pol) ──/24V── U2.7 (MP1584 fan buck) ──/SW_FAN─► 5V_FAN
|
||||
P2.1 ─/VEH5V── RS1 (8 mΩ) ──/VIN_SW── U6.7 (LTC4040 VIN)
|
||||
U6 (LTC4040) ──/MAIN5V── Q7 (AO4406A cutoff, gate /IGATE) ──/SYS5V── F1 (PPTC 3A)
|
||||
├─ U12 (MP1584) ─► /3V3_ETH
|
||||
└─ K1.2 (relay coil supply)
|
||||
/SW_IN ── Q1 (AO3401A, gate /GATE_LS ← PWR_CTRL) ──/SW_5V──┬─ MOD1.5 (shield 5 V)
|
||||
└─ U7.2/7 (MP1584) ─L7─► /3V3_MODEM
|
||||
U17 (AMS1117-3.3) ──/3V3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Deletions
|
||||
|
||||
| Ref | What | Action |
|
||||
|---|---|---|
|
||||
| `MOD1` | simpleRTK2B Pro, Arduino UNO R3 socket, 32 pads @ (90.6, 129.9) | **DELETE** — nets `/GNSS_RX` `/GNSS_TX` `/DOUT_4G` `/USART1_RX` `/SW_5V` re-terminate on `U18` |
|
||||
| `J7`, `J8` | XBee 2.0 mm sockets, 10 pads each @ (174.2 / 196.5, 74.8) | **DELETE** — nets re-terminate on `U19` |
|
||||
| `SW4G1` | modem RESET pushbutton | **DELETE** — replaced by MCU control (Block T) |
|
||||
| `Rmled1`, `MODEM_LED_A` | modem LED + resistor | **KEEP**, re-source from the A7670SA `NETLIGHT` pin |
|
||||
| `MH7`, `MH9`, `MH10`, `MH11` | shield / heatsink M2.5 bolt holes | **DELETE MH7/MH9/MH10**; **KEEP MH11** and add one partner near `U19` for the modem heatsink clamp |
|
||||
| `U7`, `L7`, `C41`, `C43` | the 3V3 modem buck | **REPURPOSE** — see Block P |
|
||||
|
||||
**Do not delete `K1`.** It is rewired (Block T), not removed — it is the fail-safe that keeps
|
||||
corrections flowing with a dead MCU.
|
||||
|
||||
---
|
||||
|
||||
## 3. MCU pin allocation — v6 additions
|
||||
|
||||
| New net | Pin | Dir | Function |
|
||||
|---|---|---|---|
|
||||
| `/USART6_TX` | **PC6** | OUT | MCU → modem (AT, GGA uplink, `$PHLG`) — **via `K1` NO** |
|
||||
| `/USART6_RX` | **PC7** | IN | modem → MCU (AT responses, RTCM tap) |
|
||||
| `/MODEM_PWRKEY` | **PC8** | OUT | open-drain via `Q20` (2N7002) |
|
||||
| `/MODEM_STATUS` | **PC9** | IN | level-shifted 1.8 → 3.3 V |
|
||||
| `/MODEM_PWR_EN` | **PC10** | OUT | enables `U20` buck — the `HARD_CYCLE` path |
|
||||
| `/MODEM_RST` | **PC11** | OUT | open-drain via `Q21` (2N7002) — **new MCU control** |
|
||||
| `/GNSS_RESET_N` | **PC12** | OUT | open-drain; `U18` needs a 7–13 kΩ pull-up |
|
||||
| `/GNSS_ANT_DETECT` | **PC1** | IN | antenna-open telemetry |
|
||||
| `/GNSS_ANT_SHORT_N` | **PC2** | IN | antenna-short telemetry |
|
||||
| `/GNSS_TIMEPULSE` | **PA7** | IN | TIM3_CH2 input capture |
|
||||
|
||||
**Unchanged:** `/GNSS_TX` PA2 · `/GNSS_RX` PA3 (USART2, `HardwareSerial GNSS(PA3, PA2)`) ·
|
||||
`/USART1_RX` PA10 (receiver UART2 tap) · `/BYP_CTRL` PB5 (K1 coil).
|
||||
**Freed:** **PA9** — `K1.10` moves to PC6. Leave PA9 as a test point.
|
||||
**Still spare after v6:** PC3, PC4, PC5, PB9, PB10, PD2, PA9 — **7 pins.**
|
||||
|
||||
> ⚠ **Verify no double-allocation** with an independent netlist parse before ERC. That check
|
||||
> is the reason this table exists.
|
||||
|
||||
---
|
||||
|
||||
## Block N — GNSS island (`U18`)
|
||||
|
||||
**Part:** `ZED-F9P-04B`, 54-pin LGA 22.0 × 17.0 × 2.4 mm — **LCSC `C5120257`, Extended.**
|
||||
*(Alternative under evaluation for v7: LG290P, 12.2 × 16 mm — see brief §1.4. **Do not**
|
||||
attempt a dual footprint; the pad rings overlap and the pin functions do not align.)*
|
||||
|
||||
| Signal | Connect to | Note |
|
||||
|---|---|---|
|
||||
| `VCC` | `/3V3_GNSS` | **from `U21` LDO only** — never the shared `/3V3` |
|
||||
| `V_BCKP` | `/3V3` via 1 kΩ + Schottky | 45 µA hot-start; keeps ephemeris across a power-cycle |
|
||||
| `GND` | GND | full pad-under-module via field |
|
||||
| `UART1 TX/RX` | `/GNSS_RX` / `/GNSS_TX` | ⚠ net names are **MCU-relative** in this design: `/GNSS_TX` = MCU PA2 out → receiver RX |
|
||||
| `UART2 RX` | `/DOUT_4G` | **RTCM in — hardware path, unswitched** |
|
||||
| `UART2 TX` | `/USART1_RX` | uplink NMEA → `K1.1` (NC) and PA10 |
|
||||
| `RESET_N` | `/GNSS_RESET_N` | **7–13 kΩ pull-up to `/3V3_GNSS` required** |
|
||||
| `TIMEPULSE` | `/GNSS_TIMEPULSE` | 4 mA drive |
|
||||
| `ANT_DETECT` / `ANT_SHORT_N` | `/GNSS_ANT_DETECT` / `/GNSS_ANT_SHORT_N` | free antenna-fault telemetry — you have already lost H0005 and H0014 to antenna faults |
|
||||
| `VCC_RF` | Block S bias tee | 50 mA typ / **300 mA abs max** |
|
||||
| `D_SEL` | leave open (UART mode) | open/high = UART; low = SPI |
|
||||
| `SAFEBOOT_N` | leave open | |
|
||||
| `USB` (`V_USB`, D+/D−) | **test pads only**, not a connector | u-center access at first article; 90 Ω pair, ESD if ever fitted |
|
||||
|
||||
**Decoupling:** 100 nF per supply pin **≤ 2 mm**, 10 µF + 1 µF bulk ≤ 10 mm, per the
|
||||
Capacitor-Placement-Map rules.
|
||||
|
||||
### `U21` — GNSS LDO
|
||||
|
||||
**3.3 V, ≥ 300 mA, low noise.** Candidates: `TPS7A2033PDBVR` (low-noise, preferred) or
|
||||
`AP2112K-3.3TRG1` (cheap, LCSC `C51118`).
|
||||
|
||||
**Input = `/SW_5V`, output = `/3V3_GNSS`** — deliberately *not* the shared `/3V3`. Two reasons:
|
||||
the receiver stays on the MCU-gated stack rail, so `PWR_CTRL` (PA1 → `Q1`) still power-cycles
|
||||
it exactly as it does today; and an LDO fed from 5 V has ~1.7 V of headroom, which is what
|
||||
gives it the PSRR to reject the three MP1584 switchers this board carries. The ZED-F9P has
|
||||
**no internal SAW** — it will lock onto buck harmonics given the chance.
|
||||
|
||||
> 🔴 **`ZED-F9P` VCC ramp rate ≤ 20 µs/V is a DAMAGE limit, not a guideline** — 0 → 3.3 V in
|
||||
> **≤ 66 µs**. Most LDOs with soft-start are far slower. **Measure the chosen LDO's rise time
|
||||
> on the bench before fab.** If it cannot meet it, add a fast load switch or a supervisor.
|
||||
> This is a fab gate, not a nice-to-have.
|
||||
|
||||
---
|
||||
|
||||
## Block O — LTE island (`U19`)
|
||||
|
||||
**Part:** `A7670SA-LASC`, 88-pin LGA 24 × 24 × 2.5 mm — **LCSC `C18548276`**, in the JLCPCB
|
||||
library, Extended, **X-ray inspection required, MSL 3**.
|
||||
⚠ **`-SA` only.** `-E`/`-C` variants have no B4/B66 and will never attach on Telcel.
|
||||
⚠ A7670 (88-LGA) ≠ A7672 (124 LCC+LGA) ≠ SIM7070 (68-LCC). Not interchangeable.
|
||||
|
||||
| Signal | Connect to | Note |
|
||||
|---|---|---|
|
||||
| `VBAT` **pins 55, 56, 57** | `/VBAT_MODEM` | **all three, mandatory** for RF performance. Trace/pour **≥ 3 mm** |
|
||||
| `GND` | GND | full via field |
|
||||
| `UART_TXD` | `/DOUT_4G` (level-shifted, Block R) | → `U18` UART2 RX **and** → `/USART6_RX` |
|
||||
| `UART_RXD` | `/DIN_4G` (level-shifted) | ← `K1` COM |
|
||||
| `PWRKEY` | `/MODEM_PWRKEY_M` | via `Q20` open-drain. **Low ≥ 50 ms = on; ≥ 2.5 s = off.** Internal 50 kΩ to VBAT. **≤ 100 nF total capacitance** or you get spurious auto-power-on |
|
||||
| `RESET` | `/MODEM_RST_M` | via `Q21` open-drain, low 2–2.5 s. Same ≤ 100 nF rule |
|
||||
| `STATUS` | `/MODEM_STATUS_M` | 1.8 V out; asserts ≈ 11.2 s after power-on |
|
||||
| `NETLIGHT` | `Rmled1` → `/MODEM_LED_A` | reuse the existing LED |
|
||||
| `VDD_1V8` | `/1V8_MODEM` + 2.2 µF | **50 mA max — level-shift reference ONLY, never a system rail** |
|
||||
| `USIM1_*` | Block Q | internal 4.7 kΩ pull-up on `USIM1_DATA` — no external needed |
|
||||
| `USB` D+/D−, VBUS | `J9` (kept) | 90 Ω pair. **Reserve a `USB_BOOT` test pad** — the only recovery if a firmware update bricks it. `USB_BOOT` must **not** be pulled low at power-up |
|
||||
| `RF_ANT` (pin 60) | Block S | **single antenna** — Cat-1bis, no diversity port |
|
||||
|
||||
**Decoupling at the VBAT pins:** 33 pF ∥ 10 pF ∥ 100 nF ∥ 1 µF ceramics, plus the Block P bulk.
|
||||
|
||||
---
|
||||
|
||||
## Block P — modem 3.8 V rail (`U20`) — replaces `U7`'s modem duty
|
||||
|
||||
> **Why this block exists, in one sentence:** the 10-Ago fleet boot-loop was caused by
|
||||
> exceeding the SD-15B's 3 A, and putting a 2 A LTE modem back on that same rail would
|
||||
> recreate it.
|
||||
|
||||
```
|
||||
/24V ──────────► D20 (SS34) ──┐
|
||||
├──/VIN_MODEM──► U20 (buck, 3.8 V) ──L20──► /VBAT_MODEM ──► U19
|
||||
/MAIN5V ──► D21 (PMEG3050) ───┘ ▲
|
||||
│ EN
|
||||
/MODEM_PWR_EN (PC10)
|
||||
```
|
||||
|
||||
| Item | Spec |
|
||||
|---|---|
|
||||
| `U20` | **4.5–40 V in, ≥ 3 A, synchronous buck.** Candidate: `AP64350` class. ⚠ **40 V rating required** — `D10` (SMBJ33A) clamps at ~53 V, a 28 V part such as TPS54331 is **not** acceptable. **NOT a fourth MP1584** — the existing three are the source of EMC findings 3 and 4 |
|
||||
| Output | **3.8 V** (mid-window of the A7670SA's 3.4–4.2 V) |
|
||||
| `D20` | SS34 — already in the BOM (`D2`, `D12`). Drop irrelevant on the 24 V leg |
|
||||
| `D21` | **low-Vf Schottky** (PMEG3050 class, Vf ≈ 0.35 V @ 2 A). In blackout `MAIN5V` ≈ 5 V → `VIN_MODEM` ≈ 4.65 V → duty ≈ 0.82. **Verify `U20`'s max duty covers it**; if marginal, use a P-FET ideal diode (`AO3401A`, already in the BOM as `Q1`) |
|
||||
| Bulk | **≥ 470 µF low-ESR** within 20 mm of the VBAT pins (spec floor is 300 µF; the extra is cheap insurance against the 2.8 A GSM burst) |
|
||||
| `Cin` | **1 µF + 10 µF within 3 mm** of `U20`'s VIN-GND — EMC finding 4, mandatory |
|
||||
| Feedback | divider **at the FB pin**, `/FB_MODEM` short and routed away from `/SW_MODEM` — EMC finding 3, mandatory |
|
||||
| Enable | `/MODEM_PWR_EN` (PC10). **Power-cycle = EN low ≥ 2 s.** Add a 1 kΩ bleed across the bulk so it actually collapses |
|
||||
|
||||
**Behaviour:** cabinet live → modem's ~9.5 W comes from **24 V at ~0.45 A** and the SD-15B
|
||||
never sees it. Blackout → same buck accepts `MAIN5V` (pack via LTC4040), unit keeps reporting
|
||||
(**invariant #2**).
|
||||
|
||||
### `U7` / `L7` / `C41` / `C43` — disposition
|
||||
|
||||
`/3V3_MODEM` no longer feeds a modem. **Repurpose `U7` as the 3.3 V supply for the level
|
||||
shifters and the LTE-island logic**, renaming the net `/3V3_LTE`, **or** delete `U7`/`L7`/
|
||||
`C41`/`C43` entirely and take that logic from `/3V3`. **Decide before routing the 3V3 tree** —
|
||||
this is the same open decision that was left dangling on `U12`/`L8` in the 3.0 brief, and it
|
||||
should not be left dangling twice. Recommendation: **delete**, feed from `/3V3`; the shifters
|
||||
draw milliamps.
|
||||
|
||||
### 🔴 Also fix in this block
|
||||
|
||||
**The LTC4040 input-limit errata.** `RS1` (8 mΩ) sets a 3.12 A input limit **above** the
|
||||
SD-15B-05's 3 A rating. Bring the limit **below** the PSU rating, or specify a larger PSU.
|
||||
This has been a standing errata since 10-Ago. **v6 is where it gets fixed.**
|
||||
|
||||
---
|
||||
|
||||
## Block Q — MFF2 SIM (`U22`)
|
||||
|
||||
| Item | Spec |
|
||||
|---|---|
|
||||
| Package | **MFF2 / DFN-8 5 × 6 mm (SON-8)** |
|
||||
| Signals | `USIM1_VDD` · `USIM1_CLK` · `USIM1_DATA` · `USIM1_RST` · GND |
|
||||
| Pull-ups | none — `U19` has an internal 4.7 kΩ on `USIM1_DATA` |
|
||||
| Voltage | 1.8 V / 3.0 V auto-detect, no strapping |
|
||||
| ESD | array at the SIM lines (`ESDA6V1W5` / `SMF15C` class) |
|
||||
| Placement | **≤ 20 mm from `U19`**, short matched-ish traces, GND guard |
|
||||
|
||||
⚠ **Sourcing is a lead-time item, not an engineering task.** You need a real Telcel/AT&T MX or
|
||||
Mexican-profile MVNO MFF2. Flat-rate sensor IoT SIMs (500 MB / 10 years) are **an order of
|
||||
magnitude too small** for 50–200 MB/month. Start this in Phase 0.
|
||||
|
||||
---
|
||||
|
||||
## Block R — level translation (`U23`, `U24`)
|
||||
|
||||
**A7670SA I/O is 1.8 V. The STM32's V_IH ≈ 2.31 V — 1.8 V will not read as HIGH.**
|
||||
|
||||
| Line | Direction | Method |
|
||||
|---|---|---|
|
||||
| `/DIN_4G` (MCU/K1 → modem RX) | 3.3 → 1.8 | `SN74LVC2T45DCUR`, DIR fixed |
|
||||
| `/DOUT_4G` (modem TX → MCU + receiver) | 1.8 → 3.3 | `SN74LVC2T45DCUR`, DIR fixed |
|
||||
| `/MODEM_STATUS` | 1.8 → 3.3 | same device, 2nd channel |
|
||||
| `/MODEM_PWRKEY`, `/MODEM_RST` | 3.3 → open-drain | `Q20`, `Q21` = 2N7002. **Not** through a shifter — MCU absent must leave them unasserted |
|
||||
|
||||
- `VccA` = `/1V8_MODEM` (from `U19`'s `VDD_1V8`, 2.2 µF); `VccB` = `/3V3`.
|
||||
- **Prefer direction-fixed `74LVC2T45` over auto-direction `TXS0108E`** — auto-direction parts
|
||||
misbehave against strong drivers and multi-load nets, and `/DOUT_4G` drives **two** loads.
|
||||
- ⚠ **The RTCM path passes through `U24`.** Add DNP 0 Ω bypass footprints across it so a
|
||||
suspect shifter can be jumpered out at bring-up.
|
||||
- `/DOUT_4G` fan-out on the 3.3 V side: `U18` UART2 RX **and** `/USART6_RX` (PC7) through a
|
||||
**100 Ω series** resistor for isolation.
|
||||
|
||||
---
|
||||
|
||||
## Block S — RF front ends
|
||||
|
||||
### Stackup (unchanged topology, new requirement)
|
||||
|
||||
`F.Cu` signal + GND pour · `In1` **solid GND** · `In2` 3V3 plane · `B.Cu` signal + GND pour. ✔
|
||||
|
||||
**New: order as controlled impedance.** JLCPCB 4-layer `JLC04161H-7628`, F.Cu→In1 prepreg
|
||||
≈ 0.2104 mm. Target **50 Ω CPWG on F.Cu referenced to In1** — roughly 0.35 mm trace / 0.25 mm
|
||||
gap at εr ≈ 4.4. **Confirm with JLCPCB's own impedance calculator before layout, not after.**
|
||||
|
||||
**Rules for both RF traces, no exceptions:** referenced to an unbroken `In1` (no plane split,
|
||||
no `In2` island underneath), as short and straight as possible, ground-stitched both sides at
|
||||
≤ λ/20, nothing crossing on any layer.
|
||||
|
||||
### S1 — GNSS (`J12`)
|
||||
|
||||
```
|
||||
J12 (U.FL) ─╥─ C_DC (100 nF) ─── U18.RF_IN
|
||||
║
|
||||
└─ L_BIAS (≥68 nH 0603, SRF > 1.6 GHz) ─┬─ 100 nF ∥ 100 pF ─ GND
|
||||
└─ VCC_RF (U18)
|
||||
D_ESD (>2 GHz rated, <0.5 pF) at J12
|
||||
```
|
||||
|
||||
Active antenna is **mandatory** — the ZED-F9P has **no internal LNA**.
|
||||
|
||||
### S2 — LTE (`J13`)
|
||||
|
||||
```
|
||||
U19.RF_ANT ─── π-match pads (series 0402 + 2× shunt 0402, series = 0 Ω default)
|
||||
─── J13 (U.FL) ─── D_ESD (low-cap <0.5 pF)
|
||||
```
|
||||
|
||||
### S3 — 🔴 GNSS desense — the #1 technical risk
|
||||
|
||||
A 23 dBm transmitter drawing 2–3 A bursts next to a GNSS front end is a classic RTK-killer.
|
||||
**The Ardusimple shield has been hiding this. A bare module hands it to you.**
|
||||
|
||||
1. **Board separation:** `U18` in the freed `MOD1` area (bottom-left), `U19` in the freed
|
||||
XBee area (right edge) → **≈ 149 mm apart, the full diagonal.** The GNSS corner is also the
|
||||
furthest point from all four switchers (nearest, `U7` at 140.0/112.9, ~60 mm).
|
||||
2. **Antenna separation ≥ 20 cm** on the enclosure — two bulkheads. **Design rule, not an
|
||||
optimization.**
|
||||
3. Modem return path routed away from the GNSS island; no shared reference but `In1`.
|
||||
4. **No 1575 SAW** — it would kill L2 on a dual-band receiver. Specify an active antenna with
|
||||
good out-of-band rejection instead.
|
||||
5. **Measured at gate G3** (Phase 6): fix quality and C/N₀ with the modem idle vs. transmitting
|
||||
at full power. Pass/fail, not opinion.
|
||||
|
||||
### S4 — ESD everywhere (EMC finding 5)
|
||||
|
||||
**Every external connector gets a clamp within 10 mm.** `D14/D15/D16` move from the MCU to
|
||||
within 10 mm of `J11`. `J9` gets the ESD array it never had (**< 2 pF** on the data lines).
|
||||
`J12`, `J13`, `P1`, `P4`, `P15`, `P16` all get appropriate clamps.
|
||||
|
||||
---
|
||||
|
||||
## Block T — `K1` rewiring (one net change, big consequence)
|
||||
|
||||
| `K1` pin | v5 | **v6** |
|
||||
|---|---|---|
|
||||
| 1 (NC) | `/USART1_RX` | **unchanged** — receiver UART2 TX |
|
||||
| 5/6 (COM) | `/DIN_4G` | **unchanged** — modem RX |
|
||||
| 10 (NO) | `/USART1_TX` (PA9) | **`/USART6_TX` (PC6)** ← the only change |
|
||||
| 2 | `/SYS5V` | unchanged |
|
||||
| 9 | `/KCOIL` ← `BYP_CTRL` (PB5) | unchanged |
|
||||
|
||||
**What this buys.** Energised (normal operation) the MCU owns the modem's RX line: it can send
|
||||
AT commands, forward the receiver's GGA byte-exact, and inject `$PHLG`. **De-energised — MCU
|
||||
dead or absent — the relay relaxes and the receiver's own GGA goes straight to the modem, so
|
||||
corrections keep flowing.** Invariants #2 and #3 both hold, with hardware, for the price of one
|
||||
moved net.
|
||||
|
||||
**Consequence to state plainly: v6 makes Modo B the normal operating mode.** Consistent with
|
||||
the EPCT decision already on record.
|
||||
|
||||
---
|
||||
|
||||
## 4. New net list
|
||||
|
||||
| Net | Members |
|
||||
|---|---|
|
||||
| `/3V3_GNSS` | `U21.out` · `U18.VCC` · decoupling |
|
||||
| `/VBAT_MODEM` | `L20.2` · `U19.55/56/57` · bulk + ceramics |
|
||||
| `/VIN_MODEM` | `D20.k` · `D21.k` · `U20.VIN` · Cin |
|
||||
| `/1V8_MODEM` | `U19.VDD_1V8` · `U23.VccA` · `U24.VccA` · 2.2 µF |
|
||||
| `/USART6_TX` | `U16.37` (PC6) · `K1.10` |
|
||||
| `/USART6_RX` | `U16.38` (PC7) · 100 Ω → `/DOUT_4G` (3.3 V side) |
|
||||
| `/MODEM_PWRKEY` / `_M` | `U16.39` (PC8) → `Q20` → `U19.PWRKEY` |
|
||||
| `/MODEM_STATUS` / `_M` | `U19.STATUS` → `U24` → `U16.40` (PC9) |
|
||||
| `/MODEM_PWR_EN` | `U16.51` (PC10) · `U20.EN` |
|
||||
| `/MODEM_RST` / `_M` | `U16.52` (PC11) → `Q21` → `U19.RESET` |
|
||||
| `/GNSS_RESET_N` | `U16.53` (PC12) · `U18.RESET_N` · 10 kΩ pull-up |
|
||||
| `/GNSS_ANT_DETECT` | `U16.9` (PC1) · `U18.ANT_DETECT` |
|
||||
| `/GNSS_ANT_SHORT_N` | `U16.10` (PC2) · `U18.ANT_SHORT_N` |
|
||||
| `/GNSS_TIMEPULSE` | `U16.23` (PA7) · `U18.TIMEPULSE` |
|
||||
| `/SIM_VDD` `/SIM_CLK` `/SIM_DATA` `/SIM_RST` | `U19` ↔ `U22` |
|
||||
| `/RF_GNSS` `/RF_LTE` | 50 Ω, own netclass, **no autorouting** |
|
||||
|
||||
**Changed:** `/DOUT_4G` gains `U18`.UART2_RX, `U24` and the PC7 tap; loses `J7.2`, `MOD1.17`.
|
||||
`/DIN_4G` gains `U23`/`U19`; loses `J7.3`. `/USART1_RX` loses `MOD1.23`, gains `U18`.UART2_TX.
|
||||
`/USART1_TX` loses `K1.10` → PA9 becomes a test point.
|
||||
|
||||
**Netclasses:** keep `HV24`; add **`RF`** (50 Ω, no auto-route) and **`PWR_MODEM`**
|
||||
(`/VBAT_MODEM`, `/VIN_MODEM` — min width 1.5 mm, target ≥ 3 mm).
|
||||
|
||||
---
|
||||
|
||||
## 5. Inherited EMC fixes — schematic-level, day-one
|
||||
|
||||
| # | Fix | Where it lands in the schematic |
|
||||
|---|---|---|
|
||||
| 🔴 1 | `/SW_BAT` ≥ 2 mm polygon | netclass + layout; **load-bearing** — blackout now feeds the modem too |
|
||||
| 🔴 2 | `/BAT_PLUS` ≥ 1.5 mm + via farm | same |
|
||||
| 🟠 3 | `R20`/`R21` at `U2` pin 4, `/FB_FAN` short | schematic grouping so layout can honour it |
|
||||
| 🟠 4 | 1 µF + 10 µF ≤ 3 mm of every buck VIN — **including `U20`** | add the parts now |
|
||||
| 🟠 5 | ESD < 10 mm of every external connector | Block S4 |
|
||||
| 🟡 6 | Crystals → **3225** package, < 5 mm (`Y1`, `Y3`, `Y4` are HC-49 at 12–15 mm) | change footprints in the schematic pass |
|
||||
| 🟡 7 | ETH RX pair coupled and length-matched | layout, flagged here |
|
||||
| 🟡 8 | Fill the 8 empty 20 mm stitching cells | free — most were under `MOD1` |
|
||||
|
||||
---
|
||||
|
||||
## 6. Part additions
|
||||
|
||||
| Ref | Part | LCSC | Note |
|
||||
|---|---|---|---|
|
||||
| `U18` | ZED-F9P-04B | **C5120257** | Extended, X-ray, ~104–129 in stock — **consign from Mouser for ≥ 100 units** |
|
||||
| `U19` | A7670SA-LASC | **C18548276** | Extended, X-ray, MSL 3, 220 in stock |
|
||||
| `U20` | 40 V / 3 A sync buck (AP64350 class) | verify | **40 V minimum** |
|
||||
| `U21` | TPS7A2033 or AP2112K-3.3 | C51118 (AP2112K) | ⚠ verify the ≤ 66 µs ramp |
|
||||
| `U22` | MFF2 SIM | carrier | lead-time item |
|
||||
| `U23`, `U24` | SN74LVC2T45DCUR | verify | direction-fixed |
|
||||
| `Q20`, `Q21` | 2N7002 | verify | open-drain PWRKEY / RESET |
|
||||
| `D20` | SS34 | in BOM (`D2`) | 24 V leg |
|
||||
| `D21` | PMEG3050 class | verify | low-Vf, 5 V leg |
|
||||
| `J12`, `J13` | U.FL / IPEX MHF1 SMT receptacle | verify | + 2 pigtails, 2 SMA bulkheads |
|
||||
| `L20` | buck inductor per `U20`'s datasheet | verify | ≥ 4 A saturation |
|
||||
| — | 2× 220 µF low-ESR + ceramics | verify | Block P bulk |
|
||||
| — | ESD arrays, RF ESD, bias-tee L/C, π-match 0402s | verify | Blocks S1–S4 |
|
||||
|
||||
> **All "verify" rows must be confirmed against the live JLCPCB library at upload**, not from
|
||||
> a cached DB. The `v4-bom.xls` labels are on record as unreliable; check pin-counts vs
|
||||
> pad-counts every time.
|
||||
|
||||
---
|
||||
|
||||
## 7. Execution order & verification
|
||||
|
||||
| Step | Work | Check |
|
||||
|---|---|---|
|
||||
| T0 | Clear stale `.git/*.lock`; confirm the v5.5 fix commit landed; branch `v6` from `c3e8e48`; copy v5 → `hbox-watchdog-v6` | parse-verify the copy opens clean |
|
||||
| T1 | Deletions (§2) | netlist parse: `MOD1`/`J7`/`J8`/`SW4G1` gone, no orphan nets |
|
||||
| T2 | Block T (`K1.10` → PC6) | 1-net diff, nothing else moved |
|
||||
| T3 | Block N + `U21` | ERC on the GNSS island alone |
|
||||
| T4 | Block O + Block Q + Block R | ERC; **confirm every 1.8 V line crosses a shifter** |
|
||||
| T5 | Block P + LTC4040 input-limit fix | recompute the backup power budget vs the 1S4P ~12 Ah pack |
|
||||
| T6 | Block S (RF + ESD) + netclasses | `RF` netclass exists and is excluded from autorouting |
|
||||
| T7 | EMC list §5 | each row ticked individually |
|
||||
| T8 | Full ERC + independent pin-map parse | **ERC 0**; no double-allocated MCU pin |
|
||||
|
||||
**Gate G2:** ERC 0 **plus** an external review of the RF and power sections. This is Helgen's
|
||||
first RF design — do not let ERC-clean stand in for reviewed.
|
||||
|
||||
---
|
||||
|
||||
## 8. What is deliberately NOT in v6
|
||||
|
||||
- **A second GNSS footprint.** LG290P's pads overlap the F9P ring and the functions don't align.
|
||||
The cost-down is a v7 respin of one corner — which is exactly why Block N is an island.
|
||||
- **A modem mezzanine daughtercard.** Rejected with "full integration" (brief §0); revisit only
|
||||
if gate T1's cabinet temperature forces the industrial tier.
|
||||
- **eUICC.** MFF2 only, per the 24-Ago decision.
|
||||
- **Cat-4 / WCDMA.** See brief §2.3.
|
||||
|
||||
---
|
||||
|
||||
## 9. Cross-references
|
||||
|
||||
- v6 brief: `2026-08-24_Drogheda_EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md`
|
||||
- Phase 1 firmware: `2026-08-24_Drogheda_EinarBoard-v6_Phase-1_NTRIP-Firmware-Spec_v1.0.md`
|
||||
- EMC fix list: `2026-08-22_Drogheda_EinarBoard-v5_EMC-Audit_v1.0.md`
|
||||
- Canonical electrical state: `watchdog-electrical-design-state.md`
|
||||
- Format precedent: `watchdog-production-schematic-delta.md`
|
||||
- Cap placement rules: `2026-08-14_Drogheda_EinarBoard-3.0_Capacitor-Placement-Map_v1.0.md`
|
||||
- Invariants: **#1** power-off safe (`Q20`/`Q21` open-drain, buck EN default off) · **#2**
|
||||
never vanish (Block P dual input, Block T fail-safe) · **#3** power-cycle recovery
|
||||
(`MODEM_PWR_EN`) · **#5** no head-of-line blocking · **#6** no secrets
|
||||
660
docs/historia/v6-gnss-4g-brief.md
Normal file
660
docs/historia/v6-gnss-4g-brief.md
Normal file
@ -0,0 +1,660 @@
|
||||
---
|
||||
title: EinarBoard v6 — On-Board GNSS + 4G Integration — Design Brief & Execution Prompt
|
||||
tags: [project-drogheda, hardware, einarboard, v6, gnss, rtk, 4g, lte, rf, design-brief, prompt]
|
||||
project: Project Drogheda
|
||||
author: Helgen CTO + Claude (decisions opened 2026-08-24)
|
||||
date: 2026-08-24
|
||||
status: Draft v1.0 — planning. §0 decisions locked by Leo 24-Ago; §1 GNSS choice is the one open gate.
|
||||
authority: supplements [[watchdog-electrical-design-state]] (canonical) and inherits the fix list of
|
||||
`2026-08-22_Drogheda_EinarBoard-v5_EMC-Audit_v1.0.md` in full. Where this conflicts with the 3.0
|
||||
brief on v6 scope, THIS file wins.
|
||||
---
|
||||
|
||||
# EinarBoard v6 — Design Brief ("the prompt")
|
||||
|
||||
!!! warning "Designators from before 22-Oct (T43)"
|
||||
This document keeps the designators the board had on its date. Since 22-Oct (T43) the V5.6, V5.7 and v6 carry new ones, and several old names now belong to another part: on the v6, U6 is now the A7670SA modem (was U19) and the LTC4040 is U17 (was U6). To read it against today's board, use the [old → new table](https://drogheda.helgen.online/hardware/designadores-t43-22oct/#8-tablas-viejo-nuevo) (in Spanish); parts already gone by 22-Oct are not in it.
|
||||
|
||||
> **What this is.** The instruction set for turning `hbox-watchdog-v5` (EinarBoard 3.0 / v5.5,
|
||||
> 15 pcs in fab since 22-Ago) into **EinarBoard v6** — the first HBOX board with the **GNSS
|
||||
> receiver and the 4G modem soldered on**, retiring the simpleRTK2B Pro shield (MOD1) and the
|
||||
> XBee-socket 4G NTRIP Master (J7/J8).
|
||||
>
|
||||
> Standing rules unchanged: **file-level KiCad edits only**; git checkpoint after every phase;
|
||||
> every structural edit verified by an independent parse; bench gates before fab.
|
||||
|
||||
---
|
||||
|
||||
## 0. Decisions locked 2026-08-24 (Leo)
|
||||
|
||||
| Topic | Decision |
|
||||
|---|---|
|
||||
| Scope | **Full integration** — GNSS + 4G both on-board; MOD1 and J7/J8 deleted |
|
||||
| Board outline | **Keep 140.000 × 100.000 mm** (x ∈ [60.19, 200.19], y ∈ [40.32, 140.32]) — no enclosure rework |
|
||||
| SIM | **MFF2 soldered SIM** (DFN 5×6). No tray, no spring contacts, no opening a sealed IP67 box in Veracruz |
|
||||
| **GNSS receiver** | 🔒 **LOCKED — ZED-F9P-04B, LCSC `C5120257`** (Leo, 24-Ago). Rationale §1. LG290P stays as the documented v7 cost-down |
|
||||
| **4G module** | 🔒 **LOCKED — SIMCom A7670SA-LASC, LCSC `C18548276`** (Leo, 24-Ago). LTE Cat-1 + **2G GSM** fallback; 3G answered in §2.3 |
|
||||
| Inherited | The **entire v5.5 EMC fix list** is day-one scope, not a wish list (§7) |
|
||||
|
||||
**Still open — need Leo:** antenna interface (§4.4), modem VBAT source sign-off (§3.2),
|
||||
homologación path (§9.4), and gate **T1** (§2.4) — which does **not** block the schematic, only
|
||||
the decision to stay on the A7670SA if the cabinet runs hot.
|
||||
|
||||
---
|
||||
|
||||
## 1. GNSS receiver — "cheapest" vs "best value" are not the same part
|
||||
|
||||
### 1.1 The hard constraint that eliminates the truly cheap tier
|
||||
|
||||
Your base is the Ardusimple **AS-PRO-CORS = ZED-F9P = L1/L2**, and Mexican public CORS (INEGI
|
||||
RGNA) are overwhelmingly **GPS L1/L2 + GLONASS**. RTK requires the rover to track the same
|
||||
frequencies the base broadcasts.
|
||||
|
||||
That **disqualifies the entire cheap L1/L5 tier**: Quectel LC29H (all variants, $22–33),
|
||||
ZED-F9P-**15B**, NEO-F9P, LG69T. They are L1 + L5, **no L2**. Fed from an L1/L2 base they
|
||||
degrade to effectively single-frequency — you would be paying for RTK silicon and getting
|
||||
float. Do not let the $22 LC29H price tempt anyone: it is the right part for a product where
|
||||
you also own the base, and the wrong part for a fleet you diagnose remotely from Querétaro.
|
||||
|
||||
Also permanently out: ATGM332D / ATGM336H / AT6668 ($1.44–4.75, huge LCSC stock) — **single-band
|
||||
L1, no carrier-phase RTK at all.**
|
||||
|
||||
### 1.2 The three that survive
|
||||
|
||||
| | **ZED-F9P-04B** | **LG290P03AAMD** | **UM980** |
|
||||
|---|---|---|---|
|
||||
| Vendor | u-blox (CH) | Quectel (CN) | Unicore (CN) |
|
||||
| Bands | L1/L2 | **L1/L2/L5/E6** | all incl. E6/B2b |
|
||||
| RTK rate / convergence | 7–24 Hz / <12 s | 20 Hz / **5 s** | 50 Hz / <5 s |
|
||||
| Package | 54-LGA 22×17×2.4 | **12.2×16×2.6** | 54-LGA 22×17×2.6 |
|
||||
| Supply / current | 2.7–3.6 V, 85 mA trk (130 pk) | 3.15–3.45 V, **91 mA** | 3.0–3.6 V, **145–180 mA / 480 mW** |
|
||||
| Internal LNA | **No — active antenna mandatory** | ambiguous (see §1.5) | **No** |
|
||||
| USB | **Yes** | **No** | No |
|
||||
| qty 1 | **$84.82 LCSC** (C5120257) / $131.74 Mouser | $79.89 | ~$94.88 |
|
||||
| qty 100 | $116.56 Mouser | **$59.54** | ~$80.65 |
|
||||
| JLCPCB | C5120257, Extended, 104–129 stk | **not listed** — consign | C9900015354, Extended |
|
||||
|
||||
### 1.3 The money, honestly
|
||||
|
||||
| Volume | ZED-F9P (LCSC) | LG290P | Saving by switching |
|
||||
|---|---|---|---|
|
||||
| 30 units | $2,545 | ~$2,100 | **~$450** |
|
||||
| 100 units | $8,482 (if stock holds) | $5,954 | ~$2,500 |
|
||||
| 300 units | ~$34,968 (Mouser $116.56) | $17,862 | **~$17,100** |
|
||||
|
||||
Against that saving, switching away from u-blox costs you, **once**:
|
||||
|
||||
- **The entire config toolchain.** u-center, UBX-CFG-VALSET, your saved receiver configs, your
|
||||
RAM-vs-BBR/Flash lesson from the June RCA — all of it is u-blox-specific. LG290P configures
|
||||
with `$PQTM…` sentences.
|
||||
- **The backend decoder.** htracker already ingests `$PUBX,00` (retranslator +138, delivered
|
||||
10-Ago). LG290P does not emit PUBX. That's a protocol change on the server too.
|
||||
- **A fresh RTCM-compatibility unknown**, in a program whose *actual* June root cause (free
|
||||
rtk2go caster, RAM-only config, unverified UART2 path) is still not fully closed.
|
||||
|
||||
**Read: at 30 units the saving is $450 — less than a week of firmware and backend work. At
|
||||
300 units it is $17k — real money.**
|
||||
|
||||
### 1.4 🔒 DECIDED 24-Ago — ZED-F9P-04B
|
||||
|
||||
> **v6 = ZED-F9P-04B, sourced from LCSC (C5120257) at $84.82.**
|
||||
> It is $32/unit cheaper than Mouser with *zero* engineering change, it is the receiver your
|
||||
> base station is paired to, and it keeps u-center as your field-diagnosis tool while you are
|
||||
> simultaneously taking on an RF front end, a cellular radio, and an NTRIP client you've never
|
||||
> shipped. **One new unknown at a time.**
|
||||
>
|
||||
> **Documented cost-down for the 300-unit phase = LG290P.** Scope it as a bounded respin of the
|
||||
> GNSS island only (§5 puts the receiver in its own corner precisely so this stays cheap), plus
|
||||
> the firmware/backend work, and pull the trigger when volume pays for it.
|
||||
|
||||
⚠ **Sourcing caveat that applies either way:** LCSC shows ~104–129 pcs of ZED-F9P-04B. A
|
||||
100-unit run eats the entire bin. Mouser has 573 + 834 arriving, 18-week factory line. **For a
|
||||
30–100 unit run, buy the receivers yourself and consign them to JLCPCB** — you control the most
|
||||
expensive, longest-lead line item instead of being hostage to an LCSC bin.
|
||||
|
||||
*(If you want the LG290P now anyway, say so and I'll re-baseline §5/§6 — but then Phase 1 in
|
||||
§10 grows a receiver-bring-up spike and the backend decoder becomes v6 scope.)*
|
||||
|
||||
### 1.5 Open verification if LG290P is ever chosen
|
||||
|
||||
Its datasheet says "external active antenna"; Mouser's part description says "**w/ LNA
|
||||
(KT5030)**". These cannot both drive the RF section. **Resolve against the LG290P Hardware
|
||||
Design doc before laying out one millimetre of RF.**
|
||||
|
||||
---
|
||||
|
||||
## 2. 4G modem
|
||||
|
||||
### 2.1 Bands decide the variant before anything else
|
||||
|
||||
Telcel = **B4** (bulk) + B7 + B66. AT&T MX = B2/B4/B5/B7. Altán "Red Compartida" = **B28**
|
||||
(700 MHz — the best-propagating band in the country). Movistar rides AT&T's RAN.
|
||||
|
||||
→ You need **B2/B4/B5/B7/B28/B66**. That is exactly the **`-SA`** (SIMCom), **`-LA`** (Quectel
|
||||
EG915x) and **`-AU`** (Quectel EC200x) suffixes. **Any `-EU` / `-E` / `-CN` / `-C` part is
|
||||
useless here** — no B4, no B66. This is the single most common way this decision goes wrong.
|
||||
|
||||
### 2.2 🔒 DECIDED 24-Ago — SIMCom A7670SA-LASC
|
||||
|
||||
> **SIMCom A7670SA-LASC — LCSC C18548276, $7.58 @1, $7.39 @10, ~$6.3–6.9 @1k, 220 in stock,
|
||||
> already in the JLCPCB parts library** (Extended, X-ray inspection required, MSL3).
|
||||
|
||||
Why it wins on your criteria:
|
||||
|
||||
- **Cheapest thing that is actually assemblable at JLCPCB.** The only real alternative in the
|
||||
JLC library is Quectel EG915U-LA (C19185632) at ~$9–13.
|
||||
- **Correct LatAm bands**: LTE-FDD B1/2/3/4/5/7/8/**28**/**66**, plus GSM 900/1800.
|
||||
- **Single antenna.** Cat-1bis-class: one `RF_ANT` pin, no RX-diversity port. One less 50 Ω
|
||||
path, one less antenna, one less thing to get wrong. (This is also why **Cat-4 is the wrong
|
||||
choice** — EC200A/SIM7600 add a second antenna port, a 29×32 mm footprint and 2–4× the price
|
||||
for throughput you will never use on a 50–200 MB/month NTRIP + NMEA stream.)
|
||||
- **Public docs, no NDA**, and the A76xx AT set is SIM7600-compatible → **TinyGSM works**, which
|
||||
is what makes the §2.5 firmware estimate credible.
|
||||
- **Feature-variant upgrade path with no respin**: `-LASC` (bare) → `-LASE` (+BLE) → `-FASE`
|
||||
(+GNSS +BLE) are the same 88-LGA footprint.
|
||||
|
||||
**Not interchangeable, do not confuse:** A7670 = 88-LGA · A7672 = 124 hybrid LCC+LGA ·
|
||||
SIM7070 = 68-LCC. All 24×24 mm, all different footprints. Migration = respin.
|
||||
|
||||
### 2.3 On your 3G question — the honest answer
|
||||
|
||||
**3G is not the lever you want, and it is on its way out.** Mexico's 2G/3G shutdown is still
|
||||
under negotiation rather than executed (operators were still discussing it in July 2026 —
|
||||
millions of handsets block it), so 3G *exists* today. But designing a 2026 board with a 5–10
|
||||
year fleet life around a network the regulator and all three carriers are actively trying to
|
||||
switch off is buying a fallback with an expiry date.
|
||||
|
||||
What actually fixes poor coverage, in order of effect:
|
||||
|
||||
1. **Band coverage at low frequency.** B28 (700 MHz) and B5 (850 MHz) penetrate buildings and
|
||||
container stacks far better than B4/B66 (1700/2100 MHz). The A7670SA has all of them.
|
||||
2. **Cat-1bis link budget.** A narrower, lower-order category has a better link budget than
|
||||
Cat-4 at the cell edge. The cheap choice is also the more robust one here.
|
||||
3. **A proper external antenna on a bulkhead SMA**, not a chip antenna inside a metal-adjacent
|
||||
cabinet. §4.4.
|
||||
4. **2G/GSM fallback** — which the A7670SA already has (GSM 900/1800). At 20–40 kbps GPRS you
|
||||
will not hold RTCM at full rate, but you keep telemetry and remote command alive, which is
|
||||
what "never vanish" (invariant #2) actually requires.
|
||||
|
||||
**If you still want WCDMA/3G**, the cheap route is **SIM7600SA-H** (Cat-4 + WCDMA + GSM, LatAm
|
||||
bands) at ~$25–40 — but you pay ~4× the module cost, a second antenna port, a 29×32 mm
|
||||
footprint, and more heat, for a network that is being switched off. **My recommendation is no.**
|
||||
Take the A7670SA's 2G fallback and spend the difference on the antenna.
|
||||
|
||||
### 2.4 Temperature — the one place I'd push back on "cheapest"
|
||||
|
||||
Your own RCA names the 4G modem as a heat casualty in the nested cabinet under Veracruz sun.
|
||||
|
||||
| Part | Operating range |
|
||||
|---|---|
|
||||
| SIMCom A7670SA | **−30…+80 °C** (−40…+85 degraded) |
|
||||
| Quectel EG915U-LA | **−35…+75 °C** (−40…+85 extended) |
|
||||
| Telit LE910C1-LA (~$48) | **−40…+85 °C full** |
|
||||
| u-blox LARA-R6001 (~$43–55) | **−40…+85 °C full** |
|
||||
|
||||
> 🔴 **GATE T1 — cabinet temperature. YOU ALREADY HAVE THIS DATA. DO NOT INSTRUMENT ANYTHING.**
|
||||
>
|
||||
> Fielded V3 units already report **P12 = 10 k NTC → PB1 → `tntc` ("Cámara" = cabinet
|
||||
> interior)** and **P5 = SHT40 → `tair`**, inside `$PHLG` every 30 s, and the htracker
|
||||
> retranslator has been ingesting `$PHLG` in production since 10-Ago. **Query the existing
|
||||
> telemetry history for the max `tntc` per unit over the hottest days.**
|
||||
>
|
||||
> That query — free, today, no hardware, no truck roll — settles the single largest cost
|
||||
> decision in v6: an **$8** A7670SA versus a **$48** Telit LE910C1-LA. Across 300 units that
|
||||
> is a **$12,000** decision sitting in a database you already own.
|
||||
>
|
||||
> Decision rule: **max `tntc` < 70 °C → A7670SA.** 70–75 °C → A7670SA **plus** the M2.5
|
||||
> heatsink clamp (§5) and re-measure on the first article. **> 75 °C → escalate the tier**;
|
||||
> you already know what a modem failure costs at ICAVE.
|
||||
|
||||
### 2.5 What the modem costs you in firmware
|
||||
|
||||
**Neither the A7670SA nor any Quectel module has a built-in NTRIP client.** That was the
|
||||
Ardusimple 4G NTRIP Master's entire value, and you are giving it up. You implement NTRIP v1
|
||||
yourself: `GET /MOUNTPOINT HTTP/1.0` + `Authorization: Basic …`, then read raw RTCM3 off a
|
||||
transparent TCP socket and shovel it into the receiver.
|
||||
|
||||
Realistic budget: **1–2 weeks**, not 1 day — the AT/socket layer is a day; reconnect/backoff
|
||||
logic, watchdog integration, TLS-on/TLS-off caster handling, signal-quality telemetry into
|
||||
`$PHLG`, and field failure modes are the rest.
|
||||
|
||||
> ✅ **This risk can be retired for free, right now.** See §10 Phase 1: the **XBee socket on the
|
||||
> v5.5 boards currently in fab** will host an A7670SA breakout. You can prove the entire
|
||||
> NTRIP-in-STM32 stack on hardware you already paid for, while the boards are in transit,
|
||||
> before a single v6 trace exists.
|
||||
|
||||
---
|
||||
|
||||
## 3. Power architecture — the biggest new electrical work
|
||||
|
||||
### 3.1 What the modem demands
|
||||
|
||||
| Parameter | A7670SA | Consequence |
|
||||
|---|---|---|
|
||||
| VBAT | 3.4–4.2 V (typ 3.8) | **Not the 3V3 rail. Ever.** |
|
||||
| Peak current | **2.8 A** (GSM TX burst) | |
|
||||
| Droop limit | **≤300 mV** at the burst | |
|
||||
| Bulk | **≥300 µF** if the source truly delivers 2.8 A; **≥1000 µF** if not | |
|
||||
| VBAT trace | **≥3 mm** wide or equivalent pour | |
|
||||
| VBAT pins | **All three (55/56/57) must be connected** | RF performance depends on it |
|
||||
| I/O rail | **1.8 V** | **Level shifting to the 3.3 V STM32 is mandatory** |
|
||||
| VDD_1V8 | output, 50 mA max | level-shift reference only, not a system rail |
|
||||
|
||||
### 3.2 🔴 The 10-Ago fleet boot-loop makes this decision for us
|
||||
|
||||
**Read the 10/11-Ago incident before designing this rail.** Root cause on record: a flat 12 Ah
|
||||
pack demanding 2.42 A **plus the LTE attach burst** exceeded the **SD-15B-05's 3 A**, while the
|
||||
**LTC4040 input limit (3.12 A) is set ABOVE the supply's rating** → rail sagged to the PFI
|
||||
4.60 V trip → failover to a flat pack → brownout → reboot → loop. The modem stayed "invisible"
|
||||
because it never finished its 30–60 s LTE attach inside the loop. Standing HW errata: *"input-limit
|
||||
resistor must come DOWN below PSU rating (or bigger PSU)."*
|
||||
|
||||
**v6 adds ~1.6 A of on-board modem to that same 5 V rail.** Naively bucking 3.8 V off MAIN5V
|
||||
would aim the new radio straight at the failure mode that already took the fleet down.
|
||||
|
||||
| | (a) Direct from the 1S pack | (b) Buck from MAIN5V | (c) **Buck from OR(24V_IN, MAIN5V)** ← recommended |
|
||||
|---|---|---|---|
|
||||
| Load on the SD-15B 3 A budget, normal op | none | 🔴 **+1.6 A — the 10-Ago failure mode** | **none — runs off 24 V** |
|
||||
| Alive during blackout | ✔ | ✔ | ✔ (falls back to MAIN5V ← pack ← LTC4040) |
|
||||
| Bench without a pack | ✘ | ✔ | ✔ |
|
||||
| Under-voltage risk | 🔴 pack sags below the 3.4 V modem floor | ✔ | ✔ |
|
||||
| Couples TX bursts onto | 🔴 the LTC4040 BAT node — flagged 🔴 in the EMC audit | the 5 V rail | 24 V (normal) / 5 V (blackout only) |
|
||||
|
||||
**Recommendation (c) — one buck, two inputs.** A **4.5–40 V input, 3 A synchronous buck** (e.g.
|
||||
MP4560 / AP64350 / TPS54331 class — *not* a fourth MP1584, which is already the source of EMC
|
||||
findings 3 and 4) whose input is an **ideal-diode / PMOS OR of `24V_IN` and `MAIN5V`**, output
|
||||
3.8 V, feeding the modem through a **P-FET load switch under MCU control (`MODEM_PWR_EN`)**.
|
||||
|
||||
Why this is the robust *and* cheap answer, for roughly **$1.50 more** than option (b):
|
||||
- With the cabinet live, the modem's ~9.5 W comes off **24 V at ~0.45 A** — the SD-15B never
|
||||
sees it, and the 10-Ago brownout path is structurally removed rather than merely re-tuned.
|
||||
- In blackout the same buck accepts MAIN5V and the radio keeps reporting — invariant #2 holds.
|
||||
- One converter, one inductor, one BOM line. D10 (SMBJ33A) already clamps the 24 V input.
|
||||
- `MODEM_PWR_EN` gives the watchdog authority to hard power-cycle a wedged radio — invariant #3.
|
||||
|
||||
> ⚠ **Still fix the input-limit errata.** Even with the modem off the 5 V rail, the LTC4040's
|
||||
> input limit must come below the PSU rating (or the PSU must grow). v6 is the board where that
|
||||
> gets corrected, not deferred again.
|
||||
|
||||
> ⚠ **And this makes the v5.5 🔴 EMC findings load-bearing.** In blackout MAIN5V does feed the
|
||||
> modem, so the LTC4040 boost hits the ~4 A `/SW_BAT` case the EMC audit flagged. **`/SW_BAT`
|
||||
> ≥2 mm polygon and `/BAT_PLUS` ≥1.5 mm + via farm are v6 hard requirements, not second-run
|
||||
> nice-to-haves.**
|
||||
|
||||
### 3.3 GNSS rail — low-noise, non-negotiable
|
||||
|
||||
The F9P and UM980 have **no internal SAW**. This board carries **three MP1584 switchers**.
|
||||
They will happily lock onto your buck's harmonics.
|
||||
|
||||
→ **Dedicated low-noise LDO** off 3V3 for the GNSS island (not a buck, not a shared tap),
|
||||
placed on the GNSS island itself. Verify the **ZED-F9P VCC ramp rate ≤ 20 µs/V** — that is a
|
||||
*damage* limit, not a guideline; check it against the LDO's soft-start.
|
||||
|
||||
### 3.4 Budget delta (worst case, backup mode)
|
||||
|
||||
| Load | Was (v5.5) | v6 |
|
||||
|---|---|---|
|
||||
| 4G | via 3V3_MODEM to XBee socket, ~1.5 A pk @3.3 V | **2.0 A pk @3.8 V ≈ 7.6 W → ~1.6 A from MAIN5V** |
|
||||
| GNSS | via MOD1 shield off 5 V | 85–130 mA @3.3 V (F9P) |
|
||||
| Everything else | unchanged | unchanged |
|
||||
|
||||
Recompute the LTC4040 backup runtime against the 1S4P ~12 Ah pack once (b) is locked.
|
||||
|
||||
---
|
||||
|
||||
## 4. RF — the part that decides the stackup before anything is placed
|
||||
|
||||
### 4.1 Stackup
|
||||
|
||||
Current topology is already correct and stays: **F.Cu signal+GND pour · In1 solid GND ·
|
||||
In2 3V3 plane · B.Cu signal+GND pour.**
|
||||
|
||||
New requirement: **order the board as controlled impedance** (JLCPCB 4-layer, JLC04161H-7628;
|
||||
F.Cu→In1 prepreg ≈ 0.2104 mm). Target **50 Ω CPWG on F.Cu referenced to In1** — roughly
|
||||
0.35 mm trace / 0.25 mm gap at εr ≈ 4.4, **to be confirmed against JLCPCB's own impedance
|
||||
calculator before layout**, not after.
|
||||
|
||||
Absolute rules for both RF traces:
|
||||
- Referenced to the **In1 GND plane with no split, ever** — no plane cut, no via field gap, no
|
||||
In2 3V3 island underneath.
|
||||
- As short and straight as possible; ground-stitched along **both** sides at ≤ λ/20.
|
||||
- No other signal crosses them on any layer.
|
||||
|
||||
### 4.2 GNSS RF chain
|
||||
|
||||
Active antenna (mandatory for F9P/UM980) → bias tee: **≥68 nH 0603 inductor** (self-resonance
|
||||
above 1.6 GHz) + DC block on the RF path + 100 nF ∥ 100 pF on the bias node + **ESD diode rated
|
||||
>2 GHz** at the connector.
|
||||
|
||||
Wire **`ANT_DETECT` and `ANT_SHORT_N`** to the MCU (or a spare PCF8574 pin). Free antenna-fault
|
||||
telemetry — and you have already lost two units (H0005, H0014) to 0-sats antenna faults you
|
||||
could only diagnose by reading logs.
|
||||
|
||||
⚠ **Do not use `VCC_RF` as the antenna bias on UM980** — Unicore explicitly says its protection
|
||||
is inadequate. Feed the antenna from a separate current-limited rail.
|
||||
|
||||
### 4.3 🔴 The #1 technical risk in this whole change: GNSS desense
|
||||
|
||||
A 23 dBm LTE transmitter drawing 2–3 A bursts, 3 cm from a GNSS front end, is a classic
|
||||
RTK-killer — LNA compression and broadband noise will cost you exactly the fix you are paying
|
||||
for. **The Ardusimple shield has been hiding this problem from you. A bare module hands it to
|
||||
you.**
|
||||
|
||||
Mitigations, in order of effect:
|
||||
|
||||
1. **Maximum board separation** — §5 puts GNSS in the bottom-left corner and LTE at the right
|
||||
edge: **~149 mm apart, essentially the full board diagonal.** This is the whole reason the
|
||||
floorplan is shaped the way it is.
|
||||
2. **Antenna separation ≥ 20 cm** on the enclosure. **Treat this as a design rule, not an
|
||||
optimization.** Two separate bulkheads, as far apart as the cabinet allows.
|
||||
3. Route the modem's return path away from the GNSS island; no shared reference between the
|
||||
two RF regions except the solid In1 plane.
|
||||
4. **Front-end filtering.** Straightforward for an L1-only receiver; **for a multi-band receiver
|
||||
a single 1575 SAW would kill L2/L5**, so rely on an active antenna with good out-of-band
|
||||
rejection + separation. Specify the antenna accordingly.
|
||||
5. **Measure it.** Gate G3 in §10: fix quality with the modem idle vs. transmitting at full
|
||||
power. This is a pass/fail bench test, not an opinion.
|
||||
|
||||
### 4.4 Antenna interface — OPEN, needs Leo
|
||||
|
||||
Recommendation: **2× U.FL on board → pigtails → 2× SMA bulkhead on the enclosure**, placed as
|
||||
far apart as the cabinet allows. This is what makes mitigation #2 possible; an on-board or
|
||||
short-pigtail arrangement forfeits it.
|
||||
|
||||
Needs from you: does the current enclosure have room for a second bulkhead, and where? That
|
||||
answer changes where the U.FL connectors sit, which changes the floorplan.
|
||||
|
||||
### 4.5 LTE link budget — the module is not the variable
|
||||
|
||||
**Every LTE module here transmits at the same power.** 3GPP Power Class 3 is **+23 dBm ±2 dB**,
|
||||
and it is a conformance requirement, not a feature — the $8 A7670SA and the $48 Telit put out
|
||||
the same watt. Receive sensitivity is likewise floored by 3GPP; premium parts buy perhaps
|
||||
**1–3 dB** of margin, not a different class of performance.
|
||||
|
||||
What actually sets the link budget on this product, in descending order of effect:
|
||||
|
||||
| Factor | Typical swing | Notes |
|
||||
|---|---|---|
|
||||
| **Antenna type and placement** | **10–20 dB** | An antenna inside or flush against a metal cabinet on a metal crane is the whole ballgame |
|
||||
| **Band in use** | 6–10 dB | **B28 (700 MHz)** and B5 (850 MHz) penetrate structure far better than B4/B66 at 1700/2100 MHz. The A7670SA has all of them |
|
||||
| **Cable / pigtail loss** | 1–3 dB | Keep the LTE pigtail short; every extra 300 mm of thin coax at 1.9 GHz is real |
|
||||
| **Cat-1bis vs Cat-4** | 1–2 dB in our favour | Lower-order category = better cell-edge behaviour. The cheap choice is also the more robust one |
|
||||
| **Module vendor** | **1–3 dB** | The only line the extra $40 buys |
|
||||
|
||||
> **You already have the field measurement.** The `$GPTXT,NET,H00xx,TELCEL,-87,334 020` line
|
||||
> the fleet emits carries the modem's **RSSI in dBm** and the operator's MCC/MNC (334/020 =
|
||||
> Telcel MX). The example on record shows **−87 dBm**, which is a comfortable working level.
|
||||
> **Query the fleet for the per-unit distribution over the last month, the same way as gate T1.**
|
||||
> If the worst unit sits near −105 dBm you have an antenna/siting problem to solve — and no
|
||||
> module in this survey would have solved it. Phase 1 also adds `csq` to `$PHLG` (field 30),
|
||||
> which makes this permanently visible instead of a one-off query.
|
||||
|
||||
**Conclusion: spend on the antenna and its mounting, not on the modem.** A better bulkhead
|
||||
antenna, a shorter pigtail and a clear mount are worth 10 dB; a more expensive module is worth 2.
|
||||
|
||||
### 4.6 🔴 GNSS multipath — the environment problem, and what actually fixes it
|
||||
|
||||
A container terminal is close to a worst case: stacked steel boxes, RTG and STS cranes,
|
||||
reach stackers, all of it moving. **Multipath is a receiver-and-antenna problem. No modem
|
||||
choice affects it, and no cabling change fixes it.**
|
||||
|
||||
**First, make sure it is actually the diagnosis.** As of 10-Ago the fleet showed **zero
|
||||
corrections applied anywhere** — that produces SPS, full stop, and it looks nothing like
|
||||
multipath. **Multipath presents as float-instead-of-fixed, or a fix that wanders**, and it can
|
||||
only be diagnosed *after* RTCM is confirmed arriving. Phase 1's new telemetry separates the
|
||||
two for the first time:
|
||||
|
||||
| `rtcm_msgs` | fix quality | Diagnosis |
|
||||
|---|---|---|
|
||||
| 0 / not rising | 1 (SPS) | **link or caster** — not multipath |
|
||||
| rising | 1 (SPS) | receiver config, or base/rover **band mismatch** |
|
||||
| rising | 5 (float), never 4 | **multipath / sky obstruction** — this is the real thing |
|
||||
| rising | 4 but `hAcc` noisy | multipath, partially mitigated |
|
||||
|
||||
**Mitigations, cheapest and most effective first:**
|
||||
|
||||
1. **A ground plane under the patch antenna.** A metal disc **≥ 100 mm diameter** beneath a
|
||||
patch kills ground-bounce and is the single best return per peso in this list. Often free.
|
||||
2. **Mount height and clear sky.** Highest point of the machine, away from vertical steel.
|
||||
On an RTG that is the top of the gantry, not the cabinet lid.
|
||||
3. **A better antenna.** A multiband antenna with good **axial ratio** and suppressed gain at
|
||||
low elevation rejects reflections that arrive from below the horizon. Budget US$60–120 —
|
||||
still cheaper than a receiver upgrade and more effective for this failure mode.
|
||||
4. **Raise the elevation mask** in the receiver (10–15°, `CFG-NAVSPG-INFIL_MINELEV`) to discard
|
||||
low-elevation reflected signals, and set the correct **dynamic model** for the machine.
|
||||
5. **Keep L2.** Dual-frequency is materially better at ambiguity resolution under multipath —
|
||||
another reason the L1/L5-only cheap tier was the wrong answer (§1.1).
|
||||
6. **Log C/N₀ per satellite** at a problem site. A healthy sky and a reflective sky look
|
||||
completely different, and it turns an argument into a measurement.
|
||||
|
||||
**If — after 1–6 — multipath is still the binding constraint**, then and only then does silicon
|
||||
help, and the honest options are: **ZED-X20P (+$31/unit**, same 54-LGA 22×17 footprint, so
|
||||
Block N accepts it with a pin-map check, plus better interference mitigation and 55 mA instead
|
||||
of 85 mA) or **Septentrio mosaic-X5** (genuinely best-in-class multipath rejection, but 3–5×
|
||||
the F9P and no LCSC/JLC presence). **Do not buy either on suspicion — buy it on a C/N₀ log.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Floorplan on 140 × 100 — proposed
|
||||
|
||||
Verified from `hbox-watchdog-v5.kicad_pcb` (228 footprints, board x ∈ [60.19, 200.19],
|
||||
y ∈ [40.32, 140.32]).
|
||||
|
||||
**Freed by deletion:**
|
||||
|
||||
| Deleted | Ref | Position | Area recovered |
|
||||
|---|---|---|---|
|
||||
| simpleRTK2B Pro UNO socket | **MOD1** (32 pads) | (90.6, 129.9) | **bottom-left, ~68 × 53 mm** |
|
||||
| XBee sockets, 2.0 mm | **J7, J8** (10 pads each) | (174.2, 74.8) / (196.5, 74.8) | **right edge, ~27 × 20 mm** |
|
||||
| Shield / heatsink bolts | MH7, MH9, MH10, MH11 | various | reclaim as GND stitching (the EMC audit found 8 empty 20 mm stitching cells, **mostly under MOD1**) |
|
||||
|
||||
**Proposed placement:**
|
||||
|
||||
```
|
||||
x=60 ┌────────────────────────────────────────────────────┐ x=200
|
||||
y=40 │ [J10 RJ45] connector row: P5 P10 P2 P11 P6 P8 P12 P4 │
|
||||
│ │
|
||||
│ [U11 W5500] [U16 STM32F411] [U2/L2 fan] │
|
||||
y=70 │ [SW1 SW2 J11] Y3 Y4 [U10 SC16IS] ┌──────────┤
|
||||
│ [U14][U15] │ ★ LTE │ ← freed XBee area
|
||||
│ [U13 PCF8574] │ U19 A7670SA
|
||||
y=100│ │ MFF2 SIM │
|
||||
│ ┌──────────────────┐ [U7] [U6 LTC4040+L6] │ 3.8V buck
|
||||
│ │ ★ GNSS ISLAND │ [U12] [P14]│
|
||||
y=140│ │ U18 + LDO + bias │ [P15][P16] [J9] [P1] │
|
||||
└──┴──[U.FL GNSS]─────┴───────────────────[U.FL LTE]┘
|
||||
```
|
||||
|
||||
- **GNSS island → the freed MOD1 area, bottom-left corner.** It is the furthest point on this
|
||||
board from all four switchers (nearest: U7 at 140.0, 112.9 — ~60 mm; L6/U6 LTC4040 at
|
||||
~173, 110 — ~95 mm), and it puts the U.FL at a board edge. The island holds only the
|
||||
receiver, its LDO, its decoupling and its bias tee — **nothing else**, so a future LG290P
|
||||
swap (§1.4) is a contained respin.
|
||||
- **LTE island → the freed XBee area at the right edge**, next to the existing power cluster
|
||||
where the current already lives. The 27 × 20 mm freed is **too tight for a 24 × 24 module +
|
||||
≥300 µF bulk + buck + MFF2** — reclaim adjacent area by relocating **P14 (RGB_LED_EXT, at
|
||||
195.1, 101.4)** and shifting the **U2/L2 fan buck** north. Budget ~35 × 30 mm.
|
||||
- **Resulting GNSS ↔ LTE separation ≈ 149 mm** — the full diagonal. That is the point.
|
||||
- Keep **2× M2.5 holes** near the modem for a heatsink clamp (§2.4).
|
||||
|
||||
---
|
||||
|
||||
## 6. Signal & connectivity delta
|
||||
|
||||
### 6.1 The good news: the MCU side barely changes
|
||||
|
||||
These nets already exist in `hbox-watchdog-v5.kicad_sch` and simply terminate on solder-down
|
||||
silicon instead of a socket:
|
||||
|
||||
`GNSS_RX` · `GNSS_TX` · `DIN_4G` · `DOUT_4G` · `MODEM_RST` · `MODEM_LED` · `3V3_MODEM`
|
||||
|
||||
**No MCU pin reallocation is required for the basic paths.** USART1/2/6 are all on the F411RET6;
|
||||
RS-485/RS-232 stay on the SC16IS752 over SPI.
|
||||
|
||||
### 6.2 New signals needed
|
||||
|
||||
| Signal | Where it goes | Note |
|
||||
|---|---|---|
|
||||
| `MODEM_PWRKEY` | spare **PCF8574 (U13)** pin | ⚠ **timing is vendor-specific**: SIMCom = low ≥50 ms to power on, ≥2.5 s to power off. Quectel = low ≥2 s on, ≥3 s off. **Never copy timing between vendors.** |
|
||||
| `MODEM_STATUS` | spare PCF8574 pin | asserts ≈11.2 s after power-on (A7670); UART ready ≈8 s |
|
||||
| `MODEM_PWR_EN` | GPIO → P-FET load switch | the hard power-cycle path (§3.2) |
|
||||
| `GNSS_RESET_N` | GPIO | F9P needs a 7–13 kΩ pull-up |
|
||||
| `GNSS_ANT_DETECT`, `GNSS_ANT_SHORT_N` | PCF8574 | free antenna-fault telemetry (§4.2) |
|
||||
| `GNSS_TIMEPULSE` | GPIO / timer input | optional but nearly free |
|
||||
| **RTCM path** | **modem UART → GNSS UART2, direct in hardware** | this is what the Ardusimple shield did internally; replicate it. MCU does the NTRIP handshake, then the RTCM3 stream flows without touching the MCU — exactly the Modo B architecture already engineered |
|
||||
|
||||
> **Action:** confirm free PCF8574 pins against the v5 netlist before Phase 2. `F0_SEL`,
|
||||
> `F1_SEL`, `F2_SEL`, `EXT_LED`, `MODEM_LED`, `CHRG_STAT`, `CHRG_FAULT` are already on it.
|
||||
|
||||
### 6.3 Level shifting — mandatory, do not skip
|
||||
|
||||
Modem UART is **1.8 V**; the STM32 is 3.3 V. Use **TXS0108E** (what Quectel recommends) or the
|
||||
discrete MMBT3904 network SIMCom documents. This is not a "3.3 V is close enough" situation.
|
||||
|
||||
### 6.4 MFF2 SIM
|
||||
|
||||
- DFN 5×6, on the LTE island, close to the modem.
|
||||
- **ESD array at the SIM lines** (ESDA6V1W5 / SMF15C class).
|
||||
- A7670SA has an internal 4.7 kΩ pull-up on USIM1_DATA; **USIM2_DATA would need an external
|
||||
one** — you only need SIM1.
|
||||
- 1.8 V and 3.0 V auto-detect, no strapping needed.
|
||||
- ⚠ **Sourcing:** you need an actual MFF2 SIM from Telcel/AT&T MX or an M2M MVNO with a Mexican
|
||||
profile. **Flat-rate IoT SIMs sized for sensors (500 MB / 10 years) are an order of magnitude
|
||||
too small** for 50–200 MB/month. Start this conversation early — it has a lead time and it is
|
||||
not an engineering task.
|
||||
|
||||
### 6.5 USB
|
||||
|
||||
**J9 (USB-C) is kept** and becomes the modem's USB — recovery path if a firmware update bricks
|
||||
it, plus PPP/RNDIS debugging. **Reserve VBUS/D+/D−/GND and a `USB_BOOT` test point**; USB_BOOT
|
||||
must not be pulled low at power-up. Add the ESD array that v5.5 does not have (**<2 pF** on the
|
||||
data lines). 90 Ω differential.
|
||||
|
||||
---
|
||||
|
||||
## 7. Inherited EMC fix list — v6 hard scope
|
||||
|
||||
Straight from `2026-08-22_…EMC-Audit_v1.0.md`. **None of these is optional in v6:**
|
||||
|
||||
| # | Fix | v6 status |
|
||||
|---|---|---|
|
||||
| 🔴 1 | `/SW_BAT` → **≥2 mm polygon** (was 0.3 mm × 15 mm carrying ~4 A) | **Now load-bearing** — the modem adds ~1.6 A to MAIN5V in backup (§3.2) |
|
||||
| 🔴 2 | `/BAT_PLUS` final approach → **≥1.5 mm + via farm** | same reason |
|
||||
| 🟠 3 | Fan buck FB: **R20/R21 at pin 4**, `/FB_FAN` short and away from `/SW_FAN` | mandatory |
|
||||
| 🟠 4 | **1 µF + 10 µF within 3 mm** of every buck VIN-GND (was 5–8.5 mm) | mandatory — and applies to the new 3.8 V buck |
|
||||
| 🟠 5 | **ESD at every external connector, <10 mm** (D14/D15/D16 were 41 mm from J11) | mandatory + J9 now gets ESD it never had |
|
||||
| 🟡 6 | Crystals **3225 package, <5 mm** from their ICs (Y1/Y3/Y4 are 12–15 mm away in HC-49) | do it |
|
||||
| 🟡 7 | ETH RX pair **coupled and length-matched** (Δ7.5 mm today) | do it |
|
||||
| 🟡 8 | Fill the 8 empty 20 mm stitching cells | free — they were mostly under MOD1, which is being deleted |
|
||||
|
||||
---
|
||||
|
||||
## 8. BOM & cost delta
|
||||
|
||||
**Removed per unit:** Ardusimple **4G NTRIP Master** (€112 @≥50 ≈ **$121**) + Ardusimple
|
||||
**simpleRTK2B Pro** shield *(price to verify)* + MOD1 headers + J7/J8 XBee sockets.
|
||||
|
||||
**Added per unit (indicative):**
|
||||
|
||||
| Item | Cost |
|
||||
|---|---|
|
||||
| ZED-F9P-04B (LCSC C5120257) | $84.82 |
|
||||
| A7670SA-LASC (LCSC C18548276) | $7.58 |
|
||||
| MFF2 SIM | carrier-dependent |
|
||||
| 3.8 V/3 A buck + inductor + ≥300 µF bulk | ~$2.50 |
|
||||
| Low-noise LDO + GNSS decoupling | ~$0.80 |
|
||||
| TXS0108E level shifter | ~$0.50 |
|
||||
| 2× U.FL + 2 pigtails + 2 SMA bulkheads | ~$6 |
|
||||
| RF passives, bias tee, ESD arrays | ~$2 |
|
||||
| Controlled-impedance fab option | per-order, small |
|
||||
| **Subtotal added** | **~$104 + SIM** |
|
||||
|
||||
**The headline: the 4G side alone saves ~$113/unit** ($121 Ardusimple → $7.58 module). Add the
|
||||
simpleRTK2B Pro's margin over a bare F9P on top. At 30 units that is meaningful; at 300 it is
|
||||
the difference between a viable and a non-viable unit price.
|
||||
|
||||
⚠ JLCPCB notes: both modules are **Extended** ($3 one-time setup each), both flagged **X-ray
|
||||
inspection required**, both **Standard PCBA tier only** — no Economic assembly. A7670SA is
|
||||
**MSL3** (168 h floor life ≤30 °C/60 %RH) — matters for JLC and for your own rework.
|
||||
|
||||
---
|
||||
|
||||
## 9. Risk register
|
||||
|
||||
| # | Risk | Severity | Mitigation |
|
||||
|---|---|---|---|
|
||||
| R1 | **GNSS desense from LTE TX** — you buy RTK silicon and get float | 🔴 | §4.3: 149 mm board separation, ≥20 cm antenna separation, measured at gate G3 |
|
||||
| R2 | **Modem thermal** — cheap tier is −30…+80 °C, cabinet may exceed it | 🔴 | **Gate T1 (§2.4): soak a fielded unit now.** Escalate to Telit/u-blox if >75 °C |
|
||||
| R3 | **NTRIP client in STM32** — new firmware on the critical path | 🟠 | **Phase 1: prove it on v5.5's XBee socket while the boards are in fab** |
|
||||
| R4 | **Homologación** — the host now contains a radio (§9.4) | 🟠 | Start the lab conversation in Phase 0; it is calendar time, not engineering time |
|
||||
| R5 | **Pack sag below the 3.4 V modem floor** | 🟠 | Resolved by §3.2 option (b); if (a) is ever chosen, add UV inhibit |
|
||||
| R6 | **v5.5 EMC findings carried forward** | 🟠 | §7 — day-one scope |
|
||||
| R7 | **F9P LCSC stock (~104–129 pcs)** | 🟡 | Consign from Mouser for anything ≥100 units |
|
||||
| R8 | **First RF design in this program** | 🟡 | Controlled impedance ordered, In1 unbroken, external review of the RF section before fab |
|
||||
|
||||
### 9.4 Homologación — start this early
|
||||
|
||||
The regulator changed: **IFT was dissolved 17 Oct 2025 and replaced by the Comisión Reguladora
|
||||
de Telecomunicaciones (CRT)** from 21 Oct 2025. Existing IFT certificates remain valid and
|
||||
labels still carry the IFT number; procedures continue under the same technical norms.
|
||||
|
||||
The certificate is issued to **your product** (applicant + brand + model + year), not to the
|
||||
module. Modular relief exists in practice — the host is labelled *"Este producto contiene el
|
||||
módulo aprobado modelo XXXX, IFT No. …"* — but there is **no blanket exemption**. Applicable:
|
||||
**IFT-008** (cellular) and **NOM-208 / NOM-221**. Testing in an accredited Mexican lab or a US
|
||||
MRA-recognized lab, ~3–5 weeks.
|
||||
|
||||
Two consequences that bite:
|
||||
1. **Freeze on one module before you file.** Switching modules later = new or amended certificate.
|
||||
2. Sources **disagree** on whether the certificate holder must be the importer. **Get this pinned
|
||||
down by the lab, in writing, in Phase 0.**
|
||||
|
||||
None of the cheap LatAm modules advertises an IFT/CRT certificate. **Do not assume one exists
|
||||
because the datasheet says "Latin America."** A7670SA does carry FCC + Anatel, which shortens
|
||||
the RF test scope without eliminating the filing.
|
||||
|
||||
Treat it as one fixed cost amortized over hundreds of units — which is an argument *for* the
|
||||
cheap module, not against it.
|
||||
|
||||
---
|
||||
|
||||
## 10. Phases & gates
|
||||
|
||||
| Phase | Work | Gate |
|
||||
|---|---|---|
|
||||
| **−1** — 🔴 **before any of this** | **Fix the base / caster.** 10-Ago fleet truth: *zero corrections applied anywhere* → check `HelgenTestStation` @ rtk2go:2101. **No silicon in this brief produces RTK if the correction source is broken.** v6 would ship five units of new hardware into the same SPS-not-RTK result | **G0: at least one fielded unit holding GGA quality 4/5 for 1 h on the existing hardware** |
|
||||
| **0** — now, parallel | **Run the T1 telemetry query (§2.4) — it is free and it decides the modem tier** · lock §1.4 GNSS · lock §3.2 rail · answer §4.4 antenna · **call a homologación lab** · order A7670SA breakout + M2M SIM (~$40 total) | Decisions in this doc, v1.1 |
|
||||
| **1** — while v5.5 is in fab | **NTRIP-in-STM32 spike on a v5.5 board via the XBee socket** + A7670SA breakout: AT driver, TCP, NTRIP v1 GET, RTCM3 → GNSS UART2, reconnect/backoff, `$PHLG` signal telemetry | **G1: 24 h continuous RTK fix through the on-board modem path, ≥1 reconnect survived** |
|
||||
| **2** — schematic | New blocks (GNSS island, LTE island, 3.8 V rail, MFF2, level shift, RF chains) + **all of §7** | **G2: ERC 0** + external review of the RF and power sections |
|
||||
| **3** — stackup & floorplan | Confirm 50 Ω geometry against JLCPCB's calculator · place per §5 · verify GNSS↔LTE separation · confirm the enclosure takes a second bulkhead | **G3 (design): RF traces referenced to unbroken In1, no exceptions** |
|
||||
| **4** — route | RF first, then power (wide from day one), then signal · zones · DRC | DRC 0 · independent netlist parse |
|
||||
| **5** — DFM | BOM/CPL, LCSC pass, consignment plan, controlled-impedance order option | fab release |
|
||||
| **6** — first article | Bring-up, DFU, **then G3 measured**: fix quality with modem idle vs. TX at full power | **G3 (bench): no measurable degradation in fix quality or C/N₀ under LTE TX** |
|
||||
|
||||
**Phase 1 is the whole trick.** It converts the largest unknown in v6 — firmware you have never
|
||||
shipped talking to a radio you have never soldered — into a bench result you obtain on hardware
|
||||
already paid for, weeks before committing a layout.
|
||||
|
||||
---
|
||||
|
||||
## 11. What I'd argue for, in one paragraph
|
||||
|
||||
Take the **A7670SA** — it is the cheapest part that is simultaneously band-correct, JLC-assemblable,
|
||||
2G-capable and well-documented, and the money you save there is real ($113/unit against the
|
||||
Ardusimple). But **keep the ZED-F9P for v6**. The $450 you would save at 30 units by switching
|
||||
to an LG290P does not pay for a new configuration protocol, a new backend decoder, and a fresh
|
||||
RTCM-compatibility unknown — in the same cycle where you are already taking on your first RF
|
||||
front end, your first cellular radio and your first NTRIP client. Do the receiver cost-down as
|
||||
a contained v7 respin of one corner of the board, once volume makes it worth $17k. **One new
|
||||
unknown at a time** is the rule that gets this board to work on the first article.
|
||||
|
||||
---
|
||||
|
||||
## 12. Cross-references
|
||||
|
||||
- v5.5 EMC fix list: `2026-08-22_Drogheda_EinarBoard-v5_EMC-Audit_v1.0.md`
|
||||
- Prior modem survey (bench-era): `2026-07-08_Drogheda_4G-Modem-Alternatives_v1.0.md`
|
||||
- 3.0 brief & method rules: `2026-08-12_Drogheda_EinarBoard-3.0_MCU-Integration_Design-Brief_v1.0.md`
|
||||
- Canonical electrical state: `watchdog-electrical-design-state.md`
|
||||
- Invariants touched: **#1** power-off safe · **#2** never vanish (§2.3, §3.2) · **#3**
|
||||
corrections ride the 4G, recover by power-cycling (§3.2 `MODEM_PWR_EN`)
|
||||
57
docs/historia/wialon-viajes-hallazgos.md
Normal file
57
docs/historia/wialon-viajes-hallazgos.md
Normal file
@ -0,0 +1,57 @@
|
||||
# HBOX RTK vs CalAmp on RTG ZPMC-20 — Wialon trip discrepancy: root cause, framework, fixes (2026-10-06)
|
||||
|
||||
**Context.** ICAVE compares Wialon trips from two devices on the same RTG crane (ZPMC-20): HBOX **H0013** (EinarBoard V5.5, fw 0.6.0, Wialon unit `RT-ZPM-20 (RTK)`) and the legacy CalAmp (`RT-ZPMC-20`). They derive fuel consumption from trips and asked why the two disagree. Data from Jacobo (Drive folder, 5 and 6 Oct 2026): Wialon NMEA message exports of both units, Wialon *Trips* .xlsx of both, and H0013's raw `historial` from htracker. Everything is now in `Drogheda/06-Testing-Validation/wialon-trips/` (raw data per day + `informe_viajes_2026-10-0{5,6}.html` + `analisis_*.json`).
|
||||
|
||||
## Bottom line
|
||||
|
||||
The complaint as phrased ("more trips from the RTK → more fuel") is the **opposite** of what the data shows. The CalAmp over-reports trips and mileage by **8–18×** because it reports movement while the crane is parked; the HBOX is clean but Wialon's trip-distance threshold discards most real RTG manoeuvres.
|
||||
|
||||
| | CalAmp (RT-ZPMC-20) | HBOX H0013 (RTK) | Real crane motion (from the 1 Hz RTK track) |
|
||||
|---|---|---|---|
|
||||
| 5-Oct 15:00–17:00 | 1 trip · 1 h 47 min · **3.30 km** | 1 trip · 6 min · 0.19 km | **0.41 km in 8 min** — 6 manoeuvres (12–166 m, 2–5 km/h) |
|
||||
| 6-Oct 08:00–10:46 | 2 trips · 2 h 40 min · **6.17 km** | 1 trip · 2 min · 0.17 km | **0.35 km in 6 min** — 8 manoeuvres (11–170 m, 2–6 km/h) |
|
||||
|
||||
### Evidence
|
||||
1. **Same Wialon trip-detector settings on both units.** A Wialon-equivalent detector (by GPS speed ≥ 1 km/h, parking ≥ 300 s, min trip distance ≈ 150 m, speed floored to integer km/h as Wialon stores it) reproduces **all four** Wialon Trips reports: RTK boundaries within ±3–5 s, CalAmp within 0–30 s (one 30-s sample). Parameter sweep over 525 combinations: only this family fits all four. So the difference is in the data, not the configuration.
|
||||
2. **CalAmp false motion.** Per 30-s CalAmp sample, with the RTK as ground truth (0.2 m resolution; H0013 historial matches the Wialon export to 0.07 m, zero lag; Wialon speed = floor(km/h)):
|
||||
- while the crane is stationary, CalAmp reports 1–5 km/h **33.5 % (5-Oct) / 43.8 % (6-Oct)** of samples;
|
||||
- its position jumps 6.5 / 10.4 m median (p90 15 / 26 m, max 34 / 140 m) between consecutive messages while parked → **1.67 km / 3.64 km phantom mileage** per window;
|
||||
- it detects real motion only 70 % / 47 % of the time;
|
||||
- stationary drift it integrates: 785 / 1152 m/h. Because it rarely strings 300 s of zero speed, Wialon never closes the trip → one all-afternoon trip.
|
||||
- Wialon's printed CalAmp mileage (3.30 / 5.81 km) is even higher than the path sum of its own exported positions (1.94 / 3.81 km) — Wialon's counter for that unit likely uses the CalAmp odometer parameter; either way it is noise.
|
||||
3. **HBOX under-reporting is a Wialon threshold, not a device problem.** 93–97 % of samples at 0 km/h, stationary scatter p50 1.9 m / p95 4.9 m (DGPS fix quality 2, 12 sats, HDOP 0.5 — note: *not* RTK fixed/float; corrections issue is separate, see earlier notes). Real RTG moves last 20–120 s and cover 10–170 m; with min distance ≈ 150 m Wialon kept 1 of 6 (5-Oct) and 1 of 8 (6-Oct). The one it kept (15:16–15:22, 166 m) only crossed 150 m thanks to rest-jitter mileage during a 3.8-min pause inside the trip.
|
||||
4. **HBOX stationary drift** at 1 Hz integrates ~0.33 km/h of path (does not affect Wialon trips, which ignore 0 km/h, but dirties tracks/counters) — fixed by the retranslator motion filter below.
|
||||
|
||||
### The map says it in one glance
|
||||
RTK track = clean line along the container block; CalAmp track = scribble around a parked crane (see the reports).
|
||||
|
||||
## What was built — `htracker/trips/` (committed to the repo folder on Leo's Mac, not yet pushed)
|
||||
|
||||
Pure-stdlib Python package (openpyxl optional) + 40-line hook in `retranslator.py` + 26-line hook in `web_monitor.py`. Full details, env vars and deploy steps: `04-Source-Code/htracker/CHANGES-2026-10-06-viajes.md`.
|
||||
|
||||
- `nmea.py` readers: Wialon NMEA export, htracker `historial_unidades` ring files (unsorted → sorted/deduped), raw NMEA, `.gz`, Wialon Trips `.xlsx` (openpyxl must *not* be in read_only mode — Wialon's xlsx has a bad dimension).
|
||||
- `detector.py`: `TripParams` + `detect_trips()` (Wialon-equivalent; defaults 1 km/h · 300 s · 150 m); `MotionFilter` (REPOSO/MOVIMIENTO hysteresis: ≥1 km/h × 2 samples or > 8 m from anchor → moving; < 1 km/h × 10 s → rest; at rest forward speed 0 and the anchor position). Hold radius 8 m because a parked F9P in DGPS wanders up to ~5 m over 20 min; 3 m caused 114 spurious transitions.
|
||||
- `compare.py`: fine-vs-coarse alignment, confusion matrix, phantom mileage, true mileage, trip matching, chart series; `analyze()` builds the whole JSON.
|
||||
- `report.py`: self-contained HTML (Chart.js + Leaflet CDN, monitor dark theme) — verdict cards, speed timeline with Wialon trips as bands, cumulative km (device path vs real vs Wialon-printed), map, noise histogram, Wialon-vs-recalculated tables, real-manoeuvre list, filter simulation, method. Also the `/trips` monitor page (same renderer, API-fed).
|
||||
- `store.py`: `trips_positions.db` (SQLite WAL) fed by the retranslator at 1 Hz while moving, 1/10 s at rest (10 s, not 30: at 30 s the rest-jitter mileage disappears and the marginal 166-m Wialon trip would be lost; at 10 s all four Wialon reports still reproduce). ≈ 0.7 MB/unit/day, retention 14 d (~300 MB for 30 units). Writer thread, UDP loop never blocks.
|
||||
- `gateway.py`: `TripsGateway.process()` hook (store raw fix; if the filter is enabled for the unit, send Wialon a copy with speed 0.00 and the held position). Never raises. Env: `GW_TRIPS_STORE_ENABLED=1` (default on), `GW_MOTION_FILTER_ENABLED=0` (default off), `GW_MOTION_FILTER_UNITS=H0013` for a staged rollout.
|
||||
- `monitor_routes.py`: `GET /trips`, `GET /api/trips_1hz/units`, `POST /api/trips_1hz` (unit, window, params, optional reference NMEA file + Wialon .xlsx uploads, raw vs sent data). Same auth/unit gates as `/api/track`. Nav links added on dashboard and reports.
|
||||
- Tests: 22 (`unittest`, real data fixtures 116 KB gzip): Wialon-report reproduction, readers, filter behaviour, store downsampling, gateway, FastAPI routes (skip without fastapi/openpyxl). Green here and on Leo's Mac (3.14; 7 skipped there for missing openpyxl/fastapi). End-to-end with a fake Wialon listener verified `#SD#` packets carry speed 0.00 + held position at rest and pass through unchanged when moving.
|
||||
- CLI: `python3 -m trips.cli --device … --reference … --wialon-trips-device … --out informe.html --json …`.
|
||||
|
||||
Existing `/api/trips` (5-minute `gps_positions.db`) cannot see 20–120 s crane moves; it stays as is.
|
||||
|
||||
## Recommendations (in order)
|
||||
1. **Wialon config for HBOX units:** min speed 1 km/h, parking 120–300 s, **min trip distance 10 m**, min time 0. Turns 1 → 6 and 1 → 8 trips (0.50 / 0.48 km ≈ real). Do *not* apply to the CalAmp (would explode its false trips).
|
||||
2. **Enable the motion filter on H0013 only** (`GW_MOTION_FILTER_ENABLED=1`, `GW_MOTION_FILTER_UNITS=H0013`), validate 2–3 days with `/trips` ("raw" vs "sent"), then fleet-wide. It cleans tracks/counters; it does not change trip counts under the current 150 m threshold.
|
||||
3. **Fuel = engine hours, not trip km** for an RTG. Forward ignition (`$PHEV IGN_ON/OFF`, fw 0.5.1+; PA5) as a Wialon IPS `#D#` param `ign` and use "ignition sensor" engine hours (and optionally ignition-based trips). Proposed, not implemented — needs a one-unit bench check that Wialon accepts the `D` packet with params from the current unit type.
|
||||
4. Do not "match" the HBOX to the CalAmp.
|
||||
|
||||
Customer-facing note (es-MX, Leo's voice): `00-Project-Management/client-comms/2026-10-06_Drogheda_Viajes-Wialon-RTK-vs-CalAmp_Nota-ICAVE_v1.0.md`.
|
||||
|
||||
## Open points / caveats
|
||||
- Wialon's detector parameters are inferred from the reports (they fit exactly), not read from the account — confirm in Wialon → unit → Trip detector before changing them.
|
||||
- H0013 fix quality is 2 (DGPS), never 4/5; the RTK-corrections thread is unchanged by this work.
|
||||
- The Wialon-printed CalAmp mileage source (odometer param vs GPS) is unverified; irrelevant to the conclusion.
|
||||
- The `historial` ring covers only ~1 h and is unordered; the new store is the proper source for `/trips`.
|
||||
- Git: Leo runs `git add trips/ retranslator.py web_monitor.py .gitignore CHANGES-2026-10-06-viajes.md README.md`, commit, `./push-deploy.sh`; then check `journalctl -u htracker` for "TripsStore iniciado".
|
||||
157
docs/inventario/flujo-compras-28oct.md
Normal file
157
docs/inventario/flujo-compras-28oct.md
Normal file
@ -0,0 +1,157 @@
|
||||
---
|
||||
title: "Cómo funciona el portal para compras e inventario: el flujo acordado en la junta del 28-oct"
|
||||
date: 2026-10-28
|
||||
tags: [project-drogheda, hbox, compras, inventario, cotizador, integracion, portal]
|
||||
estado: flujo acordado en la junta del 28-oct; fase 1 del portal construida y probada (58 pruebas)
|
||||
---
|
||||
|
||||
# Cómo funciona el portal para compras e inventario
|
||||
|
||||
**28-oct-2026 · v1.0 · Claude (CTO/CSO), de la junta de dirección del 28-oct (10:17) con Leo y Adriana.**
|
||||
Reemplaza el flujo de [Cotizador ↔ centro de mando](integracion-cotizador.md) del 27-oct en la parte del
|
||||
**proceso**: aquel documento describía una sola puerta de autorización en compras; en la junta quedaron **dos
|
||||
puertas de Adriana** y el orden real de cotización y pago. El contrato técnico entre los dos sistemas (las rutas
|
||||
firmadas, los avisos, la base) **no cambia** y sigue siendo el de ese documento y el de las
|
||||
[instrucciones para el cotizador](integracion-cotizador-instrucciones.md).
|
||||
|
||||
!!! warning "Arranca mañana"
|
||||
Leo, en la junta: **«a partir de mañana todas las requisiciones relacionadas HBOX se van a hacer por el
|
||||
sistema»**. Alcance: **solo instalaciones de HBOX**; mantenimiento se evalúa en enero. Lo que llegue por
|
||||
WhatsApp se regresa al sistema.
|
||||
|
||||
## Por qué
|
||||
|
||||
Hoy el material se pide por WhatsApp, por teléfono y en tres direcciones a la vez. Leo, en la junta: *«te hablan
|
||||
a ti, le hablan a Tania, me hablan a mí y ya es un cagadero»*. El resultado es que nadie sabe qué se pidió, qué se
|
||||
autorizó, qué viene en camino ni qué llegó — y cuando llega, no entra al inventario de nadie. Un solo hilo, del
|
||||
que pide al que recibe, arregla las dos cosas: la trazabilidad del gasto y las existencias por sitio.
|
||||
|
||||
## El flujo, paso por paso
|
||||
|
||||
| # | Dónde | Quién | Qué pasa |
|
||||
|---|---|---|---|
|
||||
| 1 | **Cotizador** | El responsable del sitio | Levanta la requisición en el proyecto DROGHEDA, con **sitio de entrega** y los renglones de material. Sin precios: solo qué necesita y cuánto. |
|
||||
| 2 | **Cotizador** | **Adriana** | **Autoriza renglón por renglón.** Puede regresar un renglón con comentarios; es ida y vuelta con quien pidió hasta que queda limpio. Leo: *«vamos a autorizar ítem por ítem si es necesario, para que no pidan tampoco de más»*. |
|
||||
| 3 | **Cotizador** | **Tania** (con Efraín y Leo) | Con la requisición autorizada, cotiza: ferreterías (incluido Edwin), Mercado Libre, o Jesús lleva la lista a la ferretería. Marca por renglón si existe el ítem exacto, si el proveedor ofrece otra marca, o si hay que aclararlo. |
|
||||
| 4 | **Cotizador** | **Adriana** | **Visto bueno** de la cotización: que sea el material correcto, las cantidades correctas y **el lugar de entrega correcto**. |
|
||||
| 5 | **Cotizador** | **Tania** | Con el visto bueno sale la orden de pago; paga y empieza el tiempo de entrega. |
|
||||
| 6 | **Cotizador** | Compras o quien pidió | Cuando el proveedor manda el material: **En camino**, con paquetería, guía y fecha estimada. → El centro de mando lo pone **en camino** en la pestaña del sitio en [Inventario](/tableros/inventario/). |
|
||||
| 7 | **Centro de mando** | **El responsable del sitio** | Abre [Compras](/tableros/compras/) y da **Recibir** con lo que llegó **de verdad** (puede ser menos). Eso da de alta el material en el inventario de su sitio. |
|
||||
| 8 | **Cotizador** (solo) | — | Cada 15 min consulta las recepciones: si llegó todo, cierra la requisición solo; si llegó una parte, lo anota. |
|
||||
|
||||
**Quién recibe en cada sitio:** Alán (VER · ICAVE) · Jesús Honorato (ZLO · TIMSA) · Miguel (ESE · EIT) ·
|
||||
Antonio (LZC · LCT) · Jesús Romero (QRO · Querétaro).
|
||||
|
||||
**Quién puede levantar requisiciones** (lo que Leo nombró en la junta, a confirmar por Adriana): Jesús Romero,
|
||||
Miguel, Antonio, Daniel, **Alán — que entra en lugar de Paola** — y Adriana.
|
||||
|
||||
## Lo que decide cada sistema
|
||||
|
||||
El reparto no cambió y conviene tenerlo claro porque es lo que evita duplicar trabajo:
|
||||
|
||||
| | Cotizador | Centro de mando |
|
||||
|---|---|---|
|
||||
| Dinero, montos, cotizaciones, pagos, proveedores | **Sí** | nunca |
|
||||
| Autorización de la requisición y de la OC | **Sí** (Adriana, dos puertas) | nunca |
|
||||
| Qué material, cuánto y a qué sitio va | **Sí** | lo recibe y lo refleja |
|
||||
| Existencias por sitio, en camino, kit de instalación | — | **Sí** |
|
||||
| Quién recibió, cuánto llegó y cuándo | lo consulta | **Sí** |
|
||||
| El pendiente de cada OC, con su responsable y su fecha | — | **Sí** |
|
||||
|
||||
**El centro de mando no recibe montos.** Si un aviso del cotizador los trae, se descartan antes de guardar: esta
|
||||
página la ve todo el equipo (D-27-4, confirmado en la junta).
|
||||
|
||||
## Decisiones
|
||||
|
||||
| # | Decisión | Estado |
|
||||
|---|---|---|
|
||||
| D-28-1 | La autorización de la requisición es de **Adriana, renglón por renglón**, y hay un **segundo visto bueno** suyo sobre la cotización antes del pago. Corrige D-27-4, que dejaba la autorización en compras (Efraín, Tania, Leo). | **Tomada** (junta 28-oct) |
|
||||
| D-28-2 | El sistema arranca **el 29-oct**, solo para **instalaciones de HBOX**. Mantenimiento se evalúa en enero. | **Tomada** (junta 28-oct) |
|
||||
| D-28-3 | **Alán** entra en lugar de **Paola** como autorizado para levantar requisiciones. | **Tomada** (junta 28-oct) |
|
||||
| D-28-4 | El material se recibe en el centro de mando **desde el celular, ítem por ítem**, y eso crea el movimiento de entrada en el inventario del sitio. Es D-27-1, ahora con la pantalla que lo hace. | **Tomada** (junta 28-oct) |
|
||||
| D-28-5 | El **conteo físico de inventario es recurrente**: una vez al mes (Querétaro ya quedó; los puertos, esta semana). | **Tomada** (junta 28-oct) |
|
||||
| D-27-3 | Códigos de sitio **VER · ZLO · ESE · LZC · QRO**. Leo ya agregó el campo de sitio en el cotizador durante la junta. | Falta el «sí» formal (`d-27-3-sitios-oc`) |
|
||||
| D-27-2 | Base **SQLite** en el servidor del sitio. Construida y probada. | **Tomada** (27-oct) · **hecha** |
|
||||
|
||||
## Lo que ya está construido (fase 1 del portal)
|
||||
|
||||
Todo esto está escrito y probado; falta que Leo lo publique.
|
||||
|
||||
- **`backend/compras.py`** — las órdenes de compra en SQLite (`/srv/projects/drogheda/drogheda.db`, modo WAL), las
|
||||
tres rutas firmadas de integración, los envíos «en camino» por sitio, el pendiente de cada OC y el cierre
|
||||
automático al recibir todo. **58 pruebas** (`backend/tests-1028.sh`), todas pasando.
|
||||
- **Página [Compras](/tableros/compras/)** — una pestaña por sitio, una tarjeta por OC con sus renglones, el botón
|
||||
**Recibir** (completo o en parte) y el botón **Ligar** para los renglones que llegaron sin ítem de inventario.
|
||||
Sin montos.
|
||||
- **Las dos puertas para recibir funcionan:** desde Compras (la OC completa de una vez) o desde el envío «en
|
||||
camino» de [Inventario](/tableros/inventario/). Las dos terminan en lo mismo y Compras se entera de ambas.
|
||||
- **Un pendiente por OC:** «Recibir OC-… en ZLO», pista Compras, el sitio de entrega, el responsable del sitio y
|
||||
vence el día de la entrega. Se cierra solo cuando llega todo; si llega una parte, se queda abierto y dice qué
|
||||
falta. **No se cierra a mano.**
|
||||
- **Las rondas de PRUEBA del cotizador** (`"prueba": true`) se guardan aparte: no tocan el inventario ni el tablero.
|
||||
|
||||
**Para encenderlo** Leo genera el secreto (`openssl rand -hex 32`) y lo pone en los dos lados:
|
||||
`COTIZADOR_SECRETO` en `actas-hbox.service` y `PORTAL_SECRETO` en las propiedades del script del cotizador.
|
||||
Mientras no esté, las rutas de integración contestan 503 con un mensaje claro, la página lo avisa y **el resto del
|
||||
sitio funciona igual**.
|
||||
|
||||
## Lo que falta del lado del cotizador
|
||||
|
||||
Los pasos 1 a 7 de las [instrucciones del 27-oct](integracion-cotizador-instrucciones.md) siguen vigentes, con dos
|
||||
cosas que salieron en la junta:
|
||||
|
||||
1. **El formulario no deja guardar la requisición sin cotización adjunta** y eso está al revés: la requisición la
|
||||
levanta quien necesita el material, la cotización la adjunta compras después. Leo: *«eso se tiene que
|
||||
modificar»*. Es lo primero, porque bloquea el arranque del 29-oct.
|
||||
2. **El campo de sitio de entrega** ya lo agregó Leo en la junta; falta la regla que lo hace obligatorio en los
|
||||
proyectos ligados (del lado del servidor, no solo de la interfaz).
|
||||
|
||||
## Movimientos de inventario: cómo se nombra cada salida
|
||||
|
||||
Lo que se acordó en la junta, con lo que el inventario ya sabe hacer:
|
||||
|
||||
| Lo que pasó | Cómo se registra |
|
||||
|---|---|
|
||||
| Llegó material comprado | **Recibir** la OC en Compras (o el envío en Inventario). No se captura a mano. |
|
||||
| Material de un puerto a otro (Manzanillo → Ensenada) | **Envío** desde el sitio que lo manda; el que lo recibe da **Recibir** y revisa que esté completo. |
|
||||
| Se usó en una instalación | **Salida**, con la unidad o el acta como referencia. |
|
||||
| Se dio de baja o se destruyó | **Salida**, con el motivo. |
|
||||
| Contamos y no cuadra | **Conteo físico**: lo que hay en ese sitio queda exactamente en ese número. |
|
||||
|
||||
Las salidas **no se autorizan**: se supervisan. Lo que las cuadra es el conteo físico mensual (D-28-5).
|
||||
|
||||
## Lo que el equipo reportó del sitio en la junta
|
||||
|
||||
Se levantan como incidencias o pendientes; ninguna es de compras, pero todas estorban el arranque:
|
||||
|
||||
| Qué | Quién lo vio | Estado |
|
||||
|---|---|---|
|
||||
| **Las horas se ven en UTC** (6 h adelantadas) | todo el equipo | **arreglado**: `TZ=America/Mexico_City` en el servicio. Lo guardado antes del 28-oct sigue en UTC. |
|
||||
| Un acta con fotos **se perdió al salir de la pantalla y volver** | Daniel | pendiente — es pérdida de trabajo, va primero |
|
||||
| El formulario **no deja escoger fotos de la galería** del celular | Jesús Romero | pendiente — mientras tanto, se guardan y se reabre el acta para subirlas |
|
||||
| **Gibran no puede entrar** (su usuario es `gibranlp`) | Gibran | pendiente |
|
||||
| La bandera de «contraseña inicial» **no se apaga** aunque ya la cambiaron | Adriana | pendiente |
|
||||
| **Dos sesiones en el mismo navegador** no funcionan (cookies) | Leo | pendiente |
|
||||
| Se quiere **descargar un acta como documento** | Adriana | pendiente |
|
||||
| La flecha de cambio de estado del tablero no respondía | Leo | se arregló reiniciando el servicio |
|
||||
| **Pendientes sin sentido creados solos** desde la minuta de la junta anterior | Leo | el agente diario los crea; hay que afinarlo |
|
||||
|
||||
## Lo que no cuadra con lo escrito
|
||||
|
||||
- **El agente aduanal de los tres pedimentos es «Tera Mercurio», no Grupo Lorenzo.** El
|
||||
[costeo del 26-oct](/hardware/costeo-jlc-26oct/) supone Grupo Lorenzo (patente 3927) y MX$6,500 por pedimento:
|
||||
esas cifras hay que rehacerlas con la cotización real.
|
||||
- **La cotización de la herrería de las bases de RTG**: Adriana dice que Jesús y Tania ya la mandaron; Leo dice que
|
||||
nunca le llegó. Las medidas y fotos sí están; nada se ha pedido. Sigue bloqueada por el pago.
|
||||
|
||||
## Riesgos
|
||||
|
||||
- **El arranque es mañana y el cotizador todavía no manda avisos.** No es bloqueante: las requisiciones pueden
|
||||
correr en el cotizador desde el día uno y el material entrar al inventario a mano; lo que se pierde mientras
|
||||
tanto es el «en camino» automático y el cierre solo. Se gana en cuanto Leo ponga el secreto.
|
||||
- **Seis personas nuevas usando un sistema nuevo el mismo día.** Por eso la sesión general que va a programar
|
||||
Adriana, y por eso el alcance chico (solo HBOX): Leo lo dijo en la junta, es para *«probar y sacar todos los
|
||||
errores»*.
|
||||
- **Renglones sin ítem de inventario.** Un renglón que el cotizador manda sin ítem no entra solo al inventario.
|
||||
Queda visible como «por ligar» en Compras y se puede recibir igual (se anota sin tocar existencias), pero si
|
||||
nadie lo liga, el material llega y no aparece contado en ningún lado.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 271 KiB |
@ -10,7 +10,7 @@ Qué hay **en cada sitio** —Querétaro (taller) y los cuatro puertos: ICAVE, T
|
||||
|
||||
**[Abrir el inventario →](../tableros/inventario.md)** · por sitio: [ICAVE](../tableros/inventario.md?sitio=icave) · [TIMSA](../tableros/inventario.md?sitio=timsa) · [EIT](../tableros/inventario.md?sitio=eit) · [LCT](../tableros/inventario.md?sitio=lzc) · [Querétaro](../tableros/inventario.md?sitio=qro)
|
||||
|
||||
**Compras (27-oct):** las órdenes de compra del cotizador van a llegar solas, en camino a su sitio, para darles **Recibir** aquí: [cómo funciona](integracion-cotizador.md).
|
||||
**Compras (28-oct):** las órdenes de compra del cotizador llegan solas, en camino a su sitio, y se reciben en [Compras](../tableros/compras.md) o en la pestaña del sitio. El proceso completo, de quien pide el material a quien lo recibe: [el flujo acordado en la junta del 28-oct](flujo-compras-28oct.md) · [contrato con el cotizador](integracion-cotizador.md).
|
||||
|
||||
## Qué hay aquí
|
||||
|
||||
@ -19,6 +19,7 @@ Qué hay **en cada sitio** —Querétaro (taller) y los cuatro puertos: ICAVE, T
|
||||
| [Inventario](../tableros/inventario.md) | **Una pestaña por sitio:** lo que hay ahí, lo que va en camino hacia ese sitio y, con el kit, cuánto falta para instalar las unidades que pongas. **Todo:** el total de cada ítem y dónde está. Movimientos (entrada, salida, conteo físico, envío y recibir), alta de ítem y exportar CSV con una columna por sitio |
|
||||
| [Instalaciones](../tableros/instalaciones.md) | Cuántas unidades van instaladas y probadas en cada sitio. Su botón **Material** abre la pestaña de ese sitio en el inventario |
|
||||
| [Tablero de producción](../tableros/produccion.md) | Lo que el inventario tiene que cubrir: 113 V5.6 · 327 V5.7 · 130 v6 |
|
||||
| [Compras](../tableros/compras.md) | Las órdenes de compra del cotizador por sitio: qué viene en camino, qué llegó y qué falta por recibir. Sin montos: el dinero vive en el cotizador |
|
||||
| [Pendientes, pista Compras](../tableros/pendientes.md?pista=compras) | Pedidos en curso (SD-15B-05 ×113, paquetes LFP, gabinetes, cajas HBOX, antenas, ferretería de EIT y TIMSA) |
|
||||
|
||||
## Qué se registra y qué no
|
||||
|
||||
55
docs/tableros/compras.css
Normal file
55
docs/tableros/compras.css
Normal file
@ -0,0 +1,55 @@
|
||||
/* Compras (28-oct-2026). Lo común (tabla .t, .acciones, .pill, diálogos, pestañas .inv-tabs) viene de
|
||||
tablero.css y de inventario.css: esta página reusa la clase tb-inv para las pestañas de sitio. */
|
||||
|
||||
.tb-compras .cmp-apagado { margin: 0 0 16px; }
|
||||
|
||||
/* ---- Una tarjeta por orden de compra -------------------------------------- */
|
||||
.tb-compras .cmp-oc { border: 1px solid var(--hg-linea-2); border-radius: 12px; background: var(--hg-plano-2);
|
||||
padding: 14px 16px 12px; margin: 0 0 14px; }
|
||||
.tb-compras .cmp-oc.cancelada { opacity: .62; }
|
||||
|
||||
.tb-compras .cmp-cab { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin: 0 0 8px; }
|
||||
.md-typeset .tb-compras .cmp-cab h2 { margin: 0; font-size: 18px; font-variant-numeric: tabular-nums; }
|
||||
.tb-compras .cmp-cab .chip { font-size: 12px; padding: 1px 8px; border-radius: 6px; background: var(--hg-elevado);
|
||||
border: 1px solid var(--hg-linea); color: var(--hg-texto-2); white-space: nowrap; }
|
||||
.tb-compras .cmp-cab .chip.tarde { background: var(--hg-bloqueado-bg); color: var(--hg-bloqueado); border-color: transparent; font-weight: 600; }
|
||||
|
||||
/* estados de la OC: los mismos tonos que el tablero de pendientes */
|
||||
.tb-compras .pill.e-aut { background: var(--hg-plano-2); color: var(--hg-texto-2); }
|
||||
.tb-compras .pill.e-pag { background: var(--hg-curso-bg); color: var(--hg-curso); }
|
||||
.tb-compras .pill.e-cam { background: var(--hg-aviso-bg); color: var(--hg-aviso); }
|
||||
.tb-compras .pill.e-par { background: var(--hg-aviso-bg); color: var(--hg-aviso); }
|
||||
.tb-compras .pill.e-rec { background: var(--hg-hecho-bg); color: var(--hg-hecho); }
|
||||
.tb-compras .pill.e-can { background: var(--hg-bloqueado-bg); color: var(--hg-bloqueado); }
|
||||
|
||||
.tb-compras .cmp-meta { display: flex; flex-wrap: wrap; gap: 4px 18px; font-size: 13px; color: var(--hg-texto-2);
|
||||
margin: 0 0 10px; }
|
||||
.tb-compras .cmp-meta b { font-weight: 600; color: var(--hg-texto-2); }
|
||||
|
||||
/* ---- Renglones ------------------------------------------------------------ */
|
||||
.tb.tb-compras #cont table.cmp-ren { min-width: 0; margin: 0; } /* tablero.css fuerza 780 px: esta ya cabe */
|
||||
.tb-compras .cmp-ren { table-layout: auto; width: 100%; }
|
||||
.tb-compras .cmp-ren tr.recibido td { color: var(--hg-texto-2); }
|
||||
.tb-compras .cmp-ren tr.recibido td:last-child { color: var(--hg-hecho); font-weight: 600; }
|
||||
.tb-compras .cmp-ren .desc { font-weight: 500; color: var(--hg-texto-fuerte); }
|
||||
.tb-compras .cmp-ren code { font-size: 12.5px; }
|
||||
.tb-compras .cmp-ren .aviso-t { color: var(--hg-aviso); }
|
||||
.tb-compras button.cmp-ligar { padding: 3px 10px; font-size: 12.5px; }
|
||||
|
||||
.tb-compras .cmp-acc { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin: 10px 0 0;
|
||||
padding-top: 10px; border-top: 1px dashed var(--hg-linea); }
|
||||
.tb-compras .cmp-acc button { padding: 6px 16px; }
|
||||
|
||||
.tb-compras dialog .t { margin: 6px 0 12px; }
|
||||
.tb-compras dialog input[type="number"] { width: 5.5em; text-align: center; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.tb-compras .cmp-oc { padding: 12px 12px 10px; }
|
||||
.tb-compras .cmp-meta { gap: 2px 12px; font-size: 12.5px; }
|
||||
/* teléfono: fuera el número de renglón y la columna de «Pedido» (la cantidad se ve en el diálogo de Recibir) */
|
||||
.tb-compras .cmp-ren th:nth-child(1), .tb-compras .cmp-ren td:nth-child(1),
|
||||
.tb-compras .cmp-ren th:nth-child(3), .tb-compras .cmp-ren td:nth-child(3) { display: none; }
|
||||
.tb-compras .cmp-ren { font-size: 13px; }
|
||||
.tb-compras .cmp-ren th, .tb-compras .cmp-ren td { padding-left: 6px; padding-right: 6px; }
|
||||
.tb-compras dialog .t th:nth-child(1), .tb-compras dialog .t td:nth-child(1) { display: none; }
|
||||
}
|
||||
264
docs/tableros/compras.js
Normal file
264
docs/tableros/compras.js
Normal file
@ -0,0 +1,264 @@
|
||||
/* Compras: las órdenes de compra del cotizador dentro del centro de mando (28-oct-2026).
|
||||
|
||||
Una pestaña por sitio (ICAVE, TIMSA, EIT, LCT, Querétaro) y «Todo». Cada OC es una tarjeta con sus renglones:
|
||||
qué se pidió, a qué ítem del inventario corresponde, qué va en camino y qué llegó. Dos acciones: «Ligar» un
|
||||
renglón que vino sin ítem y «Recibir» con lo que llegó de verdad.
|
||||
|
||||
Servidor (backend/compras.py): GET /api/compras · POST /api/compras/ligar · POST /api/compras/recibir.
|
||||
Las OC entran solas por /api/integracion/cotizador/evento, firmadas por el cotizador: esta página nunca las crea.
|
||||
Aquí NO hay montos: el dinero vive en el cotizador (D-27-4).
|
||||
Necesita tableros/kanban.js (íconos) y tableros/tablero.css; estilos propios en tableros/compras.css. */
|
||||
(function () {
|
||||
const el = (t, a, ...k) => {
|
||||
const e = document.createElement(t);
|
||||
if (a) for (const p in a) { if (a[p] == null) continue; if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); }
|
||||
k.forEach(c => c != null && e.append(typeof c === 'string' || typeof c === 'number' ? document.createTextNode(String(c)) : c));
|
||||
return e;
|
||||
};
|
||||
const $ = id => document.getElementById(id);
|
||||
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
|
||||
const fecha = iso => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso || ''); return m ? (+m[3]) + ' ' + MES[+m[2] - 1] : ''; };
|
||||
const num = n => (Math.round((+n || 0) * 100) / 100).toLocaleString('es-MX');
|
||||
const hoy = () => new Date().toISOString().slice(0, 10);
|
||||
|
||||
const ESTADOS = {
|
||||
autorizada: ['Autorizada', 'e-aut'], pagada: ['Pagada', 'e-pag'], 'en-camino': ['En camino', 'e-cam'],
|
||||
'recibida-parcial': ['Recibida en parte', 'e-par'], recibida: ['Recibida', 'e-rec'], cancelada: ['Cancelada', 'e-can']
|
||||
};
|
||||
const ABIERTAS = ['autorizada', 'pagada', 'en-camino', 'recibida-parcial'];
|
||||
const ORDEN = ['icave', 'timsa', 'eit', 'lzc', 'qro'];
|
||||
|
||||
let OC = [], ITEMS = [], SITIOS = [], USER = '', INTEG = true, vista = 'todo';
|
||||
const dlg = $('dlg'), aviso = $('aviso'), cont = $('cont'), tabs = $('cmp-tabs');
|
||||
|
||||
const corto = id => { const s = SITIOS.find(x => x.id === id); return id === 'qro' ? 'Querétaro' : (s ? s.corto : String(id || '').toUpperCase()); };
|
||||
|
||||
function mensaje(t, bien) {
|
||||
aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.textContent = t;
|
||||
clearTimeout(aviso._t); aviso._t = setTimeout(() => (aviso.className = 'msg'), bien ? 5000 : 10000);
|
||||
}
|
||||
async function api(url, opts) {
|
||||
const r = await fetch(url, Object.assign({ cache: 'no-store' }, opts || {}));
|
||||
let j = null; try { j = await r.json(); } catch (e) { /* sin JSON */ }
|
||||
if (!r.ok || !j || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status));
|
||||
return j;
|
||||
}
|
||||
const json = (m, b) => ({ method: m, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b) });
|
||||
function campo(l, i, ayuda) { const d = el('div'); d.append(el('label', { text: l }), i); if (ayuda) d.append(el('div', { class: 'ayuda', text: ayuda })); return d; }
|
||||
|
||||
Object.entries(ESTADOS).forEach(([k, v]) => $('fEstado').append(el('option', { value: k, text: v[0] })));
|
||||
|
||||
// ---------------------------------------------------------------- filtros y pestañas
|
||||
const faltaRecibir = o => ABIERTAS.includes(o.estado);
|
||||
|
||||
function visibles() {
|
||||
const e = $('fEstado').value, q = $('fBuscar').value.trim().toLowerCase(), abiertas = $('fAbiertas').checked;
|
||||
return OC.filter(o => (vista === 'todo' || o.sitio_portal === vista)
|
||||
&& (!e || o.estado === e) && (!abiertas || faltaRecibir(o))
|
||||
&& (!q || (o.folio + ' ' + (o.proveedor || '') + ' ' + (o.req_folio || '') + ' ' + (o.guia || '') + ' '
|
||||
+ (o.renglones || []).map(r => r.descripcion + ' ' + (r.item_codigo || '')).join(' ')).toLowerCase().includes(q)));
|
||||
}
|
||||
|
||||
function pintarTabs() {
|
||||
tabs.innerHTML = '';
|
||||
const b = (id, txt, n) => {
|
||||
const x = el('button', { type: 'button', 'aria-pressed': String(vista === id) }, txt);
|
||||
if (n) x.append(el('span', { class: 'n', text: ' ' + n }));
|
||||
x.onclick = () => { vista = id; guardarURL(); pintar(); };
|
||||
tabs.append(x);
|
||||
};
|
||||
b('todo', 'Todo', OC.filter(faltaRecibir).length);
|
||||
ORDEN.filter(id => SITIOS.some(s => s.id === id)).forEach(id =>
|
||||
b(id, corto(id), OC.filter(o => o.sitio_portal === id && faltaRecibir(o)).length));
|
||||
}
|
||||
|
||||
function guardarURL() {
|
||||
const u = new URL(location.href);
|
||||
if (vista === 'todo') u.searchParams.delete('sitio'); else u.searchParams.set('sitio', vista);
|
||||
history.replaceState(null, '', u);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- pintar
|
||||
function tarjeta(o) {
|
||||
const [etq, cls] = ESTADOS[o.estado] || [o.estado, ''];
|
||||
const c = el('article', { class: 'cmp-oc' + (o.estado === 'cancelada' ? ' cancelada' : '') });
|
||||
const cab = el('header', { class: 'cmp-cab' });
|
||||
cab.append(el('h2', {}, o.folio), el('span', { class: 'pill ' + cls, text: etq }));
|
||||
if (o.sitio_portal) cab.append(el('span', { class: 'chip', text: corto(o.sitio_portal) }));
|
||||
const venc = o.fecha_entrega && o.fecha_entrega < hoy() && faltaRecibir(o);
|
||||
if (o.fecha_entrega) cab.append(el('span', { class: 'chip' + (venc ? ' tarde' : ''), text: (venc ? 'debió llegar ' : 'llega ') + fecha(o.fecha_entrega) }));
|
||||
c.append(cab);
|
||||
|
||||
const meta = el('div', { class: 'cmp-meta' });
|
||||
const d = (k, v) => { if (v) meta.append(el('span', {}, el('b', { text: k + ': ' }), String(v))); };
|
||||
d('Proveedor', o.proveedor); d('Requisición', o.req_folio); d('Pidió', o.solicitante);
|
||||
d('Paquetería', [o.paqueteria, o.guia].filter(Boolean).join(' '));
|
||||
if (o.tarea_id) meta.append(el('a', { href: '/tableros/pendientes/?buscar=' + encodeURIComponent(o.folio), text: 'ver su pendiente' }));
|
||||
c.append(meta);
|
||||
|
||||
const t = el('table', { class: 't cmp-ren' });
|
||||
t.append(el('thead', {}, el('tr', {}, el('th', { text: '#' }), el('th', { text: 'Material' }),
|
||||
el('th', { class: 'num', text: 'Pedido' }), el('th', { text: 'Ítem del inventario' }), el('th', { class: 'num', text: 'Llegó' }))));
|
||||
const tb = el('tbody');
|
||||
(o.renglones || []).forEach(r => {
|
||||
const tr = el('tr', { class: r.recibido_en ? 'recibido' : '' });
|
||||
tr.append(el('td', { class: 'num', text: r.n }));
|
||||
const td = el('td', {}, el('span', { class: 'desc', text: r.descripcion || '—' }));
|
||||
if (r.nota) td.append(el('div', { class: 'mini aviso-t', text: r.nota }));
|
||||
tr.append(td);
|
||||
tr.append(el('td', { class: 'num', text: num(r.cantidad) + ' ' + (r.unidad || '') }));
|
||||
const tdI = el('td');
|
||||
if (r.item_codigo) {
|
||||
tdI.append(el('code', { text: r.item_codigo }));
|
||||
if (r.envio_id && !r.recibido_en) tdI.append(el('span', { class: 'mini', text: ' en camino' }));
|
||||
} else if (r.recibido_en) {
|
||||
tdI.append(el('span', { class: 'mini', text: 'sin ítem (no se cuenta)' }));
|
||||
} else {
|
||||
const b = el('button', { class: 'sec cmp-ligar', type: 'button' }, 'Ligar…');
|
||||
b.onclick = () => ligar(o, r);
|
||||
tdI.append(b);
|
||||
}
|
||||
tr.append(tdI);
|
||||
tr.append(el('td', { class: 'num' }, r.recibido_en ? num(r.recibido) : el('span', { class: 'mini', text: '—' })));
|
||||
tb.append(tr);
|
||||
});
|
||||
t.append(tb);
|
||||
c.append(t);
|
||||
|
||||
if (faltaRecibir(o)) {
|
||||
const acc = el('div', { class: 'cmp-acc' });
|
||||
const b = el('button', { type: 'button' }, 'Recibir…');
|
||||
b.onclick = () => recibir(o);
|
||||
acc.append(b);
|
||||
const falta = (o.renglones || []).filter(r => !r.recibido_en).length;
|
||||
if (falta) acc.append(el('span', { class: 'mini', text: falta === 1 ? 'falta 1 renglón por recibir' : 'faltan ' + falta + ' renglones por recibir' }));
|
||||
c.append(acc);
|
||||
} else if (o.recepcion_en) {
|
||||
c.append(el('div', { class: 'cmp-acc mini', text: 'Recibida por ' + (o.recepcion_por || '—') + ' el ' + fecha(o.recepcion_en) }));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
function pintar() {
|
||||
pintarTabs();
|
||||
cont.innerHTML = '';
|
||||
const v = visibles();
|
||||
$('cnt').textContent = v.length + (v.length === 1 ? ' orden' : ' órdenes');
|
||||
if (!INTEG) {
|
||||
cont.append(el('div', { class: 'info cmp-apagado' },
|
||||
el('b', { text: 'La conexión con el cotizador todavía no está encendida. ' }),
|
||||
'Falta el secreto compartido en el servidor (COTIZADOR_SECRETO) y en el cotizador (PORTAL_SECRETO). ',
|
||||
'Mientras tanto no van a entrar órdenes nuevas.'));
|
||||
}
|
||||
if (!v.length) {
|
||||
cont.append(el('div', { class: 'tb-vacio' },
|
||||
el('p', { text: OC.length ? 'Ninguna orden con estos filtros.' : 'Todavía no llega ninguna orden de compra del cotizador.' }),
|
||||
el('p', { class: 'mini', text: 'Las órdenes las levanta el cotizador: aquí solo se reciben.' })));
|
||||
return;
|
||||
}
|
||||
v.sort((a, b) => (faltaRecibir(b) - faltaRecibir(a)) || String(a.fecha_entrega || '9999').localeCompare(String(b.fecha_entrega || '9999')) || a.folio.localeCompare(b.folio));
|
||||
v.forEach(o => cont.append(tarjeta(o)));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- ligar
|
||||
function ligar(o, r) {
|
||||
dlg.innerHTML = '';
|
||||
const f = el('form', { method: 'dialog', class: 'form' });
|
||||
f.append(el('h2', {}, 'Ligar el renglón ' + r.n + ' de ' + o.folio));
|
||||
f.append(el('p', { class: 'mini', text: '«' + (r.descripcion || '') + '» — ' + num(r.cantidad) + ' ' + (r.unidad || '') }));
|
||||
const sel = el('select', { id: 'lgItem', required: 'required' });
|
||||
sel.append(el('option', { value: '', text: '— elige el ítem del inventario —' }));
|
||||
ITEMS.forEach(i => sel.append(el('option', { value: i.codigo, text: i.codigo + ' · ' + i.descripcion + ' (' + i.unidad + ')' })));
|
||||
const sug = ITEMS.find(i => (i.descripcion || '').toLowerCase() === String(r.descripcion || '').toLowerCase());
|
||||
if (sug) sel.value = sug.codigo;
|
||||
f.append(campo('Ítem', sel, 'Al ligarlo, si la orden ya va en camino, el material aparece en camino al sitio en ese momento.'));
|
||||
const err = el('div', { class: 'msg' });
|
||||
f.append(err);
|
||||
const acc = el('div', { class: 'form-acc' });
|
||||
const cancelar = el('button', { type: 'button', class: 'sec' }, 'Cancelar');
|
||||
cancelar.onclick = () => dlg.close();
|
||||
acc.append(cancelar, el('button', { type: 'submit' }, 'Ligar'));
|
||||
f.append(acc);
|
||||
f.onsubmit = async ev => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
await api('/api/compras/ligar', json('POST', { oc: o.folio, n: r.n, item: sel.value }));
|
||||
dlg.close(); mensaje('Renglón ' + r.n + ' de ' + o.folio + ' ligado a ' + sel.value + '.', true); cargar();
|
||||
} catch (e) { err.className = 'msg err'; err.textContent = e.message; }
|
||||
};
|
||||
dlg.append(f); dlg.showModal();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- recibir
|
||||
function recibir(o) {
|
||||
dlg.innerHTML = '';
|
||||
const f = el('form', { method: 'dialog', class: 'form' });
|
||||
f.append(el('h2', {}, 'Recibir ' + o.folio + ' en ' + corto(o.sitio_portal)));
|
||||
f.append(el('p', { class: 'mini', text: 'Anota lo que llegó DE VERDAD. Si llegó menos, déjalo en lo que llegó: la orden queda recibida en parte y su pendiente sigue abierto.' }));
|
||||
const t = el('table', { class: 't' });
|
||||
t.append(el('thead', {}, el('tr', {}, el('th', { text: '#' }), el('th', { text: 'Material' }),
|
||||
el('th', { class: 'num', text: 'Pedido' }), el('th', { class: 'num', text: 'Llegó' }))));
|
||||
const tb = el('tbody');
|
||||
const inputs = [];
|
||||
(o.renglones || []).forEach(r => {
|
||||
const tr = el('tr');
|
||||
tr.append(el('td', { class: 'num', text: r.n }), el('td', { text: r.descripcion || '—' }),
|
||||
el('td', { class: 'num', text: num(r.cantidad) + ' ' + (r.unidad || '') }));
|
||||
if (r.recibido_en) {
|
||||
tr.append(el('td', { class: 'num mini', text: 'ya: ' + num(r.recibido) }));
|
||||
} else {
|
||||
const i = el('input', { type: 'number', min: '0', step: '1', max: String(r.cantidad), value: String(r.cantidad), 'data-n': r.n });
|
||||
inputs.push(i);
|
||||
tr.append(el('td', { class: 'num' }, i));
|
||||
}
|
||||
tb.append(tr);
|
||||
});
|
||||
t.append(tb); f.append(t);
|
||||
if (!inputs.length) f.append(el('p', { class: 'mini', text: 'Todos los renglones ya se recibieron.' }));
|
||||
const err = el('div', { class: 'msg' });
|
||||
f.append(err);
|
||||
const acc = el('div', { class: 'form-acc' });
|
||||
const cancelar = el('button', { type: 'button', class: 'sec' }, 'Cancelar');
|
||||
cancelar.onclick = () => dlg.close();
|
||||
acc.append(cancelar);
|
||||
if (inputs.length) acc.append(el('button', { type: 'submit' }, 'Recibir'));
|
||||
f.append(acc);
|
||||
f.onsubmit = async ev => {
|
||||
ev.preventDefault();
|
||||
const renglones = inputs.map(i => ({ n: +i.dataset.n, recibido: +i.value }));
|
||||
try {
|
||||
const j = await api('/api/compras/recibir', json('POST', { oc: o.folio, renglones }));
|
||||
dlg.close();
|
||||
const e = (j.orden || {}).estado;
|
||||
mensaje(e === 'recibida' ? o.folio + ' recibida completa: su pendiente se cerró.'
|
||||
: o.folio + ' recibida en parte: su pendiente sigue abierto con lo que falta.', true);
|
||||
if ((j.avisos || []).length) mensaje(j.avisos.join(' '), false);
|
||||
cargar();
|
||||
} catch (e) { err.className = 'msg err'; err.textContent = e.message; }
|
||||
};
|
||||
dlg.append(f); dlg.showModal();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- cargar
|
||||
async function cargar() {
|
||||
try {
|
||||
const j = await api('/api/compras');
|
||||
OC = j.ordenes || []; ITEMS = j.items || []; SITIOS = j.sitios || []; USER = j.user || '';
|
||||
INTEG = j.integracion !== false;
|
||||
const abiertas = OC.filter(faltaRecibir).length;
|
||||
$('meta').textContent = OC.length + ' órdenes · ' + abiertas + ' por recibir'
|
||||
+ (USER ? ' · ' + USER : '');
|
||||
pintar();
|
||||
} catch (e) {
|
||||
$('meta').textContent = 'no se pudo cargar';
|
||||
mensaje('No se pudo cargar Compras: ' + e.message, false);
|
||||
}
|
||||
}
|
||||
|
||||
const u = new URL(location.href);
|
||||
const s = (u.searchParams.get('sitio') || '').toLowerCase();
|
||||
if (s) vista = { lct: 'lzc', ver: 'icave', zlo: 'timsa', ese: 'eit', veracruz: 'icave', manzanillo: 'timsa', ensenada: 'eit', queretaro: 'qro' }[s] || s;
|
||||
['fEstado', 'fBuscar', 'fAbiertas'].forEach(id => { $(id).oninput = pintar; $(id).onchange = pintar; });
|
||||
dlg.addEventListener('click', ev => { if (ev.target === dlg) dlg.close(); });
|
||||
cargar();
|
||||
})();
|
||||
44
docs/tableros/compras.md
Normal file
44
docs/tableros/compras.md
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
title: "Compras"
|
||||
description: "Las órdenes de compra del cotizador: qué viene en camino a cada sitio, qué llegó y qué falta por recibir."
|
||||
template: tablero.html
|
||||
hide:
|
||||
- toc
|
||||
---
|
||||
|
||||
<link rel="stylesheet" href="/tableros/compras.css">
|
||||
|
||||
<div class="tb tb-compras tb-inv">
|
||||
<header class="tb-cab">
|
||||
<div class="tb-cab__t"><h1>Compras</h1><span class="meta" id="meta">Cargando…</span></div>
|
||||
<div class="tb-cab__acc"><a class="boton sec" href="/tableros/inventario/"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7.5 12 4l8 3.5v9L12 20l-8-3.5zM4 7.5 12 11l8-3.5M12 11v9"/></svg>Inventario</a><a class="boton sec" href="/inventario/integracion-cotizador/"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 5.5h9l5 5V18.5H5zM14 5.5v5h5"/></svg>Cómo funciona</a></div>
|
||||
</header>
|
||||
|
||||
<details class="info tb-como">
|
||||
<summary>Cómo se usa</summary>
|
||||
<ol>
|
||||
<li>Las órdenes de compra <b>las hace el cotizador</b>, no esta página: aquí llegan solas cuando compras las autoriza. El dinero, la autorización y el pago <b>se quedan en el cotizador</b>; aquí nunca se ven montos.</li>
|
||||
<li>Cuando el proveedor manda el material, la OC pasa a <b>En camino</b> y cada renglón aparece <b>en camino</b> en la pestaña de su sitio en <a href="/tableros/inventario/">Inventario</a>.</li>
|
||||
<li>Cuando llega, <b>el responsable del sitio le da «Recibir»</b> con lo que llegó <b>de verdad</b> (puede ser menos de lo pedido). Eso da de alta el material en el inventario de ese sitio, cierra el pendiente de la OC si llegó todo, y el cotizador cierra la requisición solo.</li>
|
||||
<li>Un renglón <b>«por ligar»</b> es material que el cotizador mandó sin decir a qué ítem del inventario corresponde. Lígalo aquí y se va en camino en ese momento. Si es algo que no se cuenta en el inventario, recíbelo igual: se anota sin tocar existencias.</li>
|
||||
<li>Cada OC abre <b>un pendiente</b> en su sitio, con el responsable del sitio y la fecha de entrega. No lo cierres a mano: se cierra cuando se recibe todo.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<div class="msg" id="aviso"></div>
|
||||
|
||||
<nav class="inv-tabs" id="cmp-tabs" aria-label="Sitio"></nav>
|
||||
|
||||
<div class="acciones">
|
||||
<select id="fEstado"><option value="">Todos los estados</option></select>
|
||||
<input type="search" id="fBuscar" placeholder="Buscar folio, proveedor o material…" style="flex:1;min-width:150px">
|
||||
<label><input type="checkbox" id="fAbiertas" style="width:auto" checked> solo las que faltan por recibir</label>
|
||||
<span class="cnt" id="cnt"></span>
|
||||
</div>
|
||||
|
||||
<div id="cont"></div>
|
||||
|
||||
<dialog id="dlg"></dialog>
|
||||
</div>
|
||||
|
||||
<script src="/tableros/compras.js"></script>
|
||||
@ -37,6 +37,7 @@ hide:
|
||||
<h2>Últimas decisiones</h2>
|
||||
<p class="sub">Lo decidido manda hasta que otra decisión con fecha posterior lo cambie. Fuente: notas del proyecto; detalle en la sección de hardware.</p>
|
||||
<ul class="decisiones">
|
||||
<li><b>28-oct (10:17)</b> — <b>D-28-1 a D-28-5: el flujo de compras queda como se acordó en la junta de dirección.</b> La requisición la levanta el responsable del sitio y <b>Adriana autoriza renglón por renglón</b>; Tania cotiza con Efraín y Leo; <b>Adriana da un segundo visto bueno</b> (material, cantidades y lugar de entrega) y hasta entonces Tania paga. Esto <b>corrige D-27-4</b>, que dejaba la autorización en compras. Arranca el <b>29-oct</b>, solo para instalaciones de HBOX (mantenimiento se ve en enero); <b>Alán entra en lugar de Paola</b> como autorizado para pedir; el material se recibe <b>desde el celular, ítem por ítem</b>, y eso lo mete al inventario del sitio; el <b>conteo físico es mensual</b>. Del lado del portal ya está la fase 1: <code>backend/compras.py</code> y la página <a href="/tableros/compras/">Compras</a>, con 58 pruebas. <a href="/inventario/flujo-compras-28oct/">El flujo</a> · <a href="/inventario/flujo-compras-28oct/#decisiones">Decisiones</a></li>
|
||||
<li><b>27-oct (12:20)</b> — <b>D-27-1 y D-27-2 (Leo): las órdenes de compra se reciben en el sitio y la base es SQLite.</b> El cotizador se queda con la autorización, los montos y los pagos; el centro de mando recibe cada OC de DROGHEDA sin montos, con su sitio de entrega, la pone <b>en camino</b> en la pestaña del sitio y el responsable del sitio da <b>Recibir</b>; la requisición se cierra sola. Propuestas que confirma Leo: <b>D-27-3</b> códigos VER · ZLO · ESE · LZC · QRO (aeropuerto y puerto), <b>D-27-4</b> sin montos en el sitio, <b>D-27-5</b> solo PRY-2026-0002-DRO. <a href="/inventario/integracion-cotizador/#decisiones">Integración del cotizador</a> · <a href="/inventario/integracion-cotizador-instrucciones/">Instrucciones para el cotizador</a></li>
|
||||
<li><b>27-oct (11:30)</b> — <b>Entrada al sitio con página propia</b> (Claude, por el reporte de Adriana del 26-oct). La ventana gris de HTTP Basic se va: nginx pregunta al backend por la sesión (cookie firmada de 30 días), se entra con usuario o correo, y cambiar o restablecer la contraseña ya no deja a nadie fuera. Con ella llegan las <a href="/tableros/incidencias/">incidencias del sitio</a>, el <a href="/tableros/standup/">standup</a> y los <a href="/agentes/">agentes</a> (vigía cada hora, agente diario). Se publica con <code>./scripts/despliegue-1027.sh</code>; se regresa con <code>cambiar-entrada.sh deshacer</code>.</li>
|
||||
<li><b>23-oct (08:53)</b> — <b>D-22-9 decidido: (b) en las tres tarjetas, también en el piloto</b> (<b>Leo</b>: «yes D-22-9»; para la v6 lo eligió el 22-oct en la noche). Designadores de la serigrafía a <b>0.8 mm con trazo de 0.15 mm</b>, colocados por script (T44) junto a su pieza, sin mover piezas ni tocar cobre. V5.6 239 de 239 y V5.7 244 de 244 (<code>93d13e3</code>); Rev6/Rev7 regenerados: solo cambia la serigrafía superior, y BOM, CPL y cobre quedan iguales. v6 338 de 350 (<code>bd27501</code>); los 12 que no caben quedan para Michelle (mover una pieza, u ocultarlo y usar el plano de ensamble). La regla del <code>.kicad_dru</code> ya pide 0.8 / 0.15 en las tres. <a href="/hardware/v6-t44-serigrafia/">T44</a> · <a href="/hardware/designadores-t43-22oct/#5-d-22-9-tamano-de-los-designadores-en-la-serigrafia-decide-leo">D-22-9</a></li>
|
||||
|
||||
@ -89,4 +89,5 @@ El responsable principal no es jefe de los demás: es quien responde por el siti
|
||||
|---|---|---|---|
|
||||
| 22-oct | Arranque del modo crisis | `2026-10-22_crisis` | Semáforo rojo; tablero por sitio; meta 1–2 unidades por sitio |
|
||||
| 22-oct, 14:00 | Junta de instalación urgente (13 invitados, 1 h 47 min) | `2026-10-22_junta-instalacion` | Responsables de campo por sitio ([arriba](#responsables)); el piloto V5.6 + V5.7 se pide al terminar los quick wins; standup diario; LCT se trata aparte (falta la herrería de los soportes para las RTG); el retranslator de Wialon, con Gibran y Jacobo. Gemini la grabó en inglés: Leo confirmó los acuerdos después. [Notas en español](https://docs.google.com/document/d/1YZ5dw7Lru0s8HoCK44LbsGkDF0X5PFuk4LtmgAzkcGg/edit) |
|
||||
| 28-oct, 10:17 | Junta de dirección (1 h 35 min) | `2026-10-28_junta-direccion` | **El flujo de compras** ([D-28-1 a D-28-5](/inventario/flujo-compras-28oct/#decisiones)): Adriana autoriza renglón por renglón, Tania cotiza, Adriana da el visto bueno y hasta entonces se paga; **las requisiciones de HBOX arrancan el 29-oct** por el sistema; Alán entra en lugar de Paola; el conteo de inventario es mensual. Reglas: toda junta del proyecto **invita a Leo y prende Gemini**; Wialon no se mete directo en el monitor de EIT, TIMSA ni LCT (costo); los correos a Abraham, máximo un párrafo, y las RTG se piden por correo **siempre** y solo por Adriana; se prohíbe explicar fallas con la palabra «vibración» (es un módulo mal asentado). El agente aduanal es **Thera Mercurio**, no Grupo Lorenzo |
|
||||
| 27-oct, 21:08 | Decisión de Leo (sin junta) | `2026-10-27_proceso-leo` | D-27-6 ([arriba](#proceso)): backlog por responsable de área; standup escrito antes de las 10:00 + llamada de 10 min; «hecho» con evidencia. El mismo día, `2026-10-27_backlog-equipo` dio pendientes a producción, Tania y a quien tenía uno o ninguno |
|
||||
|
||||
39
mkdocs.yml
39
mkdocs.yml
@ -95,6 +95,7 @@ nav:
|
||||
- Producción: tableros/produccion.md
|
||||
- Actas: tableros/actas.md
|
||||
- Inventario: tableros/inventario.md
|
||||
- Compras: tableros/compras.md
|
||||
- Instalaciones: tableros/instalaciones.md
|
||||
- Sitios: tableros/sitio.md
|
||||
- Equipo: tableros/equipo.md
|
||||
@ -129,6 +130,7 @@ nav:
|
||||
- Prueba final: procedimientos/v5-prueba-final-unidad.md
|
||||
- Configuración: procedimientos/v5-configuracion-unidad.md
|
||||
- Política de inventario: inventario/index.md
|
||||
- Flujo de compras (28-oct): inventario/flujo-compras-28oct.md
|
||||
- Compras y cotizador: inventario/integracion-cotizador.md
|
||||
- Instrucciones al cotizador: inventario/integracion-cotizador-instrucciones.md
|
||||
- Taller:
|
||||
@ -241,6 +243,43 @@ nav:
|
||||
- BOM V3: montaje/bom-split.md
|
||||
- Ensamble y QC V3: montaje/guia-ensamble-qc.md
|
||||
- Gabinete 0007-1000: montaje/assembly-guide.md
|
||||
- Diseño de tarjetas (antecedentes):
|
||||
- "6 jul · EinarBoard 1.0: auditoría pre-fabricación": historia/einarboard-10-prefab-audit.md
|
||||
- 8 jul · Alternativas de módem 4G: historia/4g-modem-alternativas.md
|
||||
- "24 jul · EinarBoard 1.1: brief de diseño": historia/einarboard-11-brief.md
|
||||
- "12 ago · EinarBoard 3.0: MCU a bordo": historia/einarboard-30-mcu.md
|
||||
- "14 ago · EinarBoard 3.0: mapa de capacitores": historia/einarboard-30-capacitores.md
|
||||
- "20 ago · V5: revisiones previas a la auditoría": historia/v5-pre-auditoria.md
|
||||
- "20 ago · V5: correcciones aplicadas": historia/v5-correcciones.md
|
||||
- "22 ago · V5: auditoría de EMC": historia/v5-auditoria-emc.md
|
||||
- "24 ago · v6: GNSS y 4G a bordo": historia/v6-gnss-4g-brief.md
|
||||
- "24 ago · v6 fase 1: NTRIP en banco": historia/v6-fase1-ntrip.md
|
||||
- "24 ago · v6 fase 2: delta del esquemático": historia/v6-fase2-esquematico.md
|
||||
- 27 ago · KiCad como submódulo: historia/kicad-submodulo.md
|
||||
- "28 sep · v6: brief de la auditoría completa": historia/v6-auditoria-completa-brief.md
|
||||
- 12 oct · V5.7 ECO fuente a bordo (v0.1): historia/v57-eco-fuente-v01.md
|
||||
- Energía, batería y firmware (antecedentes):
|
||||
- 9 sep · Respaldo en bajo consumo: historia/respaldo-bajo-consumo.md
|
||||
- 9 sep · Química de batería a alta temperatura: historia/bateria-alta-temperatura.md
|
||||
- 9 sep · Fuente de 5 V a bordo contra Mean Well: historia/fuente-5v-costo.md
|
||||
- "10 sep · Respaldo: procedimiento de evaluación": historia/respaldo-procedimiento-evaluacion.md
|
||||
- "10 sep · Consulta a Vectro: PSM y eDRX": historia/consulta-vectro-psm.md
|
||||
- "8 ago · Firmware 3.1: Modo B y $PHLG": historia/fw-31-modob-phlg.md
|
||||
- Pruebas y análisis (antecedentes):
|
||||
- 20 jun · Análisis de bitácoras de banco: historia/analisis-bitacoras-banco.md
|
||||
- "20 jun · GNSS: resumen de hallazgos": historia/gnss-hallazgos.md
|
||||
- "17 sep · HALT: temperatura, vibración e impacto": historia/halt-rig-impacto.md
|
||||
- "17 sep · HALT: impacto (v1.0)": historia/halt-rig-impacto-v10.md
|
||||
- "17 sep · HALT: lista de compras": historia/halt-rig-compras.md
|
||||
- "24 sep · FW 0.5.2: prueba de batería en banco": historia/fw-052-prueba-bateria.md
|
||||
- "26 sep · V4/V5: auditoría de pinmap (FW 0.5.4)": historia/pinmap-auditoria-054.md
|
||||
- "28 sep · H0008: carga de una noche": historia/h0008-carga-nocturna.md
|
||||
- "6 oct · Viajes en Wialon: RTK contra CalAmp": historia/wialon-viajes-hallazgos.md
|
||||
- "18 sep · htracker: revisión de test500unidades": historia/htracker-revision-500.md
|
||||
- Comunicación con el cliente:
|
||||
- "20 jun · ICAVE: confiabilidad del backend": historia/icave-confiabilidad-backend.md
|
||||
- "8 jul · ICAVE: respuesta a observaciones (borrador)": historia/icave-respuesta-observaciones.md
|
||||
- "6 oct · ICAVE: nota de viajes en Wialon": historia/icave-nota-viajes-wialon.md
|
||||
- Tarjetas superadas:
|
||||
- 16 oct · Pasada 3: hardware/v56-v57-layout-pasada-3.md
|
||||
- 14 oct · Ruteo V5.7: hardware/v57-colocacion-ruteo.md
|
||||
|
||||
@ -9,6 +9,9 @@ REMOTE="${1:-leo@git.helgen.online}"
|
||||
REMOTE_PATH="${2:-/srv/projects/drogheda/hbox-docs/site}"
|
||||
PORT="${SSH_PORT:-22}"
|
||||
|
||||
# (28-oct) Antes de publicar: que ningún pendiente ni documento mande a algo que no existe
|
||||
# («si no está en el sitio, no pasó»). Si algo está roto, no se publica.
|
||||
python3 scripts/revisar-enlaces.py
|
||||
mkdocs build --strict
|
||||
rsync -az --delete -e "ssh -p $PORT" site/ "$REMOTE:$REMOTE_PATH/"
|
||||
echo "Sitio publicado en $REMOTE:$REMOTE_PATH (puerto $PORT)"
|
||||
@ -19,7 +22,7 @@ BACKEND_PATH="$(dirname "$REMOTE_PATH")/backend"
|
||||
rsync -az -e "ssh -p $PORT" backend/app.py backend/requirements.txt backend/seed_1013.py \
|
||||
backend/parche-nginx-api.py backend/actas-hbox.service backend/tablero.py backend/instalaciones.py \
|
||||
backend/cuentas.py backend/acceso.py backend/incidencias.py backend/standup.py backend/vigia.py \
|
||||
backend/semilla_1027.py "$REMOTE:$BACKEND_PATH/"
|
||||
backend/semilla_1027.py backend/compras.py "$REMOTE:$BACKEND_PATH/"
|
||||
# Página de entrada (27-oct): el nginx nuevo y el script que lo instala con respaldo (backend/nginx/cambiar-entrada.sh).
|
||||
rsync -az -e "ssh -p $PORT" backend/nginx/ "$REMOTE:$BACKEND_PATH/nginx/"
|
||||
# Archivos de altas para cuentas.py (23-oct): sin contraseñas; la contraseña la pide el script en la terminal.
|
||||
|
||||
124
scripts/revisar-enlaces.py
Executable file
124
scripts/revisar-enlaces.py
Executable file
@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Revisa que el sitio cumpla «si no está en el sitio, no pasó».
|
||||
|
||||
Cuatro preguntas, todas las veces:
|
||||
1. ¿Cada enlace de cada pendiente abre algo? (si no, alguien se queda esperando un documento)
|
||||
2. ¿Cada enlace DENTRO de los documentos publicados abre algo?
|
||||
3. ¿Hay documentos fechados en las carpetas NN- que nunca se publicaron?
|
||||
4. ¿Hay destinos del MAP cuyo origen ya no existe, o publicados fuera del nav?
|
||||
|
||||
Uso: python3 scripts/revisar-enlaces.py [ruta-a-hbox-docs]
|
||||
Sale con 1 si algo está roto, para poder colgarlo del despliegue o del agente diario.
|
||||
Las rutas /api/… son del backend, no archivos: no cuentan como rotas.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
REPO = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(__file__), ".."))
|
||||
PADRE = os.path.dirname(REPO)
|
||||
DOCS = os.path.join(REPO, "docs")
|
||||
SITE = os.path.join(REPO, "site")
|
||||
LINK = re.compile(r"\[([^\]]{0,120})\]\(\s*<?([^)\s>]+)>?(?:\s+\"[^\"]*\")?\s*\)")
|
||||
EXTERNO = ("http://", "https://", "mailto:", "tel:", "#", "data:")
|
||||
BACKEND = ("/api/",)
|
||||
|
||||
|
||||
def mapa():
|
||||
txt = open(os.path.join(REPO, "scripts", "sync-docs.sh"), encoding="utf-8").read()
|
||||
return [tuple(m.group(1).split("|", 1)) for m in re.finditer(r'^\s*"([^"]+\|[^"]+)"\s*$', txt, re.M)]
|
||||
|
||||
|
||||
def docs_fechados():
|
||||
out = []
|
||||
for nn in sorted(d for d in os.listdir(PADRE) if re.match(r"^\d\d-", d)):
|
||||
for raiz, dirs, files in os.walk(os.path.join(PADRE, nn)):
|
||||
dirs[:] = [d for d in dirs if d not in (".git", "_to_delete", "img", "node_modules", "kicad")]
|
||||
out += [os.path.relpath(os.path.join(raiz, fn), PADRE)
|
||||
for fn in files if fn.endswith(".md") and re.match(r"^\d{4}-\d{2}-\d{2}_", fn)]
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def resuelve(url, origen=None):
|
||||
"""True / False / None (externo o del backend)."""
|
||||
if url.startswith(BACKEND):
|
||||
return None
|
||||
p = urlparse(url)
|
||||
if p.scheme in ("http", "https"):
|
||||
if p.netloc not in ("drogheda.helgen.online", ""):
|
||||
return None
|
||||
elif p.scheme or url.startswith(EXTERNO):
|
||||
return None
|
||||
ruta = unquote(p.path or "")
|
||||
if not ruta:
|
||||
return None
|
||||
if ruta.startswith("/"):
|
||||
rel = ruta.lstrip("/")
|
||||
cand = [os.path.join(SITE, rel, "index.html"), os.path.join(SITE, rel),
|
||||
os.path.join(DOCS, rel), os.path.join(DOCS, rel.rstrip("/") + ".md"),
|
||||
os.path.join(DOCS, rel, "index.md")]
|
||||
elif origen:
|
||||
base = os.path.dirname(origen)
|
||||
cand = [os.path.join(base, ruta), os.path.join(base, ruta.rstrip("/") + ".md"),
|
||||
os.path.join(base, ruta, "index.md")]
|
||||
else:
|
||||
return None
|
||||
return any(os.path.exists(c) for c in cand)
|
||||
|
||||
|
||||
def main():
|
||||
malo = 0
|
||||
print("Revisión de enlaces del sitio —", REPO)
|
||||
|
||||
f = os.path.join(REPO, "_estado", "tablero.json")
|
||||
if os.path.exists(f):
|
||||
d = json.load(open(f, encoding="utf-8"))
|
||||
ts = d.get("tareas") or d.get("pendientes") or (d if isinstance(d, list) else [])
|
||||
rotos = [(t.get("id"), t.get("responsable"), e.get("titulo"), e.get("url"))
|
||||
for t in ts for e in (t.get("enlaces") or [])
|
||||
if (e.get("url") or "").strip() and resuelve(e["url"]) is False]
|
||||
print(f"1. enlaces de {len(ts)} pendientes: {len(rotos)} rotos")
|
||||
for tid, resp, et, url in rotos:
|
||||
print(f" ✕ {tid} ({resp}) «{et}» → {url}")
|
||||
malo += len(rotos)
|
||||
else:
|
||||
print("1. (sin _estado/tablero.json: corre ./scripts/tablero.sh foto)")
|
||||
|
||||
rotos = []
|
||||
for raiz, dirs, files in os.walk(DOCS):
|
||||
dirs[:] = [x for x in dirs if x not in ("img", "fonts", "stylesheets", "js", "descargas")]
|
||||
for fn in sorted(files):
|
||||
if not fn.endswith(".md"):
|
||||
continue
|
||||
ruta = os.path.join(raiz, fn)
|
||||
for m in LINK.finditer(open(ruta, encoding="utf-8", errors="replace").read()):
|
||||
if resuelve(m.group(2), ruta) is False:
|
||||
rotos.append((os.path.relpath(ruta, REPO), m.group(1)[:50], m.group(2)))
|
||||
print(f"2. enlaces dentro de los documentos: {len(rotos)} rotos")
|
||||
for d_, t_, u_ in rotos:
|
||||
print(f" ✕ {d_}: «{t_}» → {u_}")
|
||||
malo += len(rotos)
|
||||
|
||||
m = mapa()
|
||||
origenes = {o for o, _ in m}
|
||||
faltan = [d for d in docs_fechados() if d not in origenes]
|
||||
print(f"3. documentos fechados sin publicar: {len(faltan)}")
|
||||
for d_ in faltan:
|
||||
print(" ✕", d_)
|
||||
malo += len(faltan)
|
||||
|
||||
nav = open(os.path.join(REPO, "mkdocs.yml"), encoding="utf-8").read()
|
||||
perdidos = [o for o, _ in m if not os.path.exists(os.path.join(PADRE, o))]
|
||||
huerfanos = [d for o, d in m if d.endswith(".md") and d[len("docs/"):] not in nav]
|
||||
print(f"4. orígenes perdidos: {len(perdidos)} · publicados fuera del nav: {len(huerfanos)}")
|
||||
for x in perdidos + huerfanos:
|
||||
print(" ✕", x)
|
||||
malo += len(perdidos) + len(huerfanos)
|
||||
|
||||
print("TODO BIEN" if not malo else f"{malo} COSAS ROTAS")
|
||||
return 1 if malo else 0
|
||||
|
||||
|
||||
sys.exit(main())
|
||||
@ -265,6 +265,45 @@ declare -a MAP=(
|
||||
"02-Architecture-Software/2026-10-27_Drogheda_Integracion-Cotizador-Inventario_ES_v1.0.md|docs/inventario/integracion-cotizador.md"
|
||||
"02-Architecture-Software/2026-10-27_Drogheda_Instrucciones-Cotizador-Integracion_ES_v1.0.md|docs/inventario/integracion-cotizador-instrucciones.md"
|
||||
"02-Architecture-Software/img/2026-10-27_Drogheda_Flujo-OC-Inventario_v1.0.png|docs/inventario/img/2026-10-27_Drogheda_Flujo-OC-Inventario_v1.0.png"
|
||||
# 2026-10-28: documentos fechados que nunca se publicaron. Van a Historia como antecedentes:
|
||||
# ninguno es el documento vigente de su tema, pero el equipo tiene que poder abrirlos desde el sitio.
|
||||
"03-Architecture-Hardware/2026-07-06_Drogheda_EinarBoard-1.0-PreFab-Audit_v1.0.md|docs/historia/einarboard-10-prefab-audit.md"
|
||||
"03-Architecture-Hardware/2026-07-08_Drogheda_4G-Modem-Alternatives_v1.0.md|docs/historia/4g-modem-alternativas.md"
|
||||
"03-Architecture-Hardware/2026-07-24_Drogheda_EinarBoard-1.1_Design-Brief_v1.0.md|docs/historia/einarboard-11-brief.md"
|
||||
"03-Architecture-Hardware/2026-08-12_Drogheda_EinarBoard-3.0_MCU-Integration_Design-Brief_v1.0.md|docs/historia/einarboard-30-mcu.md"
|
||||
"03-Architecture-Hardware/2026-08-14_Drogheda_EinarBoard-3.0_Capacitor-Placement-Map_v1.0.md|docs/historia/einarboard-30-capacitores.md"
|
||||
"03-Architecture-Hardware/2026-08-20_Drogheda_EinarBoard-v5_Pre-Audit-Checks_v1.0.md|docs/historia/v5-pre-auditoria.md"
|
||||
"03-Architecture-Hardware/2026-08-20_Drogheda_EinarBoard-v5_Fixes-Applied_v1.0.md|docs/historia/v5-correcciones.md"
|
||||
"03-Architecture-Hardware/2026-08-22_Drogheda_EinarBoard-v5_EMC-Audit_v1.0.md|docs/historia/v5-auditoria-emc.md"
|
||||
"03-Architecture-Hardware/2026-08-24_Drogheda_EinarBoard-v6_GNSS-4G-Integration_Design-Brief_v1.0.md|docs/historia/v6-gnss-4g-brief.md"
|
||||
"03-Architecture-Hardware/2026-08-24_Drogheda_EinarBoard-v6_Phase-1_NTRIP-Firmware-Spec_v1.0.md|docs/historia/v6-fase1-ntrip.md"
|
||||
"03-Architecture-Hardware/2026-08-24_Drogheda_EinarBoard-v6_Phase-2_Schematic-Delta_v1.0.md|docs/historia/v6-fase2-esquematico.md"
|
||||
"03-Architecture-Hardware/2026-08-27_Drogheda_KiCad-Submodule-Migration_v1.0.md|docs/historia/kicad-submodulo.md"
|
||||
"03-Architecture-Hardware/2026-09-28_Drogheda_EinarBoard-v6_Full-Audit_Agent-Brief_v1.0.md|docs/historia/v6-auditoria-completa-brief.md"
|
||||
"03-Architecture-Hardware/2026-10-12_Drogheda_EinarBoard-V5.7_ECO-Fuente-a-bordo_ES_v0.1.md|docs/historia/v57-eco-fuente-v01.md"
|
||||
"03-Architecture-Hardware/2026-09-09_Drogheda_Backup-LowPower-DutyCycle_Design-Brief_v1.0.md|docs/historia/respaldo-bajo-consumo.md"
|
||||
"03-Architecture-Hardware/2026-09-09_Drogheda_Battery-Chemistry-HighTemp_v1.0.md|docs/historia/bateria-alta-temperatura.md"
|
||||
"03-Architecture-Hardware/2026-09-09_Drogheda_5V-Source-OnBoard-vs-MeanWell_Cost_v1.0.md|docs/historia/fuente-5v-costo.md"
|
||||
"03-Architecture-Hardware/2026-09-10_Drogheda_Backup-Power_Evaluation-Procedure_v1.0.md|docs/historia/respaldo-procedimiento-evaluacion.md"
|
||||
"03-Architecture-Hardware/2026-09-10_Drogheda_Consulta-Vectro_PSM-eDRX_v1.0.md|docs/historia/consulta-vectro-psm.md"
|
||||
"03-Architecture-Hardware/2026-08-08_Drogheda_Firmware-v3.1_ModoB-PHLG_Design-Brief_v1.0.md|docs/historia/fw-31-modob-phlg.md"
|
||||
"06-Testing-Validation/2026-06-20_Drogheda_Bench-Log-Analysis_v1.0.md|docs/historia/analisis-bitacoras-banco.md"
|
||||
"06-Testing-Validation/2026-06-20_Drogheda_GNSS-Findings-Summary.md|docs/historia/gnss-hallazgos.md"
|
||||
"06-Testing-Validation/2026-09-17_Drogheda_HALT-Rig_Thermal-Vibration-Impact_v1.1.md|docs/historia/halt-rig-impacto.md"
|
||||
"06-Testing-Validation/2026-09-17_Drogheda_HALT-Rig_Thermal-Vibration-Impact_v1.0.md|docs/historia/halt-rig-impacto-v10.md"
|
||||
"06-Testing-Validation/2026-09-17_Drogheda_HALT-Rig_Lista-Compras_ES_v1.0.md|docs/historia/halt-rig-compras.md"
|
||||
"06-Testing-Validation/2026-09-24_Drogheda_HBOX-FW-0.5.2_Prueba-Banco-Bateria_ES_v1.0.md|docs/historia/fw-052-prueba-bateria.md"
|
||||
"06-Testing-Validation/2026-09-26_Drogheda_HBOX-V4-V5_Pinmap-Audit-FW-0.5.4_v1.0.md|docs/historia/pinmap-auditoria-054.md"
|
||||
"06-Testing-Validation/2026-09-28_H0008_charging-overnight-log.md|docs/historia/h0008-carga-nocturna.md"
|
||||
"06-Testing-Validation/wialon-trips/2026-10-06_Drogheda_Wialon-Trips_RTK-vs-CalAmp_Findings_v1.0.md|docs/historia/wialon-viajes-hallazgos.md"
|
||||
"02-Architecture-Software/2026-09-18_Drogheda_htracker-Review_feature-test500unidades_v1.0.md|docs/historia/htracker-revision-500.md"
|
||||
"00-Project-Management/client-comms/2026-06-20_Actualizacion-Confiabilidad-Backend_ICAVE.md|docs/historia/icave-confiabilidad-backend.md"
|
||||
"00-Project-Management/client-comms/2026-07-08_Respuesta-Observaciones_ICAVE_Agustin_BORRADOR.md|docs/historia/icave-respuesta-observaciones.md"
|
||||
"00-Project-Management/client-comms/2026-10-06_Drogheda_Viajes-Wialon-RTK-vs-CalAmp_Nota-ICAVE_v1.0.md|docs/historia/icave-nota-viajes-wialon.md"
|
||||
# 2026-10-28: el flujo de compras e inventario como quedó en la junta de dirección (dos puertas de Adriana,
|
||||
# arranque el 29-oct solo para HBOX) y la fase 1 del portal (backend/compras.py + página Compras)
|
||||
"02-Architecture-Software/2026-10-28_Drogheda_Flujo-Compras-e-Inventario_ES_v1.0.md|docs/inventario/flujo-compras-28oct.md"
|
||||
"02-Architecture-Software/img/2026-10-28_Drogheda_Flujo-Compras-Centro-de-Mando_v1.0.png|docs/inventario/img/2026-10-28_Drogheda_Flujo-Compras-Centro-de-Mando_v1.0.png"
|
||||
)
|
||||
|
||||
n=0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user