Introducing NDNph and New Version of esp8266ndn

NDNph is my latest Named Data Networking (NDN) client library. This article gives an overview of this library.

History and Motivation

In 2016, I started esp8266ndn. It contains a copy of ndn-cpp-lite, UCLA REMAP's C++ library that does not use dynamic memory allocations. I then added integrations with ESP8266's network stack and crypto functions, making esp8266ndn the first NDN library that works on the ESP8266 microcontroller. Using this library, I built several projects, including a wearable jewelry and a call button. University of Memphis also deployed several sensor nodes using esp8266ndn.

Over the years, esp8266ndn gained many new features, and widened platform support to include ESP32 and nRF52. However, using ndn-cpp-lite as the core is becoming problematic:

  • New protocol features show up slowly, because ndn-cpp-lite author would not add a feature until ndn-cxx has it, and design discussions for ndn-cxx sometimes take several years.
  • There is no generic TLV encoder/decoder, making it difficult to support TLV structures in application layer.
  • ndn-cpp-lite is bloated with obsolete features, due to their backwards compatibility guarantees. Consequently, binary code size is unnecessarily large.
  • Although I can add patches to ndn-cpp-lite during importing into esp8266ndn, it has been difficult to test these patches. This isn't ndn-cpp-lite's fault, but is still an issue.

After publishing NDNts, I thought: why not rip ndn-cpp-lite out of esp8266ndn, and replace it with my own?

Learning from the Giants

ArduinoJson is one of the best Arduino libraries. I spent some time reading through their API, and liked several aspects:

  • It has a region-based memory allocator, which owns the memory associated with everything related to a JSON document.
  • It is a header-only library, making it highly portable: it works both in Arduino IDE, and on other platforms with a C++ compiler.
  • Another benefit of being header-only is that, the application can specify compile time configuration by defining macros above #include <ArduinoJson.h>, without modifying the source code of ArduinoJson library.

I also learned from NDN-Lite, an embedded NDN stack written in C language. Although I dislike many of their APIs, they did one thing right:

  • NDN-Lite does not try to support all platforms with the same codebase. Instead, maintain a "core" codebase, and several "adaptations" (i.e. ports) for different platforms.

Design Decisions

Following the example of NDN-Lite, the "core" of esp8266ndn would be in a separate library, NDNph. In this name, "p" means "packet", as this library provides NDN packet encoding, among other features. "h" means "headers", which indicates that this is a header-only library. Then, esp8266ndn becomes a port of NDNph.

Memory management is the most important aspect of any C and C++ library. Following the example of ArduinoJson, NDNph adopts region-based memory management.

NDNph supports multiple platforms, big and small. Currently, NDNph supports Linux, and esp8266ndn supports ESP8266, ESP32, and nRF52. This in turn simplifies unit testing for NDNph, because I can run most tests on a computer and in the cloud, instead of painstakingly making them work on the ESP8266. However, this does limit the codebase to C++11 because that's what Arduino is using, instead of the newer C++17 standard.

Like most of my other projects, I don't care much about backwards compatibility. Being a personal project, I have to prioritize in developing for the latest and greatest, instead of worrying about backwards compatibility.

NDNph Architecture

Unlike the many packages in NDNts, NDNph is a single library. I'd give a brief introduction of its components.

We start from the boring part, memory management:

  • Region class is the region-based memory allocator.
  • You may either use StaticRegion class template to reserve memory on the stack, or DynamicRegion class to reserve memory on the heap.
  • It's recommended to create a new Region for each packet you are sending or processing, and discard it as soon as you finish working with that packet.

The memory allocator allows us to have packet representation and encoding:

  • Encoder and Decoder implement Type-Length-Value (TLV) structure encoder and decoder.
  • EvDecoder is a decoder that is aware of TLV evolvability guidelines. The API is similar to its NDNts counterpart, but may be tricky to understand in C++.
  • Component, Name, Interest, Data, and Nack types have their usual meaning.
  • ndnph::convention namespace supports NDN Naming Conventions rev2.

We need a KeyChain to secure the packets:

  • PrivateKey is something that can sign a packet.
  • PublicKey is something that can verify signature on a packet.
  • DigestKey is both a PrivateKey and a PublicKey, but it only provides integrity protection.
  • EcdsaPrivateKey and EcdsaPublicKey can create and verify ECDSA signatures.
  • Currently, there is no key management or trust schema support.

To send or receive packets:

  • Transport is a low-level transport that transmits and receives packets as buffers. Each port is expected to implement a few subclasses of Transport.
  • Face wraps around Transport to encode/decode packets.
  • PacketHandler is the base class of (a component of) your application. You can override processInterest, processData, and processNack methods to receive packets, and invoke send or reply methods to send packets. This is generally where you start developing an application.
  • Multiple PacketHandler can be attached to the same Face. See this article for an explanation of my design.

Going a little higher layer:

  • SegmentProducer and SegmentConsumer can serve and retrieve segmented objects. It doesn't have fancy congestion control though.
  • RdrMetadataProducer enables version discovery of a published segmented object.
  • PingServer and PingClient implement a simple reachability test.
  • These can serve as examples on how to write applications.

In C++ land there are some limitations due to memory management:

  • If you allocate Interest, Data, or Nack from a Region, be sure to test for allocation failure: if (!interest) { /* allocation failure */ return; }. Otherwise, you may run into segmentation faults.
  • Neither Face nor PacketHandler keeps track of pending Interests. This means that your application may receive Data/Nack that you didn't express Interest for. You have to check in the processData and processNack override, and return false if the packet isn't for you so that it can go to the next PacketHandler.
  • Likewise, there's no notification about Interest timeout. If you need that, you'll have to keep the timer yourself.
  • Received signed Interest or Data will have the signature stored in Interest or Data class. It is valid as long as the Region holding the packet buffer is alive.
  • You can can use PacketHandler::send(interest.sign(key)) or PacketHandler::reply(data.sign(key)) to send signed Interest or Data, but the signature would not be stored in Interest or Data class.

esp8266ndn in 2020

esp8266ndn has been upgraded to become a port of NDNph. Most NDN related features come directly from NDNph. What remains in esp8266ndn codebase are:

  • Transport subclasses, including UDP, Ethernet, and Bluetooth Low Energy.
  • Crypto integrations.
  • ESP32 thread-safe queue implementation.
  • queryFchService to connect to global NDN testbed.
  • UnixTime service to perform time synchronization.

Integration with NDN-Lite has been removed, but in the future I may publish that as a separate library.

How to Get Started

NDNph repository on GitHub and esp8266ndn repository on GitHub have setup instructions in README. When installed in Arduino IDE, you can access esp8266ndn examples.

Thanks and good luck!