Networking
Project-Based Example: A Real IoT Device Architecture
Every other page in this domain explains one protocol in isolation. This page is the opposite: one real, production embedded Linux gateway device, and exactly how UDP, CoAP, an AMQP-style message bus, MQTT, and WebSocket all show up on the same device, doing genuinely different jobs. It's grounded in an actual architecture, not a hypothetical — including a correction of the one assumption almost everyone makes about it the first time.
- What it is
- An embedded Linux IoT gateway running two independent protocol adapters side by side, both publishing onto one shared local message bus, with one component relaying qualifying traffic on to the cloud.
- Mental model
- Not one pipeline — two separate front doors into the same building. Both adapters share infrastructure (the bus, the cloud uplink); neither one's code ever calls the other.
- Key numbers
- 2 independent adapters (raw UDP on port 47000, CoAP on port 5683); 1 shared local message bus, reachable only on loopback (127.0.0.1); 2 possible transports out to the cloud (MQTT or WebSocket), chosen by configuration.
- Reach for it when
- You want to see every protocol covered elsewhere in this domain working together on one real device, instead of only in isolation — and as a worked example of how easy it is to assume the wrong architecture from a diagram alone.
The device, in one sentence
The device is an embedded Linux board (an i.MX8MP-class SoC, running Ubuntu Core with each component packaged as a separately-confined snap) sitting between two populations of sensor hardware and a cloud backend — its whole job is ingesting two structurally different kinds of device traffic, getting both onto one shared internal bus, and relaying the traffic that matters onward.
- Protocol adapters / applications — the only layer that actually produces or forwards data; everything below it exists to support this layer, not to do its job for it.
- Core messaging infrastructure — the shared local bus every adapter publishes onto, plus the one component with a door out to the cloud.
- Connectivity — whatever gets the Linux network stack onto a working link at all (LTE, Ethernet) — a lower-level OS concern the layers above don't need to know about.
- Hardware & boot — partitions, bootloader, device tree — the base everything else runs on top of.
The rest of this page works top to bottom through that stack, then end to end through two real device-to-cloud journeys.
Two independent protocol adapters
Two components ingest sensor traffic, and they don't share a line of runtime code — only the infrastructure underneath them.
| UDP adapter | CoAP adapter | |
|---|---|---|
| Transport | Plain UDP — no CoAP involved at all | CoAP, RFC 7252, built on UDP |
| Bind address | 0.0.0.0:47000 — every IPv4 interface | [::]:5683 — every interface, IPv6 only |
| Device population | A separate sensor family, talking raw UDP directly | Peripherals reachable through a border router, talking CoAP |
| Current handling | Logs source / bytes / payload(hex) and re-emits every datagram as a raw signal — no decoding or schema validation yet, by explicit design | Parses each request into an {OID, payload} envelope; a per-OID parser registry decodes known OIDs into structured data, and forwards unrecognized OIDs on unparsed |
| Standard discovery | N/A — not a REST protocol | Standard CoAP resource discovery (GET /.well-known/core) is deliberately not implemented — the resources here are project-specific paths, not generic CoAP |
| Confinement | Both run as strictly-confined Ubuntu Core snaps, each declaring only the network plugs it actually needs to open its own socket. | |
The CoAP adapter doesn't reimplement RFC 7252 itself — the core wire-protocol engine (retransmission, message matching, CON/ACK/RST handling covered on the CoAP page) lives inside a general-purpose CoAP library it imports, wrapped by this project's own SDK layer. None of that low-level protocol logic is bespoke.
The mistake this section exists to prevent
Looking at a device diagram that lists both adapters, it's a completely natural first guess that one feeds the other — "CoAP traffic comes in, gets relayed as UDP to the other adapter." That guess is wrong, and it's wrong in a way that only shows up once you actually go read the source rather than infer from names and proximity.
| The natural first guess | What the source actually shows | |
|---|---|---|
| Relationship | Chained — the CoAP adapter's output feeds the UDP adapter | Parallel, independent components, each with its own ingestion responsibility |
| What the UDP adapter listens for | CoAP traffic relayed from the CoAP adapter | Its own raw UDP traffic on port 47000, directly from its own device population — its own scope doc explicitly says to exclude CoAP-speaking devices from what it handles |
| What actually connects them | A data pipeline, one hop to the next | Shared infrastructure only — the same SDK, the same underlying message bus, and borrowed source-level patterns for how to call that SDK correctly |
| Runtime code path between them | Assumed to exist | None — no file in either component references the other; no RPC call or shared queue between them |
The corrected mental model: two separate front doors into the same building, not one door leading to another.
Peripherals (CoAP devices) --CoAP:5683--> CoAP adapter ---\
\
shared local message bus
/
Sensor family B --raw UDP:47000--> UDP adapter ---/
Both adapters independently receive their own protocol's traffic from their own device population, and both independently publish onto the same shared bus for downstream consumers. Having the UDP adapter also handle CoAP-speaking devices would duplicate a responsibility the CoAP adapter already owns — the exact thing its own scope document's explicit exclusion is protecting against, not an oversight waiting to be "fixed" by wiring the two together.
Seeing "a future device will run both adapters" and assuming that means one feeds the other. The real architecture is closer to "the device runs two independent protocol adapters side by side," each scoped to a different device population, sharing only their SDK and message bus — never a request/response or forwarding relationship between the two components themselves. A diagram that puts two boxes near each other is not evidence of a code path between them; only the source is.
The shared local bus
Both adapters, and every other component on the device, publish onto one local message broker — an AMQP-style bus (see AMQP for the exchanges/queues/bindings model this kind of broker implements). A thin supervisor component launches the real broker process, health-checks it every few seconds, and force-restarts it if it dies or stops answering.
- AMQP bind
127.0.0.1:5672— loopback only, never reachable from any other device on the network.- Management interface
127.0.0.1:15672— same restriction.- Default credentials
- Only work from loopback, by explicit configuration — consistent with "same device only," not a real secret exposed to the network.
Every log line naming an AMQP sender from either adapter, and every signal a component emits through the shared SDK, is a client of this one local broker instance — never a network hop. The UDP adapter, the CoAP adapter, and every other snap on the device are, architecturally, just peers of each other on this one bus; it doesn't distinguish or prioritize between them.
From the local bus to the cloud
One component provides the device's only uplink: it's just another loopback AMQP client of the same local broker everyone else uses, and its distinguishing job is the other side of that connection.
- Relays local bus traffic to the cloud — over MQTT to a cloud IoT hub, or alternatively over WebSocket to a real-time relay endpoint, depending on which connection string is configured.
- Buffers outgoing telemetry to disk when offline — and reports a heartbeat back to the cloud that includes whether the local bus itself is reachable.
- Exposes device-management RPCs over that same connection: package install/refresh, key management, and a maintenance tunnel back to the device — the same uplink carries commands down, not just telemetry up (see Telemetry vs. commands).
Separately, at the OS level, one of several parallel connectivity mechanisms (an LTE modem path, or plain Ethernet) gets the Linux network stack onto a working link at all. None of them touch the message bus, UDP, or the CoAP service directly — they only set up the underlying link; whatever rides on top, including the cloud uplink itself, is agnostic to which one is active.
Walkthrough: a sensor, over raw UDP
One full device-to-cloud trip, hop by hop — each step names the layer doing the work and links back to that layer's own page.
- Application — the sensor builds a payload and sends it toward the gateway device's IP address, targeting port 47000.
- UDP — the OS wraps it in an 8-byte UDP header: a source port it assigns itself, destination port 47000, and a length field.
- IP, then Ethernet — wrapped in an IP header (source and destination address, protocol number 17 for UDP), then an Ethernet frame addressed to the gateway's MAC address, resolved via ARP if not already cached.
- Across the wire — sensor → switch/router → gateway device, invisible to every layer above Ethernet as long as the IP path resolves.
- Reception, bottom to top — the NIC receives the frame, Ethernet's header is stripped, IP's header is stripped, and the UDP header's destination port is matched against the one socket bound to 47000.
- The UDP adapter's socket — a blocking read call returns with the payload and the sender's address exactly as logged: source address, byte count.
- Logging + signal emission — logged, then emitted as a raw signal onto the local bus — the same AMQP client activity covered in The shared local bus above.
- A caveat worth knowing — a signal reaching the bus isn't automatically the same as durable, cloud-bound data; some of this ecosystem's own example code deliberately emits both a signal and a separate, persisted data row for exactly this reason. Today's UDP adapter emits only the raw signal — a deferred, explicitly-scoped next step for it to also emit a structured, persisted row.
- The cloud gateway picks it up — subscribed to the scope of bus traffic meant for the cloud, and relays it onward exactly as it would for any other component's data.
- Transport & cloud — depending on the gateway's configured connection string, this rides either MQTT to a cloud IoT hub or WebSocket to a real-time relay — either way, it lands in the cloud as device-to-cloud telemetry.
Walkthrough: a sensor, over CoAP
The second, independent trip — same shared bus and cloud mechanism at the end, but everything up to that point is a completely different code path.
- Application — a peripheral serializes its data, tags it with an OID (object identifier), and issues a CoAP
POSTto a sensor-data resource path on the gateway device. - CoAP over UDP — wrapped in a CoAP header (version/type/token-length, code
0.02 POST, message ID,Uri-Pathoptions), then a UDP header targeting port 5683. - Across the wire — over the local link (often via a border router) to the gateway device, whose CoAP server binds every interface, IPv6 only.
- The CoAP adapter's server — the underlying CoAP library parses the packet and hands the raw payload to the matching resource-path handler.
- A CoAP acknowledgement goes straight back — independent of anything downstream, the server responds immediately (e.g.
2.05 Content) — this is CoAP's own CON/ACK reliability from Reliability on top of UDP, confirming only that the packet was received, not that it ever reaches the cloud. - OID lookup & decode — the envelope is unwrapped; a registered OID gets decoded by its own parser into structured data, while an unrecognized OID is forwarded on unparsed so something else downstream can decode it.
- Onto the local bus — published via the shared SDK onto the same local broker every component in this walkthrough shares (see The shared local bus).
- The cloud gateway → MQTT or WebSocket — picked up as cloud-scoped traffic and relayed onward via whichever transport the gateway is configured to use.
- Lands in the cloud — the same telemetry that started as a handful of bytes on a peripheral device arrives as structured, identified data — with the CoAP hop, the bus hop, and the cloud-transport hop each having handled exactly one narrow part of the journey and nothing else.
Follow one byte, start to finish
A small, concrete exercise tying this whole page together. Suppose a peripheral's message contains a
sensor ID field with the value 25 — the exact wire-level encoding of that one field depends on
details specific to the real schema in use, so treat the encoding below as illustrative of the mechanism,
not a literal capture:
Application value: sensorId = 25 (0x19)
↓
Structured field encoding: [tag byte][value 25] e.g. 08 19
↓
CoAP payload: …OID envelope… 08 19 …
↓
CoAP packet: [Ver/T/TKL][Code=0.02 POST][MID][Token][Uri-Path options][0xFF] 08 19 …
↓
UDP payload: the entire CoAP packet above, unchanged
↓
UDP datagram: [src port][dst port=5683][length][checksum] + CoAP packet
↓
IP packet: [src IP][dst IP][proto=17] + UDP datagram
↓
Ethernet frame: [dst MAC][src MAC][EtherType] + IP packet + FCS
On the way back up at the receiver, this is the exact same list, read bottom to top —
Ethernet strips its header and hands the IP packet up, IP
strips its header and hands the UDP datagram up, UDP strips its 8 bytes and hands the CoAP packet up,
CoAP strips its own header/options and hands the raw bytes up to the application, which finally
reconstructs sensorId = 25. Every layer in between only ever sees "a payload it doesn't
interpret" — the same encapsulation idea covered in full on
Ethernet's encapsulation section and the
domain's own How the pieces fit together.
The whole device, by layer
Reading top to bottom below is reading "what depends on what to function" — applications need the bus, the bus needs a working network link, the link needs the device to have booted at all. Every layer discussed on this page fits into exactly one of these four bands.
Real-world applications
- Sensor-population diversity — real IoT gateways rarely see one clean protocol from every device; a separate adapter per device population, each scoped narrowly, is a common way to keep them from becoming each other's problem.
- No shared point of failure between adapters — because neither adapter's code calls the other, a bug or crash in one doesn't take down the other's ingestion path — only their shared bus and uplink are common risk.
- Adding a protocol without touching existing ones — a new device population speaking a third protocol becomes a third adapter publishing onto the same bus, not a change to either existing adapter.
- Offline-tolerant cloud uplink — buffering telemetry to disk when the cloud is unreachable (common on LTE-connected field devices) is a standard pattern for exactly the kind of intermittent connectivity real deployments have to tolerate.
Gotchas
- Two components appearing together in a device diagram is not evidence one feeds the other — see The mistake this section exists to prevent. Verify the actual code path, not the proximity of two boxes.
- A signal reaching the local bus doesn't guarantee it's durable or cloud-bound — some data paths need an explicit, separately-emitted persisted row, not just a fire-and-forget signal, before they reliably reach the cloud.
- The same protocol name can mean two unrelated things in the same ecosystem — CoAP here is both an application-level sensor-data protocol and, completely separately, a lower-level control-plane trick used elsewhere on the same device. Sharing a protocol name doesn't imply sharing a purpose.
- Standard CoAP resource discovery not being implemented here is a reminder worth generalizing: a component being RFC-compliant at the wire-protocol level (retransmission, message matching) says nothing about whether its resource semantics follow the RFC's optional conventions too.