Chapter 1: Configuration, Logging & Certificate Management
Welcome to the IoTMI Hub SDK walkthrough! Before your device can talk to the cloud, connect to MQTT, or manage smart-home endpoints, it needs to answer three fundamental questions:
- "What are my settings?" — Where's my endpoint? Am I provisioned yet? What's my serial number?
- "How do I report what's happening?" — When something goes wrong at 2 AM, how does a developer find out?
- "How do I prove I'm who I say I am?" — Certificates are the device's passport to the cloud.
This chapter covers the three foundational utilities that every other component in the SDK depends on: ConfigHandler, the Log framework, and CertHandler. Think of them as the plumbing, wiring, and locks of a house — invisible when they work, catastrophic when they don't.
Our Running Example: First Boot of a Smart Hub
Imagine you've just powered on a brand-new smart-home hub for the first time. The device needs to:
- Load its configuration file to discover its cloud endpoint and provisioning state.
- Initialize logging so every step is recorded.
- Read its claim certificate to authenticate with AWS IoT and begin fleet provisioning.
We'll trace this journey through the code, one utility at a time.
1. ConfigHandler — Your Device's Settings File
The Problem
An IoT device juggles dozens of settings: cloud endpoints, certificate paths, provisioning state, serial numbers, MQTT keep-alive intervals. These settings come from two sources — the factory (read-only) and runtime (read-write). You need a single place to read and update them, backed by a file that survives reboots.
The Analogy
Think of ConfigHandler as a two-drawer filing cabinet. The top drawer (ro) is locked — it holds factory-set values like the serial number and claim certificate paths. The bottom drawer (rw) is unlocked — the SDK can add, update, or remove entries as the device progresses through its lifecycle.
The Config File Format
The backing file is JSON with two top-level sections:
{
"ro": {
"SN": "H3A0PA01802300PE",
"sh_endpoint_url": "https://...",
"iot_claim_cert_path": "/etc/iotshd/claim_cert.pem"
},
"rw": {
"iot_provisioning_state": "NOT_PROVISIONED",
"managed_thing_id": ""
}
}
📁 Source:
commonUtils/IoTSmartHomeDevice-ConfigHandler/include/config_handler_constants.hdefines every well-known key as a#defineconstant.
Creating and Reading Config
To use ConfigHandler, you construct it with a file path. It immediately parses the JSON:
Reading a value searches ro first, then rw — factory settings always win:
std::string endpoint = handler.GetStringValue("sh_endpoint_url");
std::string state = handler.GetStringValue("iot_provisioning_state");
📁 Source:
config_handler.cpp—GetStringValue()checksro, thenrw, and throws if the key isn't found in either.
Writing Config
Only the rw section is writable. After updating values in memory, you flush to disk:
handler.WriteConfigStringValue("iot_provisioning_state", "PROVISIONED");
handler.WriteConfigStringValue("managed_thing_id", "mt-abc123");
Then persist to the file:
This also stamps a last_updated_epoch_time in the metadata — handy for detecting stale configs.
The C API
Because parts of the SDK are written in C, ConfigHandler exposes a C-compatible wrapper:
ConfigHandler* h = create_config_handler("/etc/iotshd/config.json");
const char* sn = get_string_value(h, "SN");
destroy_config_handler(h);
📁 Source:
config_handler_c.handconfig_handler_c.cppwrap the C++ class behind opaque pointers.
Key Constants
The SDK defines well-known config keys so you never have to hardcode strings:
| Constant | Key String | Example Value |
|---|---|---|
ENDPOINT_CONF |
"sh_endpoint_url" |
"https://..." |
IOT_PROVISIONING_STATE_CONF |
"iot_provisioning_state" |
"NOT_PROVISIONED" |
CLAIM_CERT_PATH_CONF |
"iot_claim_cert_path" |
"/etc/iotshd/claim_cert.pem" |
MANAGED_THING_ID_CONF |
"managed_thing_id" |
"mt-abc123" |
SERIAL_NUM_CONF |
"SN" |
"H3A0PA01802300PE" |
2. The Log Framework — Your Device's Black Box Recorder
The Problem
When a device is deployed in someone's home, you can't attach a debugger. You need structured, leveled logging that works from both C and C++ code, supports multiple output destinations (file, daemon, console), and is thread-safe.
The Analogy
The Log framework is like an airport control tower. There's one central tower (the IotShdLog singleton) that knows about all the radio channels (loggers). Each channel can broadcast to multiple runways (sinks) — a file on disk, a logging daemon, or the console. Every message gets a severity stamp (debug, info, warn, error) so you can filter the noise.
Creating a Logger (C++)
The simplest path uses the factory function that creates a logger with the platform's default sink:
#include "log/iotshd_log.h"
#include "log/iotshd_log_logger_default.h"
auto logger = IoTSmartHomeDevice::Log::create_default_type_logger("my_component");
Register it as the process-wide default:
Then log at any level using fmt-style format strings:
logger->info("Endpoint resolved: {}", endpoint_url);
logger->warn("Retry attempt {} of {}", attempt, max_retries);
logger->err("Failed to parse config: {}", error_msg);
📁 Source:
include/log/iotshd_log_logger.h— theLoggerclass witherr(),warn(),info(),debug()methods.
Creating a Logger (C)
C code uses an opaque handle and printf-style formatting:
#include "log/c_wrapper/iotshd_log_c_wrapper.h"
LoggerHandle h = iotshdlog_c_create_default_type_logger("my_c_module");
iotshdlog_c_set_default_logger(h);
📁 Source:
src/iotshd_log_c_wrapper.cppbridges C calls to the C++Loggerclass.
The Singleton Registry
IotShdLog is a singleton that acts as a phone book for loggers:
// Register a named logger
IoTSmartHomeDevice::Log::register_logger(my_logger);
// Retrieve it anywhere in the process
auto logger = IoTSmartHomeDevice::Log::get_logger("my_component");
This is how ConfigHandler, CertHandler, and every other module get their own named logger without passing logger objects around.
📁 Source:
include/log/iotshd_log.h—IotShdLogclass withregister_logger(),get_logger(),set_default_logger().
Convenience Macros
Each module defines shorthand macros. For example, ConfigHandler defines:
#define CONFIG_LOG_INFO(...) GetLogger().info(__VA_ARGS__)
#define CONFIG_LOG_ERROR(...) GetLogger().err(__VA_ARGS__)
📁 Source:
config_logger.hpp— wraps the singleton lookup into tidy one-liners.
Log Architecture at a Glance
The Logger class accepts one or more sinks (output destinations). Each sink receives formatted trace messages. The framework supports:
- File sink — writes to rotating log files on disk
- Daemon sink — sends logs to a separate
iotshd-log-daemonprocess via shared memory - Console sink — prints to stdout/stderr
The daemon mode is especially useful on production devices where a dedicated process manages log rotation, upload, and cleanup.
3. CertHandler — Your Device's Passport Office
The Problem
An IoT device goes through multiple identity phases. At the factory, it gets a claim certificate (a temporary visitor badge). Some devices also carry a DHA certificate (Device Hardware Attestation — a hardware-backed ID). After fleet provisioning, the cloud issues a permanent certificate (a full passport). The SDK needs to read, write, and delete these certificates — and the storage backend might be the filesystem or a secure hardware enclave accessed via D-Bus.
The Analogy
CertHandler is a passport office with two service windows. Window 1 (filesystem) handles certificates stored as plain .pem files on disk. Window 2 (secure storage) handles certificates locked inside a hardware security module, accessed through D-Bus IPC. You walk up to the front desk (CertHandler) and it routes you to the right window based on how the device was configured.
Certificate Types
The SDK defines three certificate types:
| Type | Purpose | Lifetime |
|---|---|---|
CLAIM |
Temporary cert for fleet provisioning | Until provisioned |
DHA |
Hardware attestation cert chain | Permanent (hardware) |
PERMANENT |
Cloud-issued device identity | Post-provisioning |
📁 Source:
IoTSmartHomeDevice-SecureStorageCertHandler/include/secure_storage_cert_handler_interface.hpp
The Strategy Pattern
CertHandler uses the strategy pattern — it delegates storage operations to an injected backend. The constructor you call determines which backend is active:
Filesystem backend:
auto config = std::make_shared<ConfigHandler>("/etc/iotshd/config.json");
auto fs_handler = std::make_shared<DefaultFileSystemCertHandler>("/etc/iotshd/certs");
CertHandler cert_handler(config, fs_handler);
Secure storage backend:
auto config = std::make_shared<ConfigHandler>("/etc/iotshd/config.json");
auto ss_handler = createSecureStorageCertHandler();
CertHandler cert_handler(config, ss_handler);
📁 Source:
cert_handler.hpp— two constructors, one per backend. Thestorage_type_member tracks which path to take.
Reading a Certificate
Regardless of backend, the API is the same:
std::string cert, private_key;
bool ok = cert_handler.read_cert_and_private_key(CLAIM, cert, private_key);
Under the hood, the filesystem backend reads the paths from ConfigHandler and loads the files:
// Inside CertHandler::read_cert_and_private_key (filesystem path)
cert_path = config_handler_->GetStringValue("iot_claim_cert_path");
return file_system_cert_handler_->read_cert_and_private_key(
cert_path, pk_path, cert_value, private_key_value);
The secure storage backend calls D-Bus instead:
// Inside DbusSecureStorageCertHandler::read_cert_and_private_key
std::string result = dBus_client_.callMethod(GET_CLAIM_METHOD_CONTEXT);
📁 Source:
cert_handler.cppfor the routing logic,dbus_secure_storage_cert_handler.cppfor the D-Bus implementation.
Writing a Permanent Certificate
After fleet provisioning, the cloud returns a new certificate. CertHandler writes it to the appropriate backend:
std::string cert_path, pk_path;
cert_handler.write_permanent_cert_and_private_key(
cert_pem, private_key_pem, cert_path, pk_path);
For filesystem storage, this writes .pem files and returns their paths. For secure storage, it sends the data over D-Bus and sets the paths to "SECURE_STORAGE" — a sentinel value that tells other components to use the secure storage handler for future reads.
The Interface Hierarchy
FileSystemCertHandlerInterface (abstract)
└── DefaultFileSystemCertHandler (reads/writes .pem files)
SecureStorageCertHandlerInterface (abstract)
└── DbusSecureStorageCertHandler (D-Bus to hardware enclave)
└── StubSecureStorageCertHandler (testing stub)
📁 Source:
filesystem_cert_handler_interface.hpp,default_filesystem_cert_handler.hpp,secure_storage_cert_handler_interface.hpp,internal/dbus/dbus_secure_storage_cert_handler.hpp
Putting It All Together: The First-Boot Sequence
Now let's trace our running example — what happens when the hub powers on for the first time.
sequenceDiagram
participant App as Application
participant Cfg as ConfigHandler
participant Log as Log Framework
participant Cert as CertHandler
participant FS as Filesystem
App->>Log: create_default_type_logger("hub")
App->>Cfg: ConfigHandler("/etc/iotshd/config.json")
Cfg->>FS: Read & parse JSON file
App->>Cfg: GetStringValue("iot_provisioning_state")
Cfg-->>App: "NOT_PROVISIONED"
App->>Cert: read_cert_and_private_key(CLAIM, ...)
Cert->>Cfg: GetStringValue("iot_claim_cert_path")
Cert->>FS: Read claim_cert.pem & claim_pk.pem
Cert-->>App: cert + private_key
Here's what each step does:
- Initialize logging — The app creates a named logger and registers it as the default. From this point on, every module can log.
- Load configuration — ConfigHandler reads the JSON file, parsing it into an in-memory document with
roandrwsections. - Check provisioning state — The app reads
iot_provisioning_stateand finds"NOT_PROVISIONED". - Read claim certificate — CertHandler asks ConfigHandler for the claim cert path, then reads the
.pemfiles from disk. - The app now has everything it needs to begin fleet provisioning (covered in later chapters).
Internal Implementation Walkthrough
ConfigHandler Internals
| File | Role |
|---|---|
include/config_handler.hpp |
Class declaration — GetStringValue, WriteConfigStringValue, WriteConfigToFile |
include/config_handler_constants.h |
All well-known key #defines (ENDPOINT_CONF, SERIAL_NUM_CONF, etc.) |
include/config_handler_c.h |
C-compatible API with opaque ConfigHandler* pointer |
src/config_handler.cpp |
Implementation — JSON parsing via RapidJSON, ro-then-rw lookup, metadata timestamping |
include/config_logger.hpp |
Convenience macros (CONFIG_LOG_INFO, etc.) that wrap the singleton logger |
Key design decisions:
- RapidJSON is used for zero-copy JSON parsing — important on memory-constrained devices.
- ro takes priority over rw in reads — factory settings can't be accidentally overridden.
- Metadata timestamps are auto-updated on every WriteConfigToFile() call.
Log Framework Internals
| File | Role |
|---|---|
include/log/iotshd_log.h |
IotShdLog singleton — the global logger registry |
include/log/iotshd_log_logger.h |
Logger class — sinks, trace levels, fmt-based formatting |
include/log/iotshd_log_logger_default.h |
create_default_type_logger() factory function |
src/iotshd_log_c_wrapper.cpp |
C wrapper — LoggerWrapper struct bridges C handles to C++ shared_ptrs |
daemon/iotshd_log_daemon.cpp |
Standalone daemon process for production log collection |
Key design decisions:
- Singleton pattern ensures one global registry — any module can retrieve any logger by name.
- Pluggable sinks via the BaseSink interface — add new output destinations without changing the Logger class.
- Thread safety — the singleton uses std::mutex, and the Logger runs a background task queue for async sink writes.
- Dual C/C++ API — the C wrapper uses opaque LoggerWrapper* handles and printf-style formatting.
CertHandler Internals
| File | Role |
|---|---|
include/cert_handler.hpp |
Main class — strategy pattern with two constructors |
include/filesystem_cert_handler_interface.hpp |
Abstract interface for filesystem backends |
include/default_filesystem_cert_handler.hpp |
Default .pem file reader/writer |
include/secure_storage_cert_handler_interface.hpp |
Abstract interface for secure storage + CERT_TYPE_T enum |
internal/dbus/dbus_secure_storage_cert_handler.hpp |
D-Bus backend — calls fxn.hub.securestorage service |
src/cert_handler.cpp |
Routing logic — checks storage_type_ and delegates |
src/default_filesystem_cert_handler.cpp |
File I/O for .pem files |
src/dbus_secure_storage_cert_handler.cpp |
D-Bus method calls for read/write/delete |
Key design decisions:
- Strategy pattern — the constructor determines the backend; all subsequent calls are polymorphic.
- ConfigHandler integration — CertHandler reads certificate paths from the config, keeping paths centralized.
- "SECURE_STORAGE" sentinel — when the secure storage backend writes a cert, it sets the config path to this string, signaling other components to use the secure storage handler.
Conclusion
You've now met the three utilities that form the foundation of the IoTMI Hub SDK:
- ConfigHandler — a two-section (ro/rw) JSON key-value store that holds every setting your device needs, from endpoints to provisioning state.
- Log framework — a singleton-based, multi-sink logging system with both C and C++ APIs, designed for constrained devices that need reliable diagnostics.
- CertHandler — a strategy-pattern certificate manager that abstracts away where certificates live (filesystem vs. secure hardware), letting the rest of the SDK work with a uniform API.
These three utilities are the bedrock. Every component you'll encounter in the rest of this walkthrough — from MQTT connections to device provisioning — depends on them.
In Chapter 2: ACE Middleware Framework, we'll see how the SDK builds on this foundation to create a middleware layer that orchestrates communication between your device's application logic and the cloud. The ACE framework is where ConfigHandler's settings, the Log framework's diagnostics, and CertHandler's certificates all come together to establish a live connection.