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.

Quick reference
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.

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 adapterCoAP adapter
TransportPlain UDP — no CoAP involved at allCoAP, RFC 7252, built on UDP
Bind address0.0.0.0:47000 — every IPv4 interface[::]:5683 — every interface, IPv6 only
Device populationA separate sensor family, talking raw UDP directlyPeripherals reachable through a border router, talking CoAP
Current handlingLogs source / bytes / payload(hex) and re-emits every datagram as a raw signal — no decoding or schema validation yet, by explicit designParses 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 discoveryN/A — not a REST protocolStandard CoAP resource discovery (GET /.well-known/core) is deliberately not implemented — the resources here are project-specific paths, not generic CoAP
ConfinementBoth 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 guessWhat the source actually shows
RelationshipChained — the CoAP adapter's output feeds the UDP adapterParallel, independent components, each with its own ingestion responsibility
What the UDP adapter listens forCoAP traffic relayed from the CoAP adapterIts 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 themA data pipeline, one hop to the nextShared 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 themAssumed to existNone — 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.

Common mistake this section exists to prevent

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.

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.

Two sensor populations feed two independent adapters, both of which publish onto one shared local message bus; a single gateway relays qualifying traffic onward via MQTT or WebSocket Sensor family — raw UDP sends datagrams toward port 47000 Peripherals — CoAP POSTs an OID-tagged payload UDP adapter binds 0.0.0.0:47000, logs + re-emits CoAP adapter binds [::]:5683, decodes by OID Local AMQP bus — loopback only (127.0.0.1:5672) both adapters are just peer clients of this one broker Cloud gateway relays qualifying traffic onward MQTT → cloud IoT hub one configured uplink option WebSocket → cloud relay the other configured option
Two independent paths in, one shared bus in the middle, two possible paths out — nothing here is one straight pipeline.

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.

  1. Application — the sensor builds a payload and sends it toward the gateway device's IP address, targeting port 47000.
  2. UDP — the OS wraps it in an 8-byte UDP header: a source port it assigns itself, destination port 47000, and a length field.
  3. 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.
  4. Across the wire — sensor → switch/router → gateway device, invisible to every layer above Ethernet as long as the IP path resolves.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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.

  1. Application — a peripheral serializes its data, tags it with an OID (object identifier), and issues a CoAP POST to a sensor-data resource path on the gateway device.
  2. CoAP over UDP — wrapped in a CoAP header (version/type/token-length, code 0.02 POST, message ID, Uri-Path options), then a UDP header targeting port 5683.
  3. Across the wire — over the local link (often via a border router) to the gateway device, whose CoAP server binds every interface, IPv6 only.
  4. The CoAP adapter's server — the underlying CoAP library parses the packet and hands the raw payload to the matching resource-path handler.
  5. 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.
  6. 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.
  7. 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).
  8. The cloud gateway → MQTT or WebSocket — picked up as cloud-scoped traffic and relayed onward via whichever transport the gateway is configured to use.
  9. 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.

Four stacked layers on the device: protocol adapters and applications at the top, core messaging infrastructure below them, connectivity below that, and hardware and boot at the bottom — each layer depending on the one beneath it Protocol adapters / applications the only layer that actually produces or forwards data publish via the message bus Core messaging infrastructure the shared local bus, plus the cloud-uplink gateway needs a working network path from Connectivity (WAN & peripheral links) gets the Linux network stack onto a link at all runs as managed daemons on Hardware & boot partitions, bootloader, device tree, base filesystem Dev/diagnostic tooling cuts across every layer, but sits outside the runtime data path entirely.
Compare with IoT Architecture's device/gateway/cloud/application layering — this is the same idea, one layer zoomed in to what's actually running on a single real device.

Real-world applications

Gotchas

Field notes