Skip to content

Architecture Overview

Muhammad Nuzaihan edited this page Aug 22, 2026 · 2 revisions

Architecture Overview

The Duck class hierarchy

Every node type derives from a common templated base class, Duck<WifiCapability, RadioType> (src/Ducks/Duck.h):

Duck<WifiCapability, RadioType>   (src/Ducks/Duck.h)
 ├── DuckLink                     (leaf sensor node, no relay)
 ├── MamaDuck                     (core mesh relay node)
 ├── PapaDuck                     (gateway/sink node, WiFi/MQTT uplink)
 └── DetectorDuck                 (RSSI survey tool, no data relay)

See Duck Types for what each subclass is for.

WifiCapability and RadioType are template parameters (defaulting to DuckWifiNone and DuckLoRa), so the same Duck subclass can be compiled with or without WiFi depending on the board/role.

Startup and main loop

A sketch typically does:

MamaDuck duck("MAMA0001");

void setup() {
  duck.setupWithDefaults();
}

void loop() {
  duck.run();
}

setupWithDefaults() (in Duck.h) initializes, in order:

  1. Serial (115200 baud).
  2. duckidentity::begin() — loads or generates this device's X25519 identity keypair (see Security and Encryption).
  3. opendmsconfig::begin(), meshgroupconfig::begin(), radioregionconfig::begin() — load any field-provisioned secrets/region from flash.
  4. setupLoRaRadio() — initializes the radio with the (possibly region-overridden) parameters.

run(), called every loop iteration, does:

  1. Services LoRa interrupt flags.
  2. Polls for serial provisioning commands (AT+OPENDMSKEY=, AT+MESHKEY=, AT+RADIOREGION=, see Security and Encryption and Radio and Regions).
  3. If the device hasn't joined a CDP network yet, attempts network join (RREQ-based discovery) or times out into NetworkState::PUBLIC after NET_JOIN_DELAY * 5 + 5000 ms. DetectorDuck bypasses this entirely and goes straight to PUBLIC.
  4. Once PUBLIC, dispatches any received radio packet to handleReceivedPacket().

Sending data

All Duck subclasses inherit several send methods from Duck, each targeting a different trust boundary:

Method Purpose Encrypted?
sendData(topic, data, targetDevice) Plain CDP send (string or byte-buffer overloads) No
sendSealedData(topic, data, targetDevice) One-way seal to MeshBeacon Ops's static public key Yes (sealed, ephemeral)
sendEncryptedData(topic, data, targetDevice) Duck-to-Duck session encryption Yes (peer session)
sendGroupData(topic, data, targetDevice) Broadcast authenticated with a shared mesh group key Yes, if configured (falls back to plaintext otherwise)
announceIdentity(targetDevice) Broadcast this Duck's own public key (TOFU) N/A (key is not secret)

Encryption is opt-in per call site and off by default — see Security and Encryption for the full design and reasoning.

Every send ultimately builds a CdpPacket (see Packet Format and Topics), asks DuckRouter for the best next hop, assigns a unique message ID via the bloom filter, and transmits over the radio (see Routing and Mesh and Radio and Regions).

Source layout

Directory Contents
src/Ducks/ Duck.h (base), MamaDuck.h, PapaDuck.h, DuckLink.h, DetectorDuck.h, DuckTypes.h
src/routing/ DuckRouter, bloomfilter, Neighbor, SignalScore, RouteJSON
src/radio/ DuckLoRa (RadioLib-based SX12xx driver), RadioRegionConfig
src/security/ DuckIdentity, DuckCrypto, OpenDmsConfig, MeshGroupConfig, SecurityEventCounters
src/payloads/ duck_payloads.proto + generated nanopb code + DuckPayloads encode/decode helpers
src/wifi/ DuckWifi, DuckWifiNone
src/utils/ Logging, error codes, misc helpers
src/include/ cdpcfg.h (build-time configuration) and per-board headers

Clone this wiki locally