- Compile-time macros
- NetworkProfile
- WiFiProfile
- EthProfile
- NtpProfile
- MqttProfile
- Passing a subclass config to a base function
Define these before including any library header, or via platformio.ini build_flags.
| Macro | Default | Range | Description |
|---|---|---|---|
HOST_FQDN_LEN |
253 | 1 – 253 | Maximum FQDN length in Host objects. |
HOST_FQDN_LABEL_LEN |
63 | 1 – 63 | Maximum single label length. Must be ≤ HOST_FQDN_LEN. |
NTP_PROFILE_ENABLED |
1, 0 on AVR | 0 or 1 | Whether NTP exists in the build. |
NTP_PROFILE_SERVER_COUNT |
3 (ESP32), 1 elsewhere | 1 – 3 | NTP server slots. A count, not a switch: 0 is refused with a #error naming NTP_PROFILE_ENABLED. |
NETWORK_PROFILE_DNS_SERVER_COUNT |
2 (1 on AVR) | 1 – 3 on ESP32, 1 – 2 on ESP8266, 1 on AVR | DNS server slots per profile. |
NETWORK_PROFILE_HOSTNAME_LEN |
63 | 10 – 63 | Maximum hostname length. Lower bound accommodates the generated default (e.g. ESP-02D6FC). |
NETWORK_PROFILE_JSON_LEN |
1200 | — | Maximum JSON content length. The library itself needs at most 482; the rest is headroom for subclass fields added through _doJson(). |
NETWORK_PROFILE_MUTEX_TIMEOUT |
1000 | — | Mutex acquisition timeout in milliseconds. Used on ESP32 only; ESP8266 (cooperative scheduling) and AVR (single-threaded) have no lock and ignore it. |
NETWORK_PROFILE_DEFAULT_ETH_MAC |
— | — | Required on AVR platforms without a unique serial number. Format: "4E:52:47:30:30:31". |
WIFI_PROFILE_DEFAULT_WIFI_TX_POWER |
19.5 (ESP32) / 20.5 (ESP8266) | — | Default WiFi TX power in dBm. |
Abstract base class for all network interface profiles. Not instantiated
directly — use WiFiProfile or EthProfile.
| Constant | Value | Description |
|---|---|---|
DNS_SERVER_COUNT |
2 (1 on AVR) | Effective DNS server count (reflects macro). |
MAC_LEN |
6 | MAC address length in bytes. |
MAC_STR_SIZE |
18 | MAC string buffer size ("AA:BB:CC:DD:EE:FF\0"). |
MUTEX_TIMEOUT |
1000 | Mutex acquisition timeout in milliseconds. |
MAX_HOSTNAME_LEN |
63 | Effective maximum hostname length (reflects macro). |
MAX_HOSTNAME_SIZE |
64 | Hostname buffer size including null terminator. |
PRIORITY_INVALID |
255 | Sentinel returned by getPriority() on mutex timeout. Valid priorities are [0, 254]. |
JSON_LEN |
1200 | Effective JSON content length (reflects macro). |
JSON_SIZE |
1201 | JSON buffer size including null terminator. |
enum class InterfaceType : uint8_t {
UNKNOWN = 0,
WIFI = 1,
ETH = 2,
};Returned by getInterfaceType(). Each subclass returns a compile-time constant.
enum class ConfigSource : uint8_t {
ACTIVE = 0,
FACTORY = 1,
};Selects between the active (user-set) value and the hardware default for
getMac() and getHostname().
ACTIVE— returns the user-set override if present; falls back toFACTORYif not set.FACTORY— always returns the generated/factory value regardless of any override.
using MACAddress = uint8_t[MAC_LEN];enum class ConfigCheck : uint8_t {
Ok = 0,
BadHostname,
BadIp,
BadMask,
GatewayOffSubnet,
BadDns,
BadSsid, // WiFiProfile only
BadPassword, // WiFiProfile only
BadTxPower, // WiFiProfile only
};The result of checkConfig(): Ok, or the first problem found. BadIp covers
a host address that is unusable (0.0.0.0/8, loopback, multicast or reserved)
or that is the subnet's network or broadcast address. The last three values are
produced only by WiFiProfile::checkConfig().
Aggregates all profile fields for atomic get/set via setConfig() / getConfig().
struct NetworkConfig {
bool dhcp = true;
IPAddress ip;
IPAddress mask;
IPAddress gateway;
IPAddress dns[DNS_SERVER_COUNT];
char hostname[MAX_HOSTNAME_SIZE] = {};
uint8_t priority = 0;
MACAddress mac = {};
};Notes:
macsets the active MAC override. To revert to the hardware default, set all bytes to0x00.
NetworkProfile();
explicit NetworkProfile(const NetworkConfig& cfg);NetworkProfile is non-copyable. The default MAC is generated by
_initMacDefault(), which is called from the subclass constructor.
virtual InterfaceType getInterfaceType() const = 0;Returns the interface type. Compile-time constant — no mutex needed.
All methods are thread-safe.
bool isDhcp() const;
bool setDhcp(bool dhcp);
IPAddress getIp() const;
bool setIp(IPAddress ip);
IPAddress getMask() const;
bool setMask(IPAddress mask);
IPAddress getGateway() const;
bool setGateway(IPAddress gateway);
IPAddress getDns(uint8_t index = 0) const;
bool setDns(IPAddress dns, uint8_t index = 0);getDns() and setDns() accept an index in [0, DNS_SERVER_COUNT).
Out-of-range values are clamped to 0. getDns() returns 0.0.0.0 on mutex
timeout.
bool getHostname(char* buf, size_t len,
ConfigSource source = ConfigSource::ACTIVE) const;
bool setHostname(const char* hostname);
static bool isValidHostname(const char* hostname);getHostname() — copies the hostname into buf. With
ConfigSource::ACTIVE, returns the user-set hostname if present, otherwise
falls back to the generated default (e.g. ESP-02D6FC). With
ConfigSource::FACTORY, always returns the generated default. Returns false
on truncation or mutex failure.
setHostname() — validates against RFC 1123 before storing. An empty
string resets the active hostname; subsequent getHostname() calls will return
the generated default.
isValidHostname() — enforces: length 1–MAX_HOSTNAME_LEN, ASCII
letters/digits/hyphens only, no leading or trailing hyphen, not all-numeric.
Moved to NtpProfile. The servers a device synchronises from are a property of
the device — it has one clock — not of whichever interface happens to be up, and
a copy per profile meant the same address typed twice, stored twice, and free to
drift. See NtpProfile.
NtpProfile::importLegacy() reads servers written by a version that still stored
them here.
uint8_t getPriority() const;
bool setPriority(uint8_t priority);Lower value = higher priority. Valid range: [0, 254]. Value 255 is reserved as
PRIORITY_INVALID and is rejected by setPriority(). getPriority() returns
PRIORITY_INVALID on mutex timeout.
bool getMac(MACAddress mac,
ConfigSource source = ConfigSource::ACTIVE) const;
bool setMac(const MACAddress mac);getMac() — with ConfigSource::ACTIVE, returns the user-set override if
non-zero, otherwise the hardware default. With ConfigSource::FACTORY, always
returns the hardware default.
setMac() — validates with isValidMac() before storing. To revert to the
hardware default, use setConfig() with mac set to all zeros.
bool setConfig(const NetworkConfig& cfg);
bool getConfig(NetworkConfig& cfg) const;setConfig() — atomically updates all base fields (IP, DNS, hostname,
priority, MAC) in a single mutex acquisition. The struct is validated before the
mutex is acquired — the profile is not modified if validation fails. NTP servers
are not among these fields; they live in NtpProfile.
getConfig() — atomically reads all base fields in a single
mutex acquisition. The mac field in the returned struct contains the active
MAC override (may be all zeros if no override is set).
bool toJson(char* json, size_t len) const;Serialises the full profile to a JSON object. Password is intentionally omitted for security.
Output fields (base class): dhcp, ip, mask, gw, dns0[…dns1],
host, prio, mac[, ntp0…ntp2].
WiFiProfile appends ssid and txpwr via _doJson(). txpwr is printed
with two decimals, which is exact for the 0.25 dBm grid isValidTxPower()
enforces ("txpwr":19.50).
Use NetworkProfile::JSON_SIZE as the buffer size:
char json[NetworkProfile::JSON_SIZE];
profile.toJson(json, sizeof(json));Returns false on mutex timeout or output truncation.
bool saveCfg(const char* ns);
bool loadCfg(const char* ns);
bool clearCfg(const char* ns);Use the Preferences namespace as a key (e.g. "net_wifi", "net_eth").
Persistence is transactional: saveCfg() writes a completion marker last,
so loadCfg() returns false for a namespace that was never saved or whose
save was interrupted (e.g. power loss) — it never loads a half-written profile.
saveCfg() — saves the full profile to non-volatile storage. Returns
true on success; false on a storage failure or mutex timeout. Because the
store is single-buffered, an interrupted re-save also invalidates the previously
stored profile (there is no rollback to the prior copy).
loadCfg() — loads the full profile from non-volatile storage. Returns
true on success; false on an empty/never-written namespace, an incomplete
save (torn write), a storage failure, or mutex timeout — so a false return is
the reliable "no valid saved configuration" signal used by the first-run
pattern.
loadCfg() resets every field to its default before reading it, so a load
restores exactly what the last save held rather than merging into whatever the
object already had. This matters because saveCfg() removes the key of an
empty string instead of storing a blank: clearing the hostname, saving, then
loading back into the same object would otherwise have handed back the old name.
Loading into a freshly constructed profile was never affected.
On WiFiProfile, TX power falls back to DEFAULT_WIFI_TX_POWER_dBm when the
stored value is missing or outside what this build accepts, rather than being
restored as-is. Two cases reach it: a namespace written before txpwr existed,
and a namespace carried between platforms, since the accepted range differs
(ESP32 takes discrete levels up to 19.5 dBm, ESP8266 a continuous range up to
20.5). The rest of the profile still loads and loadCfg() still returns true
— only the TX power is replaced.
clearCfg() — erases the entire Preferences namespace. After calling this,
loadCfg() returns false and a reboot will load default values.
static bool macToStr(char* buf, size_t len, const MACAddress mac);
static bool macFromStr(const char* str, MACAddress mac);
static bool isValidMac(const MACAddress mac);
static bool ipToStr(char* str, size_t len, const IPAddress ip);
static ConfigCheck checkConfig(const NetworkConfig& cfg);
static bool isValidConfig(const NetworkConfig& cfg);macToStr() — serialises a MAC address to "AA:BB:CC:DD:EE:FF" format
(uppercase hex, colon-separated). Buffer must be at least MAC_STR_SIZE bytes.
macFromStr() — parses "AA:BB:CC:DD:EE:FF" format (case-insensitive).
isValidMac() — returns false if all bytes are 0x00 (unset) or all
bytes are 0xFF (broadcast).
ipToStr() — serialises an address to "a.b.c.d", or to an empty string
when it is 0.0.0.0 (unset), so a cleared optional field (gateway, DNS)
round-trips to a form as blank rather than "0.0.0.0". Returns false on
truncation.
checkConfig() — validates a whole config and returns which check failed
(ConfigCheck), or Ok. It checks the hostname, the static-addressing block
(ip / mask / gateway / DNS coherence — only under static addressing, since DHCP
leaves those fields unused) and the NTP entries. Network-free arithmetic, so it
runs before the interface is up and works on an isolated segment with no gateway.
First failure wins.
isValidConfig() — whole-config validity as a bool; a thin wrapper over
checkConfig() (checkConfig(cfg) == ConfigCheck::Ok). This is the gate
setConfig() uses, so an incoherent config can never be stored, however a UI
validated it.
Extends NetworkProfile with WiFi-specific credentials and TX power.
Platform: ESP32 and ESP8266 only. Not available on AVR.
| Constant | Value | Description |
|---|---|---|
MAX_SSID_LEN |
32 | Maximum SSID length (IEEE 802.11), excluding null terminator. Exposed as a macro, not a class constant — see note below. |
MAX_SSID_SIZE |
33 | SSID buffer size including null terminator. |
MAX_PASSWORD_LEN |
63 | Maximum WPA2-PSK password length, excluding null terminator. |
MAX_PASSWORD_SIZE |
64 | Password buffer size including null terminator. |
MIN_WIFI_TX_POWER_dBm |
−1.0 (ESP32) / 0.0 (ESP8266) | Minimum TX power in dBm. |
MAX_WIFI_TX_POWER_dBm |
19.5 (ESP32) / 20.5 (ESP8266) | Maximum TX power in dBm. |
WIFI_TX_POWER_STEP_dBm |
0.25 | Size of the radio's power unit in dBm. Not the spacing of the selectable values on ESP32 — see TX power. |
WIFI_TX_POWER_MULTIPLIER |
4.0 | Converts dBm to the platform integer unit. Used by adapters. |
WIFI_TX_POWER_LEVELS_Q[] |
see below | ESP32 only. The selectable power levels, ascending, in quarter-dBm. |
WIFI_TX_POWER_LEVEL_COUNT |
12 (ESP32) | ESP32 only. Number of entries in WIFI_TX_POWER_LEVELS_Q. |
DEFAULT_WIFI_TX_POWER_dBm |
19.5 / 20.5 | Default TX power (overridable via macro). |
MAX_SSID_LENis a macro, not a class constant. The ESP32 WiFi SDK already definesMAX_SSID_LENas a macro (= 32); a same-named class constant would collide with it. It is therefore provided as a guarded macro for a uniform API across ESP32 and ESP8266, and must be used unqualified (MAX_SSID_LEN, neverWiFiProfile::MAX_SSID_LEN).MAX_PASSWORD_LENhas no such collision and remains a normal class constant (WiFiProfile::MAX_PASSWORD_LEN).
enum class WiFiSecurity : uint8_t {
UNKNOWN = 0,
OPEN = 1,
PASSWORD = 2,
};These values are not related to actual Wi-Fi security protocols; they merely describe the basis for authentication.
struct WiFiCredentials {
char ssid [MAX_SSID_SIZE] = {};
char password[MAX_PASSWORD_SIZE] = {};
};Used with setWiFiCredentials() / getWiFiCredentials() for atomic
SSID + password update.
struct WiFiConfig : public NetworkConfig {
char ssid [MAX_SSID_SIZE] = {};
char password[MAX_PASSWORD_SIZE] = {};
float txPower = DEFAULT_WIFI_TX_POWER_dBm;
};Extends NetworkConfig with WiFi credentials and TX power. Used with
setConfig(WiFiConfig) / getConfig(WiFiConfig) /
isValidConfig(WiFiConfig&).
WiFiProfile();
explicit WiFiProfile(const NetworkConfig& cfg);
explicit WiFiProfile(const WiFiConfig& cfg);When constructing from WiFiConfig, an invalid SSID, password or TX power is
silently ignored: the SSID stays empty, the password stays empty (= open
network) and the TX power stays at DEFAULT_WIFI_TX_POWER_dBm. Base IP
configuration is always applied.
This is deliberately more forgiving than setConfig(WiFiConfig), which rejects
the whole struct and returns false if any field fails validation — a
constructor has no way to report one. Where a rejection matters, construct
empty and use setConfig(), or run checkConfig() on the struct first.
WiFiSecurity getSecurity() const;As an empty string is accepted as password and treated as an open network
(no authentication). If the first byte of the password is \\0``,the network
is an unsecured open network.
Returns the shape of the credential, never the credential itself, so a caller
can render the right controls without the passphrase leaving the profile.
WiFiSecurity::UNKNOWN is a third state on purpose: a mutex failure is not an
answer, and folding it into either WiFiSecurity::OPEN or
WiFiSecurity::PASSWORD would make one of them a lie.
bool getSsid(char* buf, size_t len) const;
bool setSsid(const char* ssid);
bool getPassword(char* buf, size_t len) const;
bool setPassword(const char* password);
bool setWiFiCredentials(const WiFiCredentials& creds);
bool getWiFiCredentials(WiFiCredentials& creds) const;All methods are thread-safe. getSsid() / getPassword() return false
on truncation or mutex failure. An empty password is accepted and treated
as an open network (no authentication).
setWiFiCredentials() / getWiFiCredentials() update both SSID and password
atomically in a single mutex acquisition.
float getTxPower() const;
bool setTxPower(float dbm);getTxPower() returns the TX power in dBm, or NAN on mutex failure.
setTxPower() rejects anything the radio cannot actually produce; it never
adjusts the value to fit.
The two platforms differ, and a caller building a UI needs to know which it is on. Both refuse a value off the 0.25 dBm grid rather than rounding it, because a silently altered transmit power is indistinguishable from one that was applied. On top of that, ESP32 offers only a dozen discrete levels and nothing in between, so being on the grid is necessary but not sufficient there. On ESP8266 the whole grid within range is available.
| Platform | Accepted values | Suitable control |
|---|---|---|
| ESP32 | The WIFI_TX_POWER_LEVEL_COUNT entries of WIFI_TX_POWER_LEVELS_Q, in dBm: −1, 2, 5, 7, 8.5, 11, 13, 15, 17, 18.5, 19, 19.5 |
A drop-down |
| ESP8266 | Any multiple of 0.25 dBm in [MIN_WIFI_TX_POWER_dBm, MAX_WIFI_TX_POWER_dBm] |
A number input with step="0.25" |
The ESP32 levels are not written out in the library; they are derived from the
Arduino core's wifi_power_t enum, so a core release that moves a level moves
this list with it.
The ESP32 radio itself does not reject an unsupported value — the IDF maps it down to the nearest level below and reports success. That silent step down is exactly what
setTxPower()exists to catch.
Usage example:
// Preferred: the level is named, so a typo is a compile error rather than a
// value the radio silently steps down from. The number is never written out,
// so a core update that moves a level moves this with it.
cfg.txPower = WIFI_POWER_13dBm * WiFiProfile::WIFI_TX_POWER_STEP_dBm;bool setConfig(const WiFiConfig& cfg);
bool getConfig(WiFiConfig& cfg) const;Extends the base setConfig() / getConfig() with SSID, password, and
TX power. All WiFi-specific fields are validated before the mutex is acquired.
static bool isValidSsid(const char* ssid);
static bool isValidPassword(const char* password);
static bool isValidTxPower(float dbm);
static ConfigCheck checkConfig(const WiFiConfig& cfg);
static bool isValidConfig(const WiFiConfig& cfg);isValidSsid() — enforces length [1, MAX_SSID_LEN]. Character content
is not restricted (SSID is a binary field per IEEE 802.11).
isValidPassword() — accepts empty string (open network). Non-empty
passwords must be 8–63 printable ASCII characters (0x20–0x7E) per WPA2-PSK.
isValidTxPower() — on ESP32, tests membership of the supported level set
(WIFI_TX_POWER_LEVELS_Q); on ESP8266, tests the range and the 0.25 dBm grid.
See TX power for why these are different questions.
checkConfig() — extends the base checkConfig() with the SSID, password
and TX-power checks (BadSsid / BadPassword / BadTxPower), then defers to it
for the rest. Returns ConfigCheck.
isValidConfig() — whole WiFi-config validity as a bool; wraps
checkConfig().
NetworkProfile::configCheckName() — names a ConfigCheck value for logs and
configuration UIs, so a caller can report which field was refused without
writing the same switch every time. The string is flash-resident, so printing it
costs no RAM on AVR:
if (!wifiProfile.setConfig(cfg)) {
Serial.print(F("rejected: "));
Serial.println(NetworkProfile::configCheckName(WiFiProfile::checkConfig(cfg)));
}It names the field, not the remedy — a UI knows better than this library how to phrase advice to its user.
NtpProfile and MqttProfile each carry their own ConfigCheck enum and their
own configCheckName(). The three are separate types, so use whichever belongs
to the profile that refused.
Extends NetworkProfile for Ethernet interfaces. No additional fields on
ESP32/ESP8266/AVR.
EthProfile();
explicit EthProfile(const NetworkConfig& cfg);The NTP servers a device synchronises from. A standalone class in this library,
like MqttProfile — it does not extend NetworkProfile.
Device-level by default: a device has one clock, so the servers belong to it and
not to whichever interface is up. Hand one to
NetworkManager::setNtpProfile() and every interface uses it.
Where interfaces genuinely reach different time servers — an internal one on a
wired LAN, a public pool over a hotspot — keep two instances and bind each to its
interface with NetworkAdapter::setNtpProfile(). An adapter's own profile wins;
unbound adapters keep using the device-level one.
Binding rather than re-aiming the device-level profile as the serving interface changes, because the manager has to know whether an interface should take DHCP's time server (option 42) before it starts that interface — lwIP settles the question during the handshake — and at that moment no event has told the application which interface is taking over.
Two objects say that plainly; a field inside each interface's configuration said it by accident, and usually said it twice with the same value.
Storage and validation only: no client, no protocol. NetworkManager applies the servers when an interface comes up.
#include <NtpProfile.h>
NtpProfile ntp;| Constant | Default | Meaning |
|---|---|---|
SERVER_COUNT |
3 (ESP32), 1 (ESP8266), 1 (AVR) | stored servers |
MUTEX_TIMEOUT |
1000 | ms a bulk operation waits for the lock (ESP32) |
NTP_PROFILE_ENABLED decides whether this class exists at all, and guards
NetworkManager's SNTP support with it — on AVR a UDP client with its own socket
and state machine, which is why NTP is off there by default. Declaring no
instance already costs nothing, so setting it to 0 is worth it only to drop the
manager's side as well.
NTP_PROFILE_SERVER_COUNT says how many servers a profile holds: 1 to 3, three
only on ESP32 by default since each costs Host::MAX_FQDN_SIZE of RAM. It is a
count rather than a switch, so zero is refused with a message naming
NTP_PROFILE_ENABLED.
struct NtpConfig {
char server[SERVER_COUNT][Host::MAX_FQDN_SIZE] = {};
};Each entry is an FQDN or an IP literal; empty means the slot is unused. Order is kept as entered, so a slot may be empty with a later one filled — the manager compacts them when handing them to SNTP.
| Value | Meaning |
|---|---|
Ok |
usable, including "no servers at all" |
BadServer |
an entry is neither a valid FQDN nor an IP literal |
bool getConfig(NtpConfig& out) const;
bool setConfig(const NtpConfig& in);
bool getServer(uint8_t index, char* out, size_t len) const;
bool setServer(const char* str, uint8_t index = 0);
bool isConfigured() const;
virtual ConfigCheck checkConfig(const NtpConfig& cfg) const;
bool saveCfg(const char* ns);
bool loadCfg(const char* ns);
bool clearCfg(const char* ns);
virtual bool toJson(char* json, size_t len) const;
bool importLegacy(const char* legacyNs);setServer() rejects an address that is neither an FQDN nor an IP literal;
passing nullptr or an empty string clears the slot. setConfig() applies the
same rule to the whole struct through checkConfig(), so which of the two a
caller reaches for does not decide whether the check happens; nothing is stored
unless the struct passes. configCheckName() turns a ConfigCheck into text.
checkConfig() takes the config as a parameter, as NetworkProfile::checkConfig()
does, so it validates a staged copy — but it is virtual rather than static, since
a static one called through a base reference silently skips a subclass's own
rules. setConfig() calls it virtually, so those rules apply there too.
Having no servers at all passes validation: that is a device told to take its time from DHCP, or from nowhere.
clearCfg() erases the whole namespace — the counterpart of saveCfg(), and
what a factory reset needs. It clears the store, not the object: the instance
keeps what it holds until something loads over it.
loadCfg() and importLegacy() do not validate what they read — a namespace
written by an older build, or edited by hand, is loaded as it stands. The
tightening is on the writing side, where a new value arrives.
importLegacy() reads servers written by a version that stored them per
interface, under ntp0..ntpN in the network profile's own namespace. Without
it an upgraded device would come back with no time servers and no indication why.
Save to the new namespace afterwards. Scheduled for removal in 1.0.0.
#include <NetworkProfileVersion.h> // pulled in by every profile header
NETWORK_PROFILE_VERSION // major * 10000 + minor * 100 + patch0.10.1 is 1001. It lets a consumer require a minimum version at compile time,
which matters because the libraries built on this one deliberately do not
declare it as a dependency: AsyncConfigPortal's network and MQTT modules are
opt-in, and a manifest entry would force the profile family on every project
that only wants the portal. With library.json unable to express the
constraint, the check has to live in the source:
#if !defined(NETWORK_PROFILE_VERSION) || NETWORK_PROFILE_VERSION < 800
# error "This module needs NetworkProfile 0.8.0 or newer"
#endifAn undefined macro evaluates to 0 in #if, so the same test also catches the
releases from before the header existed.
Stores the settings a device needs in order to reach an MQTT broker, and its
base topic. It holds no client and no connection logic: the application
connects, this only survives the reboot. The base topic is the device's own
namespace, under which the suite's convention puts state, config, status
and set/+, and from which the discovery messages take their addresses; it may
be left empty for the application's default. Everything below it is the
project's own naming.
Unlike the interface profiles, MqttProfile does not extend NetworkProfile;
it is a standalone class in the same library.
#include <MqttProfile.h>
MqttProfile mqtt;| Constant | Default | Meaning |
|---|---|---|
MAX_USER_LEN / MAX_USER_SIZE |
32 / 33 | username, excluding / including the terminator |
MAX_PASSWORD_LEN / MAX_PASSWORD_SIZE |
64 / 65 | password, likewise |
MAX_TOPIC_LEN / MAX_TOPIC_SIZE |
hostname length + 16 (79 / 80) | base topic, likewise: the default "<prefix>/<hostname>" has to fit whole |
DEFAULT_PORT |
1883 | plain MQTT |
DEFAULT_TLS_PORT |
8883 | MQTT over TLS |
MUTEX_TIMEOUT |
1000 | ms a bulk operation waits for the lock (ESP32) |
Override MQTT_PROFILE_MAX_USER_LEN, MQTT_PROFILE_MAX_PASSWORD_LEN,
MQTT_PROFILE_MAX_TOPIC_LEN and MQTT_PROFILE_MUTEX_TIMEOUT through build
flags.
struct MqttConfig {
bool enabled = false;
char host[Host::MAX_FQDN_SIZE] = {}; // FQDN or IP literal
uint16_t port = DEFAULT_PORT;
bool tls = false;
char user[MAX_USER_SIZE] = {}; // empty: anonymous
char password[MAX_PASSWORD_SIZE] = {};
bool haDiscovery = false; // publish HA discovery messages
char baseTopic[MAX_TOPIC_SIZE] = {}; // empty: the application's default
};enabled is a field of its own rather than an empty host standing in for it:
"switched off" and "not configured yet" are two different facts, and a device
taken off the broker for a while should not have to forget its address to do it.
Changing tls does not change port. The two well-known ports are offered as
constants, but guessing would overwrite a deliberate choice.
baseTopic is the device's namespace: sensors/cellar. Empty means the
application's default, which it derives from its own naming at start-up —
typically sensors/<hostname>, so that a device follows its hostname until
someone writes a topic in; a topic written in stays whatever the hostname does.
effectiveBaseTopic(cfg, def) returns the one in effect.
static bool isValidBaseTopic(const char* topic);
static const char* effectiveBaseTopic(const MqttConfig& cfg, const char* def);isValidBaseTopic() accepts an empty topic or segments of printable
characters joined by single /: no + or # (a wildcard in a publish topic is
not an error the broker reports, the device just looks silent), no leading,
trailing or doubled /, no control characters, at most MAX_TOPIC_LEN bytes.
checkConfig() applies it (BadTopic).
The topic the device last announced on is kept beside the settings, as device
state rather than a setting — not in MqttConfig, not in the backup:
bool getLastBaseTopic(char* out, size_t size) const;
bool setLastBaseTopic(const char* topic); // stored by saveCfg()Retained messages outlive the device that sent them: a base topic that changed (written in, or the hostname behind the default renamed) leaves the old one holding a state document and the discovery entries for ever. At start-up the application compares the last topic with the one in effect, clears the old one if they differ, then notes the new one.
Home Assistant's MQTT discovery is a broker-side convention: a device publishes
retained messages under a well-known prefix, and Home Assistant creates the
entities from them. Topics are still absent from this struct, and the distinction
is not a fine one — a topic is an address only a project can know, while this is
a yes/no about a convention. MqttProfile stores the flag and nothing more: it
publishes nothing, and knows nothing about Home Assistant beyond the name.
The obvious alternative is a subclass carrying the flag, which the virtual
_doSave() / _doLoad() / toJson() hooks support perfectly well — it would
share the namespace, the save, the backup and the factory reset, so nothing is
lost on the storage side. Two things decide against it.
The flag has no natural siblings. The argument for a subclass is that a second discovery convention would want a second flag, and then a third. But two booleans of which at most one may be true is a worse model than one enum, so the honest evolution is a type change here rather than a new field elsewhere:
enum class DiscoveryFormat : uint8_t { None = 0, HomeAssistant, Homie };If that day comes, _doLoad() reads the old hadisc boolean as a fallback and
derives the initial value from it. Doing it today would be premature: a
two-entry dropdown reading "None / Home Assistant" is a worse control than a
checkbox.
A subclass field is invisible to the config page. MqttConfigComponent
holds a MqttProfile& and works through getConfig(MqttConfig&) /
setConfig(const MqttConfig&), so it cannot see a derived field. Reaching one
would need a general "extra fields" hook in the component, a duplicate component
for one row, or a template — each costing more than the flag saves.
It defaults to false, and records written before the field existed load as
false too, which is what those devices were doing.
AsyncConfigPortal's MqttConfigComponent renders the matching switch when
attach() is called with withHaDiscovery, and reports a change through
Changed::haDiscovery.
| Value | Meaning |
|---|---|
Ok |
usable as it stands |
NoHost |
enabled, but no broker address |
BadHost |
not a valid FQDN or IP literal |
BadPort |
port is zero |
PasswordWithoutUser |
a password with no username to go with it |
BadTopic |
the base topic fails isValidBaseTopic() |
bool getConfig(MqttConfig& out) const;
bool setConfig(const MqttConfig& in);Both return false if the lock could not be obtained within MUTEX_TIMEOUT; on
failure getConfig() leaves out untouched and setConfig() changes nothing.
The lock matters because the struct is copied field by field: a portal saving
while a client task reads would otherwise hand out half of each.
setConfig() validates the struct through checkConfig() and stores nothing
unless it passes, so a rejected call leaves the previous settings intact. The
stage-check-keep pattern still works; it just no longer matters whether the
check was remembered. configCheckName() turns the result into text.
virtual ConfigCheck checkConfig(const MqttConfig& cfg) const;Takes the config as a parameter, like NetworkProfile::checkConfig(), so it
validates a staged copy and touches no member state. A disabled profile always
passes: its fields are not going to be used, and refusing to save a half-filled
form the user has just switched off would only be in the way.
Virtual rather than static, so a subclass's rules also run through a base reference — see Extending it.
bool saveCfg(const char* ns);
bool loadCfg(const char* ns);
bool clearCfg(const char* ns);Same shape as NetworkProfile: the namespace is the caller's, so one device can
keep several brokers' settings side by side. A record is marked complete only
after everything is written, so a save interrupted by a reset is ignored on the
next load rather than half-applied.
clearCfg() erases the whole namespace — the counterpart of saveCfg(), and
what a factory reset needs, since a device wiped of its network configuration
would otherwise still come back holding the broker's address and credentials.
An empty string removes its key instead of storing a blank, and loadCfg()
resets every field to its default before reading, so a load restores exactly
what the last save held — an emptied broker address stays empty even when
loading into an object that still has the old one.
virtual bool toJson(char* json, size_t len) const;For backup. Carries enabled, host, port, tls, user, haDiscovery and
baseTopic; not the last-announced topic, which is device state.
The password is not included — a backup file travels through mail and cloud storage, and a broker credential in it is a credential leaked. The Wi-Fi profile makes the same trade: restoring one needs the password typed again.
Returns false and empties json if the document did not fit, rather than
leaving half of one.
A project that stores one more setting subclasses rather than patching the library:
class AlarmMqttProfile : public MqttProfile {
public:
struct AlarmConfig : public MqttConfig { char alarmTopic[64] = {}; };
using MqttProfile::checkConfig; // keep the base overload visible
ConfigCheck checkConfig(const AlarmConfig& cfg) const {
if (cfg.enabled && !isValidBaseTopic(cfg.alarmTopic)) return ConfigCheck::BadTopic;
return MqttProfile::checkConfig(static_cast<const MqttConfig&>(cfg));
}
};A second, independent topic root (one shared with other devices, say) is the
kind of thing to add this way: only the project knows it. The base topic and
haDiscovery were once examples here and are now base fields — see
Why haDiscovery is a base field.
Override _doSave() / _doLoad() (calling the base first) to persist the extra
field, and toJson() to include it.
See examples/MqttConfig.
Stores the settings a device needs in order to send its log to a syslog
server: address, port, severity floor, facility. It holds no socket and frames
no message — a sender such as SyslogSender reads this; this only survives
the reboot. Like MqttProfile, it is a standalone class in the same library.
#include <SyslogProfile.h>
SyslogProfile syslogProfile;| Constant | Default | Meaning |
|---|---|---|
DEFAULT_PORT |
514 | syslog over UDP |
SEV_EMERGENCY … SEV_DEBUG |
0 … 7 | RFC 5424 severity codes |
FAC_USER, FAC_LOCAL0, FAC_LOCAL7, FAC_MAX |
1, 16, 23, 23 | RFC 5424 facility codes |
MUTEX_TIMEOUT |
1000 | ms a bulk operation waits for the lock (ESP32) |
Override SYSLOG_PROFILE_MUTEX_TIMEOUT through build flags.
struct SyslogConfig {
bool enabled = false;
char host[Host::MAX_FQDN_SIZE] = {}; // server: FQDN or IP literal
uint16_t port = DEFAULT_PORT;
uint8_t minSeverity = SEV_INFO; // send this and more severe
uint8_t facility = FAC_LOCAL0;
};enabled is separate from an empty host on purpose: "switched off" and "not
configured yet" are two different facts. minSeverity is an RFC 5424 code, so
a smaller number means more severe; the default forwards errors, warnings
and milestones and keeps debug output on the console.
Ok, NoHost, BadHost, BadPort, BadSeverity (not 0..7),
BadFacility (not 0..23). configCheckName() names a value; severityName()
names a severity code, for UIs.
| Method | Meaning |
|---|---|
bool getConfig(SyslogConfig&) const |
copies the settings out, locked |
bool setConfig(const SyslogConfig&) |
replaces them; refused unless checkConfig() passes |
ConfigCheck checkConfig(const SyslogConfig&) const |
virtual; a disabled profile always passes |
bool saveCfg(const char* ns) / loadCfg(ns) / clearCfg(ns) |
Preferences persistence, as the other profiles |
bool toJson(char*, size_t) const |
every field, for backup — nothing here is a secret |
checkConfig(), isValidConfig() and setConfig() refuse a subclass's config
at compile time:
WiFiProfile::WiFiConfig cfg;
NetworkProfile& p = wifiProfile;
p.checkConfig(cfg); // error: use of deleted functionThe reason is that the base versions are static: the compiler picks one by the
declared type at the call site, so this used to check the addressing, skip the
SSID, password and TX-power rules, and report Ok. Deleted derived-type
overloads turn that silent half-check into an error.
Two ways forward:
WiFiProfile::checkConfig(cfg); // what was almost always meant
NetworkProfile::checkConfig(static_cast<const NetworkProfile::NetworkConfig&>(cfg));the second saying explicitly that only the addressing is of interest.
Copying a subclass config into a NetworkConfig variable first still loses the
extra fields — by then they are gone, and no signature can catch it.