Architecture Overview
Architecture Overview
System Diagram
┌──────────────────┐ ┌──────────────────┐
│ tentacle-plc │ │ tentacle-web │
│ (PLC Runtime) │ │ (SvelteKit UI) │
└────────┬─────────┘ └────────┬─────────┘
│ │
│ NATS Topics │ GraphQL
▼ ▼
┌────────────────────────────────────────────────┐
│ NATS Message Bus + JetStream + KV │
└────────────────────────────────────────────────┘
│ │ │ │
┌────▼────┐ ┌───▼──────┐ ┌────▼────┐ ┌───▼─────────────┐
│tentacle-│ │tentacle- │ │tentacle-│ │tentacle- │
│ethernet │ │modbus │ │mqtt │ │graphql │
│ip │ │ │ │ │ │ │
└────┬────┘ └───┬──────┘ └────┬────┘ └─────────────────┘
│ │ │
Allen-Bradley Modbus MQTT Broker
PLCs TCP (Sparkplug B)
Devices
Tech Stack
- Runtime: Deno (most backend services), Go (tentacle-ethernetip-go, tentacle-opcua-go)
- Frontend: SvelteKit 2.9, Svelte 5, TypeScript 5.6, Vite 6
- Messaging: NATS with JetStream and KV stores
- MQTT: Sparkplug B via @joyautomation/synapse
- GraphQL: graphql-yoga + Pothos schema builder
- EtherNet/IP: Go + CGo + libplctag (CIP Multi-Service Packet batching)
Data Flow
Reading from PLCs (PLC → Cloud)
Each service publishes to its own NATS namespace. The web UI subscribes per-module:
tentacle-ethernetip-gopolls PLC tags via libplctag (CIP batch reads)- Publishes to
ethernetip.data.{deviceId}.{tagName}(its own namespace) tentacle-plcsubscribes toethernetip.data.>for its source variables- PLC processes/composes values, publishes to
{projectId}.data.{variableId}(its own namespace) tentacle-mqttsubscribes to*.data.>, reads RBE settings frommqtt-config-{projectId}KV- If change exceeds deadband, publishes via Sparkplug B DDATA
tentacle-graphqlsubscribes to{moduleId}.data.>per client request, batches updates every 2.5stentacle-webreceives batched SSE updates scoped to the page's module
Writing to PLCs (Cloud → PLC)
Ignition/SCADA → DCMD → MQTT Broker → tentacle-mqtt → NATS → tentacle-ethernetip → PLC
- Sparkplug client (e.g., Ignition) sends DCMD message
tentacle-mqttreceives DCMD via synapse library- Extracts metric name/value, publishes to NATS
{projectId}/{variableId} tentacle-ethernetipreceives, encodes value, callswriteTag()to PLC
Report By Exception (RBE)
Traffic reduction via deadband filtering (80-95% reduction):
deadband: {
value: 0.5, // Only publish if change > threshold
maxTime: 60000 // Force publish at least every N ms
}
Configured per-variable, flows through entire stack:
- Defined in tentacle-plc/ethernetip
- Included in NATS messages
- Honored by tentacle-mqtt when publishing to Sparkplug B
Key Dependencies
| Package | Purpose |
|---|---|
| @nats-io/transport-deno | NATS transport |
| @nats-io/jetstream | JetStream features |
| @nats-io/kv | Key-Value store |
| @joyautomation/coral | Logging |
| @joyautomation/dark-matter | Configuration |
| @joyautomation/synapse | Sparkplug B MQTT |
| @joyautomation/tentacle-nats-schema | Shared schemas |