What it is
Fab equipment speaks a 1980s binary protocol (SECS-II) over TCP (HSMS). A factory's MES ("brain") doesn't care about cryptic event numbers like CEID 201 — it cares about states like RUNNING, ALARM, MAINTENANCE. FabBridgeEngine is the adapter between those two worlds.
The whole system exists to perform one transformation — SecsEvent → EquipmentStateChange — ingesting events fast, translating them, and fanning the result out to wherever it needs to go. Everything else (channel, worker, four sinks, dashboard) is plumbing around that core, and a set of well-placed interfaces lets the system grow at its edges, never its core.
Architecture
A decoupled ingestion pipeline: a bursty producer feeds a bounded in-memory channel; a steady worker drains it, translates each event, and fans the result out to independent sinks with per-sink fault isolation.
flowchart TB EQ["Factory Equipment / GEM Tool"] -->|"SECS-II over HSMS (S6F11)"| RCV["Equipment Source
(IEquipmentSource)"] RCV -->|"SecsEvent"| CH[("Bounded Channel
backpressure, FIFO")] CH -->|"await foreach"| TR["Translation Worker
CEID → MES State"] TR --> FAN{{"Fan-out
IStateChangeSink[]"}} FAN --> SQL[("SQL Server
Dapper · stored proc
audit trail")] FAN --> HUB["SignalR Hub"] FAN --> MQTT["MQTT Broker
retained per-tool"] HUB --> DASH["Blazor Live
Status Dashboard"] MQTT --> SUBS["Downstream subscribers
MES · analytics · dashboards"] classDef core fill:#0f766e,stroke:#0b5c55,color:#fff; classDef sink fill:#2563eb,stroke:#1e50c0,color:#fff; class RCV,CH,TR,FAN core; class SQL,HUB,MQTT sink;
The core translation
A raw SecsEvent carries an equipment id, a numeric CEID, and a bag of report values. The translator maps the CEID to a meaningful state — as data, not code (an injected dictionary), so a new tool is a config change, not a code change. Unknown CEIDs degrade to Unknown rather than throwing, because a 24/7 bridge must never crash on an unexpected event.
| CEID | Meaning | MES State |
|---|---|---|
101 | process started | Running |
102 | process completed | Idle |
201 | alarm set | Alarm |
202 | alarm cleared | Idle |
301 | entered maintenance | Maintenance |
302 | maintenance done | Idle |
401 | comms/heartbeat lost | Down |
| other | unrecognized | Unknown |
Translation is intentionally lossy (many CEIDs → one state), so every state change keeps its originating SourceCeid for the audit trail — you can always reconstruct why a tool became Idle.
The fidelity ladder
The equipment side was built as a ladder you climb — each rung adds one layer of real-world fidelity (transport, framing, a broker, the real protocol) while the entire downstream pipeline stays identical. Every rung is just a different IEquipmentSource, selectable by config.
In-process simulator
A fake fab tool generates a realistic, jittered lifecycle (start → run → occasional alarm → complete → maintenance) straight into the channel. Exercises the pipeline logic with zero infrastructure.
SECS-II framing
Length-prefixed binary frames — the idea that makes any TCP transport work: a boundary marker so the receiver knows where each message ends.
Real TCP transport feature/tcp-hsms
A TCP equipment server streams framed events; a client reassembles them across arbitrary packet boundaries (ReadExactly), with reconnect. Exercises sockets, fragmentation, connection lifecycle, and backpressure spanning the network.
MQTT broker feature/mqtt-pipeline
Translated states publish to an MQTT broker (retained, one topic per tool) so the dashboard is no longer the only consumer — a real distributed pipeline. Added as one more sink, zero changes to the worker.
Real SECS/GEM via secs4net feature/secs4net
Genuine HSMS with the full Select handshake, real SECS-II Item trees, and an actual S6F11 → S6F12 request/ack exchange. Standard-conformant — it would talk to a real tool. One HSMS connection = one tool.
The equipment lifecycle generator is written once and reused across rungs 1, 3, and 5 — driving an in-memory queue, a raw socket, and a real GEM link unchanged — purely because it writes to an IEventProducer seam.
Design principles
The recurring thesis, made concrete in code.
Dependencies point inward
The Core domain library depends on nothing — SQL, TCP, MQTT, and SECS/GEM are outer details plugged into Core's interfaces. That's why Core's tests run in milliseconds with no infrastructure.
Interfaces at every seam
IEquipmentSource, IEventChannel (split into producer/consumer), and IStateChangeSink let each end be swapped in isolation. The worker never changed while SQL, SignalR, TCP, MQTT, and SECS/GEM were all added around it.
Bounded channel = backpressure
A bounded queue applies backpressure instead of growing memory without limit — for a 24/7 bridge, slowing down is acceptable; losing events or an OOM crash is not.
Fault isolation & graceful degradation
Each sink publish is isolated: a transient DB blip can't stop live dashboard updates. Unknown events and dropped links are handled, never fatal. Reconnect loops distinguish shutdown from failure.
Stack & structure
Technology
- .NET 8 / C# — records, async streams,
System.Threading.Channels - Blazor Server + SignalR — live, push-based status tiles
- Dapper + SQL Server (Docker) — stored-procedure telemetry writes
- MQTT (Mosquitto) — retained per-tool state for downstream consumers
- secs4net 3.1 — real HSMS/SECS-II protocol stack
- xUnit — 28 tests incl. real loopback socket & HSMS integration
Project layout
src/ Core/ # pure domain (no deps) Persistence/ # Dapper + SQL Transport/ # TCP/HSMS framing (rung 3) Messaging/ # MQTT publish (rung 4) SecsGem/ # real secs4net (rung 5) Dashboard/ # Blazor + SignalR Host/ # console runner tests/ # Core · Transport · Messaging · SecsGem
Run any rung from config
Equipment__Mode=Simulated dotnet run --project src/Dashboard # rung 1 Equipment__Mode=Tcp dotnet run --project src/Dashboard # rung 3 Equipment__Mode=SecsGem dotnet run --project src/Dashboard # rung 5
Verification
Every layer is tested at the right altitude — pure logic in isolation, and real sockets / real HSMS where the hard bugs live.
Highlights: frame reassembly tested at one byte per read (worst-case fragmentation); a real loopback socket loop; and a genuine HSMS Select handshake exchanging S6F11/S6F12.