Stack tecnológico¶
Elecciones¶
| Capa | Elección | Estado |
|---|---|---|
| Lenguaje | TypeScript, un solo proyecto | firme (D-04) |
| Runtime | Node LTS | firme |
| Servidor Modbus | modbus-serial (ServerTCP) |
firme, verificado en fase 0 (D-17) |
| UDP | node:dgram |
firme, sin dependencia |
| WebSocket | ws |
firme |
| Registro de eventos | SQLite con better-sqlite3, modo WAL |
firme |
| Validación de configuración | JSON Schema con Ajv | firme |
| Interfaz | SPA ligera servida por el mismo proceso | firme |
| Despliegue | Docker Swarm + cloudflared |
firme (D-05) |
| Documentación | MkDocs Material | firme |
Por qué un solo proceso y un solo lenguaje¶
Núcleo, servidor Modbus, emisor UDP, servidor WebSocket e interfaz viven en el mismo proceso Node (D-01). Un salto de red entre el adaptador Modbus y el núcleo añadiría jitter que no existe en el radar real, y el banco existe para medir jitter.
El riesgo técnico principal — resuelto en fase 0¶
Múltiples unit IDs sobre una sola conexión TCP
El diseño exige que el servidor atienda diez unit IDs sobre la misma conexión TCP, porque las direcciones de los módulos ADAM colisionan entre sí y la resolución es un unit ID por módulo físico (Modbus TCP).
Prueba de concepto de la fase 0: jsmodbus no lo soporta (buffer único compartido entre
unit IDs, sin importar request.unitId). modbus-serial sí, dejando options.unitID
sin fijar y enrutando a mano por el unitID que la librería pasa a cada callback del
vector. También expone excepción Modbus ante FC05 sobre coil de solo lectura, y el
vector puede capturar el instante de recepción con process.hrtime.bigint() (resolución
de nanosegundos; el jitter del setTimeout(0) interno de la librería añade ~0,6-2,2 ms de
latencia hasta la captura, medido sin carga). Detalle completo en
D-17.
Códigos de función a implementar¶
Solo seis, por cómo Advantech usa los espacios de direcciones:
| FC | Función | Uso |
|---|---|---|
| 01 | Read Coils | DI y DO |
| 03 | Read Holding Registers | AI y AO |
| 05 | Write Single Coil | DO |
| 06 | Write Single Register | AO |
| 15 | Write Multiple Coils | DO |
| 16 | Write Multiple Registers | AO |
FC02 y FC04 no se usan: las entradas digitales viven en 0X y las analógicas en 4X. Ver
Modbus TCP.
Estructura del repositorio¶
rd100s-emu/
├── src/
│ ├── core/ # estado, tick, grafo, modos de señal
│ ├── config/ # esquema, validación, carga
│ ├── adapters/
│ │ ├── modbus/ # servidor, mapeo, conversión de crudo
│ │ ├── udp/ # emisor de encoder y degradaciones
│ │ └── ws/ # telemetría, eventos, órdenes
│ ├── log/ # SQLite, sesiones, descarga
│ ├── blocks/ # biblioteca de comportamiento (fase 2)
│ └── ui/ # SPA
├── config/ # rd100s.json y otras configuraciones
├── scenarios/ # escenarios y aserciones (fase 3)
├── docs/ # este sitio
├── stack.yml
└── mkdocs.yml
El núcleo no importa nada de adapters/. La dependencia va en un solo sentido y conviene
imponerlo con una regla de lint, no solo por convención: es lo que mantiene el modelo
independiente de los transportes.
Convenciones¶
- Reloj monótono en todo lo que se registre o se emita. Nunca hora de pared.
- Marcado
// PEND-nnen cada punto donde se use un valor provisional.