One binary, every protocol — from 6 Node.js services to 1 C binary with plugins
Today, FIWARE has 6 active IoT Agents, each a separate Node.js application, each consuming ~80–150 MB of RAM, each running its own Express HTTP server. They all depend on a shared base library (iotagent-node-lib) that handles 80% of the work: device provisioning, NGSI broker communication, configuration, and the northbound API.
| Agent | Protocol | Transport | Protocol-Specific Code |
|---|---|---|---|
| iotagent-json | JSON payloads | MQTT, HTTP, AMQP | JSON decode (trivial — it's already JSON) |
| iotagent-ul | Ultralight 2.0 | MQTT, HTTP, AMQP | Text parsing: key|value|key|value |
| iotagent-opcua | OPC UA | OPC UA TCP | OPC UA client (node-opcua library) |
| iotagent-lorawan | LoRaWAN | HTTP callbacks | CayenneLpp / CBOR decode |
| lightweightm2m | LWM2M | CoAP | CoAP server + LWM2M object model |
| sigfox-iotagent | Sigfox | HTTP callbacks | Sigfox callback parsing (12 bytes) |
A typical FIWARE IoT deployment with 3 protocols needs 3 separate Node.js processes (~300–450 MB RAM) plus the shared library duplicated in each process. They don't share device registries, don't share broker connections, and can't be managed as a single unit.
The purple boxes are protocol-specific. Everything else is shared. In practice, the protocol plugin is 5–15% of the code; the library is 85–95%.
Instead of 6 separate Node.js applications, the FiWorks Multi-Agent is a single C binary with protocol support via shared libraries (.so plugins) or compile-time flags:
# Run as JSON agent (MQTT + HTTP)
fiworks-agent --protocol json --mqtt localhost:1883 --broker localhost:1026
# Run as Ultralight agent
fiworks-agent --protocol ul --mqtt localhost:1883 --broker localhost:1026
# Run as OPC UA agent
fiworks-agent --protocol opcua --opcua-server opc.tcp://plc:4840 --broker localhost:1026
# Run as LoRaWAN agent (HTTP callback receiver)
fiworks-agent --protocol lorawan --port 4061 --broker localhost:1026
# Run as LWM2M agent (CoAP server)
fiworks-agent --protocol lwm2m --coap-port 5683 --broker localhost:1026
# Run as multi-protocol agent (why not?)
fiworks-agent --protocol json,ul,lorawan --mqtt localhost:1883 --broker localhost:1026
// FiWorks Multi-Agent core (the new "iotagent-node-lib")
typedef struct FwProtocol {
const char* name; // "json", "ul", "opcua", "lorawan", "lwm2m"
KhStatus (*init)(FwAgent* agent, FwConfig* config);
KhStatus (*decode)(FwAgent* agent, char* payload, int len, FtNode** measures);
KhStatus (*encode)(FwAgent* agent, FtNode* command, char** payload, int* len);
void (*shutdown)(FwAgent* agent);
} FwProtocol;
// The core handles everything else:
// - fwHttp: northbound API (device provisioning, config, health)
// - fwHttp client: NGSI-LD broker communication
// - fwHash: device registry (device ID -> entity mapping)
// - fwHash: device group management
// - fwJson: NGSI-LD entity building from decoded measures
// - fwAlloc: per-message bump allocation
// - fwTrace: structured logging
// - fwProm: Prometheus metrics
| iotagent-node-lib Feature | fw-lib | Status |
|---|---|---|
| Northbound REST API (provisioning, config) | fwHttp | Done |
| NGSI-LD broker communication | fwHttp client + fwJson | Done |
| JSON parsing (device payloads, API requests) | fwJson | Done |
| Device registry (in-memory + persistence) | fwHash | Done |
| Device group management | fwHash | Done |
| Memory management | fwAlloc | Done |
| Logging | fwTrace | Done |
| Metrics (Prometheus) | fwProm | Done |
| MQTT client (for JSON, UL agents) | — | To build (or use libmosquitto) |
| AMQP client | — | To build (or use librabbitmq) |
| CoAP server (for LWM2M) | — | To build (or use libcoap) |
| OPC UA client | — | To build (or use open62541) |
The core library functionality is ~80% covered by existing fw-libs. The remaining 20% is transport-specific clients (MQTT, CoAP, OPC UA) which can use mature C libraries.
| Plugin | Complexity | What It Does | External Lib |
|---|---|---|---|
| json | Trivial | Decode: fwJson parse. Encode: fwJson render. Done. |
None — fwJson IS the protocol |
| ul | Easy | Decode: split on |, pair keys with values. Encode: join with |. |
None — ~50 lines of C |
| lorawan | Easy | Receive HTTP callbacks from LoRa network server, decode CayenneLpp/CBOR payloads. | tinycbor (small, embeddable) |
| sigfox | Trivial | Receive HTTP callbacks, decode 12-byte binary payload. | None |
| opcua | Medium | OPC UA client: connect to server, subscribe to nodes, map to NGSI entities. | open62541 (C, MIT, mature) |
| lwm2m | Medium | CoAP server + LWM2M Registration/Read/Write/Observe/Execute handlers. | wakaama (C, EPL/EDL) or libcoap |
The IoT Agent hot path is: receive device message → decode → map to NGSI entity → POST/PATCH to broker. For a sensor sending a temperature reading every 10 seconds, this happens millions of times per day across a fleet.
| Phase | Node.js (iotagent-node-lib) | C (FiWorks Multi-Agent) |
|---|---|---|
| Receive MQTT message | ~0.1ms (event loop callback) | ~0.01ms (libmosquitto callback) |
| Decode payload (JSON) | ~0.05–0.5ms (JSON.parse, allocates objects) |
~0.005ms (fwJson in-place, zero alloc) |
| Device registry lookup | ~0.1–1ms (MongoDB query or in-memory cache) | ~0.001ms (fwHash O(1) lookup) |
| Attribute mapping | ~0.05–0.2ms (JS object manipulation) | ~0.005ms (FtNode tree building) |
| Build NGSI-LD entity | ~0.1–0.5ms (JSON.stringify) |
~0.01ms (fwJson render) |
| HTTP POST to broker | ~1–5ms (new HTTP connection per update) | ~0.2–1ms (pooled connection) |
| Total per message | ~2–8ms | ~0.2–1.1ms |
| Metric | 6 × Node.js Agents | 1 × FiWorks Multi-Agent | Improvement |
|---|---|---|---|
| Messages/sec (single core) | ~5,000–10,000 | ~50,000–200,000 | ~10–20× |
| RAM (3 protocols active) | ~300–450 MB (3 Node.js processes) | ~5–15 MB (1 process, 3 plugins) | ~30–60× less |
| RAM (all 6 protocols) | ~600–900 MB | ~10–25 MB | ~40–60× less |
| p99 latency (decode + forward) | ~10–30ms (GC spikes) | ~0.5–2ms | ~10–20× |
| Startup time | ~2–5s per agent | <50ms | ~50–100× |
| Broker connections | 3–6 separate connection pools | 1 shared connection pool | Fewer sockets, less broker load |
| Device registry | 3–6 separate MongoDB instances or caches | 1 shared fwHash registry | Single source of truth |
| Deployment | 6 Docker containers, 6 configs | 1 binary, 1 config | Dramatically simpler ops |
The unified design isn't just about performance — it's about operational simplicity:
--protocol json,ul,lorawanThe current FIWARE IoT Agent architecture already proves the concept of a shared core with thin protocol layers. The iotagent-node-lib base library provides 85–95% of the functionality — device provisioning, northbound API, broker communication, attribute mapping, command handling — while each protocol agent contributes only 5–15% of protocol-specific code. Running 6 separate Node.js processes that each bundle that same 85–95% is pure waste.
fwHash instance, one provisioning API. If a device switches protocol (or you realize it should be Ultralight instead of JSON), no re-provisioning is needed. The device is already known.decode function and an encode function behind a common interface.The natural question: if one protocol plugin crashes, does it take down the others? The answer depends on the protocol:
| Protocol Category | Crash Risk | Why |
|---|---|---|
| JSON, Ultralight, Sigfox | Negligible | Pure parsing code — splits strings, reads JSON. No external C libraries, no complex state. These cannot realistically crash. |
| LoRaWAN | Very low | HTTP callback receiver with CayenneLpp/CBOR decode. tinycbor is small and well-tested. |
| OPC-UA | Low–medium | Wraps open62541 — a mature C library, but any external C library could theoretically segfault under unexpected conditions. |
| LWM2M | Low–medium | Wraps wakaama or libcoap — same consideration as OPC-UA. |
For the simple text-based protocols (JSON, UL, LoRaWAN, Sigfox), there is no practical fault isolation concern — they are pure parsing code running in the process space with no external library dependencies that could cause unexpected crashes.
For the complex protocols (OPC-UA, LWM2M) that wrap external C libraries, a segfault in the library would take down the entire process. However, with restart: always and <50ms startup time, recovery is near-instant — all protocols come back online in the blink of an eye.
The --protocol flag already gives the operator full control. Want all protocols in one process? Use --protocol json,ul,lorawan,opcua. Want to isolate OPC-UA because it talks to a flaky PLC? Run two instances:
# Instance 1: simple protocols (shared)
fiworks-agent --protocol json,ul,lorawan --mqtt localhost:1883 --broker localhost:1026
# Instance 2: OPC-UA isolated
fiworks-agent --protocol opcua --opcua-server opc.tcp://plc:4840 --broker localhost:1026
Each instance is a fully independent process with its own device registry and broker connections. This is not a special "isolation mode" — it is simply running two instances of the same binary with different protocol flags. The operator decides the trade-off between operational simplicity (one instance) and fault isolation (multiple instances) based on their specific deployment needs.
| Component | Work | Estimate |
|---|---|---|
| Multi-Agent core | Northbound API (fwHttp), device registry (fwHash), NGSI-LD client (fwHttp+fwJson), device group management, attribute mapping engine, command handling | 2–3 weeks |
| MQTT transport | libmosquitto integration, topic-based device routing, QoS handling | 1 week |
| JSON plugin | Trivial: fwJson parse/render. ~100 lines. | 1–2 days |
| UL plugin | Text parsing: split on |. ~50 lines. |
1–2 days |
| HTTP transport (southbound) | fwHttp server for device HTTP push | 3–4 days |
| Configuration + CLI | Config file, --protocol flag, env var overrides |
2–3 days |
| Testing | Unit tests, integration tests with broker, MQTT broker | 1–2 weeks |
| Phase 1 total | 5–7 weeks |
| Component | Work | Estimate |
|---|---|---|
| LoRaWAN plugin | HTTP callback receiver, CayenneLpp decode, CBOR decode (tinycbor) | 1 week |
| Sigfox plugin | HTTP callback receiver, 12-byte binary decode. Trivial. | 2–3 days |
| Phase 2 total | 1.5–2 weeks |
| Component | Work | Estimate |
|---|---|---|
| OPC UA plugin | open62541 integration, node subscription, browse, monitoring, command execution | 2–3 weeks |
| LWM2M plugin | wakaama or libcoap integration, LWM2M Registration/Read/Write/Observe/Execute, object model mapping | 2–3 weeks |
| Testing + hardening | OPC UA server simulation, LWM2M device simulation, load testing | 1–2 weeks |
| Phase 3 total | 5–8 weeks |
| Phase | Deliverable | Cumulative Time |
|---|---|---|
| Phase 1 | JSON + UL over MQTT/HTTP — covers 80% of FIWARE IoT deployments | 5–7 weeks |
| Phase 2 | + LoRaWAN + Sigfox — all callback-based LPWAN protocols | 7–9 weeks |
| Phase 3 | + OPC UA + LWM2M — full protocol coverage | 12–17 weeks |
The FiWorks Multi-Agent delivers 10–20× throughput, 30–60× less RAM, and dramatically simpler operations compared to 6 separate Node.js agents. Phase 1 (5–7 weeks) covers JSON + Ultralight — the two most common protocols — and establishes the plugin architecture for easy protocol addition.
The real innovation isn't the performance — it's the unified design. One device registry, one broker connection pool, one config, one binary. Add a protocol with a CLI flag. A multi-protocol IoT gateway that runs on a Raspberry Pi with room to spare.
Development effort: 3–4 months for all 6 protocols. Phase 1 alone (5–7 weeks) replaces the two most-used agents and the entire iotagent-node-lib.