[{"data":1,"prerenderedAt":3401},["ShallowReactive",2],{"blog-post-en-nanomq-on-zephyr-rtos":3},{"post":4,"translationPath":3400},{"id":5,"title":6,"author":7,"body":8,"cover":3385,"coverHeight":3386,"coverWidth":3387,"date":3388,"description":3389,"draft":3390,"extension":3391,"meta":3392,"navigation":3393,"path":3394,"pinned":3393,"seo":3395,"sitemap":3396,"stem":3397,"tags":3398,"__hash__":3399},"blog_en\u002Fblog\u002Fnanomq-on-zephyr-rtos.md","NanoMQ on Zephyr RTOS: Running an MQTT Broker Directly on an MCU",null,{"type":9,"value":10,"toc":3340},"minimark",[11,16,24,27,45,57,60,64,67,70,105,112,116,121,126,129,161,165,168,182,185,189,193,196,256,259,333,343,349,353,372,521,525,531,547,553,559,586,592,596,609,616,633,644,651,657,663,674,680,684,690,761,802,806,813,886,892,910,914,918,933,939,946,952,956,962,968,971,977,980,1011,1015,1018,1032,1039,1045,1048,1054,1069,1075,1081,1085,1098,1104,1107,1113,1129,1138,1142,1148,1154,1157,1163,1166,1172,1178,1181,1224,1227,1264,1277,1281,1285,1291,1297,1300,1306,1309,1329,1333,1336,1342,1358,1361,1367,1377,1383,1389,1395,1401,1420,1426,1430,1433,1439,1442,1456,1462,1466,1483,1491,1495,1498,1504,1521,1527,1533,1540,1546,1549,1555,1562,1568,1580,1586,1596,1599,1726,1733,1737,1751,1755,1762,1869,1915,1919,1937,1942,2292,2297,2499,2532,2564,2611,2615,2704,2708,2711,2716,2719,2734,2740,2754,2762,2765,2773,2780,2794,2800,2819,2826,2832,2836,2839,2843,2849,2870,2874,2896,2904,2908,2915,2927,2948,2965,3123,3137,3154,3173,3182,3198,3222,3226,3325,3328,3334,3337],[12,13,15],"h2",{"id":14},"introduction","Introduction",[17,18,19,20],"p",{},"MQTT and Zephyr are most often used together on the client side: a Zephyr-based device acts as an MQTT client and connects to a remote broker. ",[21,22,23],"strong",{},"But what if the broker itself could run directly on the MCU?",[17,25,26],{},"This approach can be useful in edge scenarios where a dedicated Linux gateway adds unnecessary complexity. For example, dozens of field devices may communicate over Modbus, serial interfaces, or proprietary protocols and need to be consolidated into MQTT. In other cases, an unreliable network may require data to be buffered locally and uploaded after the connection is restored. The conventional solution is to deploy a Linux gateway alongside the devices, but the additional power, maintenance, and physical protection can be costly. Running the broker directly on an MCU can eliminate this extra layer.",[17,28,29,34,35,39,40,44],{},[30,31,33],"a",{"href":32},"\u002F","NanoMQ"," is an open-source edge MQTT broker from EMQ, originally designed for Linux and other POSIX environments. This article explores what it takes to run the NanoMQ broker core on Zephyr RTOS. The application layer uses the same ",[36,37,38],"code",{},"nanomq\u002F"," source code and NanoNNG MQTT protocol stack without feature trimming. Peripheral capabilities are disabled or remain unverified where required by the constraints of the Zephyr platform; see ",[30,41,43],{"href":42},"#appendix-a-differences-from-general-nanomq","Appendix A"," for the complete list of differences and limitations.",[17,46,47,48,51,52,56],{},"The repository includes two demos: one running on real ESP32-S3 hardware and the other in a ",[36,49,50],{},"qemu_x86"," simulation environment. See ",[30,53,55],{"href":54},"#appendix-b-comparing-the-two-demos","Appendix B"," for a comparison of the two demos and guidance on choosing between them.",[17,58,59],{},"The rest of this article walks through the environment setup, build and deployment process, functional testing, and the porting process itself, including the framework-level issues encountered along the way.",[12,61,63],{"id":62},"about-zephyr","About Zephyr",[17,65,66],{},"Zephyr is an open-source real-time operating system (RTOS) hosted by the Linux Foundation and designed for resource-constrained embedded devices.",[17,68,69],{},"Four Zephyr features are particularly relevant to this port:",[71,72,73,80,89,95],"ul",{},[74,75,76,79],"li",{},[21,77,78],{},"Configurable and scalable."," System features are enabled through Kconfig options, allowing a minimal configuration to fit into tens of kilobytes. This makes the same codebase suitable for devices ranging from simple sensor nodes to edge gateways.",[74,81,82,85,86,88],{},[21,83,84],{},"Hardware described through devicetree."," Board-specific differences are defined through devicetree and board overlays, keeping application code independent of the underlying hardware. This is what allows the port to build the same application source for both ",[36,87,50],{}," and ESP32-S3.",[74,90,91,94],{},[21,92,93],{},"Built-in networking stack."," Zephyr provides TCP\u002FIP, a BSD-compatible socket layer, and TLS. The NanoNNG Zephyr platform adaptation layer is built on top of this socket API.",[74,96,97,100,101,104],{},[21,98,99],{},"POSIX API subset."," Zephyr provides ",[36,102,103],{},"CONFIG_POSIX_API"," support for interfaces such as pthreads and file descriptors, making it possible to port middleware originally designed for POSIX environments.",[17,106,107,108,111],{},"This article is based on Zephyr 4.4 (",[36,109,110],{},"11a87708d41",").",[12,113,115],{"id":114},"about-nanomq","About NanoMQ",[17,117,118,120],{},[30,119,33],{"href":32}," is an open-source MQTT broker from EMQ and a project under LF Edge. It is designed as an ultra-lightweight, high-performance MQTT broker for IoT edge environments. Unlike EMQX, which targets cloud deployments, NanoMQ is designed for embedded systems and industrial field sites.",[122,123,125],"h3",{"id":124},"architecture","Architecture",[17,127,128],{},"NanoMQ uses a layered architecture:",[71,130,131,137,143,149,155],{},[74,132,133,136],{},[21,134,135],{},"Platform adaptation layer:"," Detects the underlying hardware and operating system and provides a portable API to the layers above, avoiding platform-specific dependencies. This is the foundation for adding Zephyr support through a new platform adaptation layer.",[74,138,139,142],{},[21,140,141],{},"Task layer:"," Uses an Actor model and thread-level parallelism, with support for scaling across SMP systems.",[74,144,145,148],{},[21,146,147],{},"Transport layer:"," Manages TCP and UDP streams through pipes and clients, using zero-copy mechanisms to reduce memory usage.",[74,150,151,154],{},[21,152,153],{},"Protocol layer:"," Parses byte streams into MQTT packets, generates events, and manages the in-flight window.",[74,156,157,160],{},[21,158,159],{},"Application layer:"," Integrates the rule engine and global topic tree, exposing MQTT messages and events through a lock-free state machine.",[122,162,164],{"id":163},"implementation","Implementation",[17,166,167],{},"NanoMQ is implemented entirely in C and has no external runtime dependencies, making it suitable for cross-compilation to a wide range of target boards.",[17,169,170,171,177,178,181],{},"Its asynchronous I\u002FO and Actor architecture are built on the NNG fork (",[30,172,176],{"href":173,"rel":174},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002FNanoNNG",[175],"nofollow","NanoNNG",") maintained by the NanoMQ team, which is extended with MQTT protocol support and the ",[36,179,180],{},"nanolib"," utility library.",[17,183,184],{},"At the protocol level, NanoMQ fully supports MQTT 3.1.1, MQTT 3.1, and MQTT 5.0, including QoS 0\u002F1\u002F2, retained messages, Last Will messages, shared subscriptions, topic aliases, and other MQTT features.",[12,186,188],{"id":187},"environment-setup","Environment Setup",[122,190,192],{"id":191},"prerequisites","Prerequisites",[17,194,195],{},"You will need the following:",[71,197,198,236,243,253],{},[74,199,200,201,210,211,214,215,218,219,221,222,225,226],{},"A Zephyr 4.4+ west workspace, together with its Python virtual environment. ",[21,202,203,204,209],{},"Both ",[21,205,206],{},[36,207,208],{},"west"," itself and Zephyr's Python dependencies are installed in this venv."," Its location is up to you: the official Zephyr documentation uses ",[36,212,213],{},".venv\u002F"," at the workspace root, while the environment used for validation in this article uses ",[36,216,217],{},"~\u002Fzephyr-venv",". Once activated, ",[36,220,208],{}," is on ",[36,223,224],{},"PATH",":",[227,228,234],"pre",{"className":229,"code":231,"language":232,"meta":233},[230],"language-bash","source \u003Cvenv>\u002Fbin\u002Factivate      # ~\u002Fzephyr-venv in this article\nexport ZEPHYR_SDK_INSTALL_DIR=$HOME\u002Fzephyr-sdk-1.0.1\n","bash","",[36,235,231],{"__ignoreMap":233},[74,237,238,239,242],{},"Zephyr SDK 1.0.x. Download and extract the SDK separately, then set ",[36,240,241],{},"ZEPHYR_SDK_INSTALL_DIR"," to its installation directory.",[74,244,245,246,249,250,252],{},"For the ESP32-S3 hardware demo, an ",[21,247,248],{},"ESP-IDF"," environment (see the next section). This is not required if you only build ",[36,251,50],{},".",[74,254,255],{},"For functional testing, additional Python dependencies and the Mosquitto client, as described in the Functional Testing section.",[17,257,258],{},"The versions used for validation are fixed as follows. The e1000 patch is specific to the corresponding Zephyr source revision and must be revalidated if you upgrade Zephyr.",[260,261,262,275],"table",{},[263,264,265],"thead",{},[266,267,268,272],"tr",{},[269,270,271],"th",{},"Component",[269,273,274],{},"Version \u002F Commit",[276,277,278,290,298,309],"tbody",{},[266,279,280,284],{},[281,282,283],"td",{},"Zephyr",[281,285,286,287,289],{},"4.4, ",[36,288,110],{}," (the e1000 patch applies to this revision)",[266,291,292,295],{},[281,293,294],{},"Zephyr SDK",[281,296,297],{},"1.0.1",[266,299,300,302],{},[281,301,33],{},[281,303,304,305,308],{},"0.25.6 (based on upstream ",[36,306,307],{},"master",")",[266,310,311,318],{},[281,312,313,314,317],{},"NanoNNG (",[36,315,316],{},"nng\u002F"," submodule)",[281,319,320,321,324,325,328,329,332],{},"Based on upstream ",[36,322,323],{},"main",", plus the Zephyr platform adaptation layer. The ",[36,326,327],{},"zephyr-rtos"," branch commits the submodule gitlink, so ",[36,330,331],{},"git clone --recursive"," checks out the complete platform adaptation layer",[17,334,335,336,338,339,342],{},"If you have not created a Zephyr workspace yet (the commands below put the venv at ",[36,337,217],{},", which is where the validation environment for this article keeps it; putting it elsewhere, or at ",[36,340,341],{},"\u003Cworkspace>\u002F.venv"," as the official documentation suggests, works just as well):",[227,344,347],{"className":345,"code":346,"language":232,"meta":233},[230],"python3 -m venv ~\u002Fzephyr-venv\nsource ~\u002Fzephyr-venv\u002Fbin\u002Factivate\npip install west\n\nwest init ~\u002Fzephyrproject\ncd ~\u002Fzephyrproject\nwest update\npip install -r zephyr\u002Fscripts\u002Frequirements.txt\n",[36,348,346],{"__ignoreMap":233},[122,350,352],{"id":351},"esp32-s3-development-environment","ESP32-S3 Development Environment",[17,354,355,363,364,367,368,371],{},[21,356,357,358,362],{},"This section applies only to the ESP32-S3 hardware demo. If you only build ",[21,359,360],{},[36,361,50],{},", skip it and use the standard Zephyr environment from the previous section."," The hardware demo needs Espressif's HAL blobs and the flashing and serial tools, but ",[21,365,366],{},"the cross-compiler still comes from the Zephyr SDK",": 1.0.1 already includes the ",[36,369,370],{},"xtensa-espressif_esp32s3_zephyr-elf"," toolchain required by this target, and ESP-IDF plays no part in compilation.",[373,374,375,398,486],"ol",{},[74,376,377,380,381,383,384,387,388,391,392],{},[21,378,379],{},"Get ESP-IDF"," (it provides ",[36,382,208],{},", ",[36,385,386],{},"esptool",", and ",[36,389,390],{},"idf-monitor","). Either installation method works:",[227,393,396],{"className":394,"code":395,"language":232,"meta":233},[230],"# a) Clone from git\ngit clone --recursive https:\u002F\u002Fgithub.com\u002Fespressif\u002Fesp-idf.git ~\u002Fesp\u002Fesp-idf\ncd ~\u002Fesp\u002Fesp-idf && .\u002Finstall.sh esp32s3\n#    the activation script is ~\u002Fesp\u002Fesp-idf\u002Fexport.sh\n\n# b) Install with the ESP-IDF Installation Manager (eim)\n#    the activation script is ~\u002F.espressif\u002Ftools\u002Factivate_idf_v\u003Cversion>.sh\n",[36,397,395],{"__ignoreMap":233},[74,399,400,403,404,406,407,410,411,225,414,420,422,423,426,427,433,435,438,439,441,442,445,446,448,449,451,452,383,454,456,457,460,461,463,464,466,467,469,470,473,474,477,478,481,482,485],{},[21,401,402],{},"Activate the environment before each build"," and point ",[36,405,241],{}," to the Zephyr SDK.",[408,409],"br",{},"The validation environment for this article turns this into an alias in ",[36,412,413],{},"~\u002F.bashrc",[227,415,418],{"className":416,"code":417,"language":232,"meta":233},[230],"alias esp-zephyr='source ~\u002F.espressif\u002Ftools\u002Factivate_idf_v6.1.sh \\\n    && export ZEPHYR_SDK_INSTALL_DIR=$HOME\u002Fzephyr-sdk-1.0.1'\n",[36,419,417],{"__ignoreMap":233},[408,421],{},"Run ",[36,424,425],{},"esp-zephyr"," before each build. Expanded, it is:",[227,428,431],{"className":429,"code":430,"language":232,"meta":233},[230],"source ~\u002F.espressif\u002Ftools\u002Factivate_idf_v6.1.sh   # ~\u002Fesp\u002Fesp-idf\u002Fexport.sh if installed from git\nexport ZEPHYR_SDK_INSTALL_DIR=$HOME\u002Fzephyr-sdk-1.0.1\nexport ZEPHYR_TOOLCHAIN_VARIANT=zephyr\n",[36,432,430],{"__ignoreMap":233},[408,434],{},[21,436,437],{},"Activating this environment replaces the Zephyr venv from the previous section",": ESP-IDF's Python environment ships its own ",[36,440,208],{}," (and ",[36,443,444],{},"python3","), and activating one of the two is enough — whichever is activated last wins. Use this one to build for ESP32 targets; use the Zephyr venv from the previous section to build for ",[36,447,50],{}," only, without installing ESP-IDF.",[408,450],{},"Activating ESP-IDF is independent of which compiler is used. It only puts ",[36,453,208],{},[36,455,386],{}," (which the ESP32 runner of ",[36,458,459],{},"west flash"," invokes), and ",[36,462,390],{}," on ",[36,465,224],{},". The compiler is selected by ",[36,468,241],{}," and ",[36,471,472],{},"ZEPHYR_TOOLCHAIN_VARIANT=zephyr",", the latter pointing to the matching Xtensa toolchain in the Zephyr SDK. ",[21,475,476],{},"Do not"," set it to ",[36,479,480],{},"espressif",": that variant has been removed from the current Zephyr release and causes CMake to look for files that do not exist in the source tree. You can also omit ",[36,483,484],{},"ZEPHYR_TOOLCHAIN_VARIANT"," entirely, since Zephyr locates the Zephyr SDK on its own, but setting it explicitly is less error-prone.",[74,487,488,491,492,498,500,503,504,507,508,511,512,514,515,520],{},[21,489,490],{},"Fetch the Espressif HAL blobs"," once from the workspace:",[227,493,496],{"className":494,"code":495,"language":232,"meta":233},[230],"cd ~\u002Fzephyrproject\nwest blobs fetch hal_espressif\n",[36,497,495],{"__ignoreMap":233},[408,499],{},[21,501,502],{},"Do not skip this step."," Without the blobs, ",[36,505,506],{},"CONFIG_WIFI_ESP32"," silently becomes unavailable. The build still succeeds and the firmware still boots, but Wi-Fi is not available, and the build system reports no warning. On the serial console, the expected ",[36,509,510],{},"wifi: connected"," message will never appear.",[408,513],{},"For distribution-specific setup instructions (including system dependencies, SDK installation, permissions, and common errors), see ",[30,516,519],{"href":517,"rel":518},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002FNanoMQ-Zephyr-Example\u002Fblob\u002Fmain\u002Fexamples\u002Fnanomq_zephyr_esp32s3\u002Fsetup-fedora-en.md",[175],"setup-fedora-en.md",". The guide uses Fedora as an example; for other distributions, replace the package manager commands as needed.",[122,522,524],{"id":523},"get-the-source-code","Get the Source Code",[227,526,529],{"className":527,"code":528,"language":232,"meta":233},[230],"cd ~\u002Fzephyrproject\ngit clone --recursive -b zephyr-rtos https:\u002F\u002Fgithub.com\u002Fnanomq\u002Fnanomq.git\ncd nanomq\n",[36,530,528],{"__ignoreMap":233},[17,532,533,534,537,538,540,541,543,544,546],{},"The ",[36,535,536],{},"--recursive"," option also fetches the ",[36,539,316],{}," submodule, which contains the NanoNNG branch with the Zephyr platform adaptation layer. The ",[36,542,327],{}," branch commits the submodule's gitlink in the repository, so the checkout matches the commit listed in the table above. ",[36,545,331],{}," always checks out the gitlink recorded by the repository; switching commits requires an explicit checkout.",[17,548,549,550,552],{},"If you cloned the repository without ",[36,551,536],{},", initialize the submodules with:",[227,554,557],{"className":555,"code":556,"language":232,"meta":233},[230],"git submodule update --init --recursive\n",[36,558,556],{"__ignoreMap":233},[17,560,561,562,565,566,569,570,569,573,569,576,579,580,582,583,585],{},"After cloning, verify the submodule with ",[36,563,564],{},"git submodule status nng",": if the output ",[21,567,568],{},"does not start with"," ",[36,571,572],{},"-",[21,574,575],{},"or",[36,577,578],{},"+",", the submodule has been checked out at the gitlink recorded by the repository (",[36,581,572],{}," means it has not been initialized, ",[36,584,578],{}," means the checked-out commit differs from the recorded one).",[17,587,588,589,111],{},"All subsequent commands in this article are run from the repository root (",[36,590,591],{},"~\u002Fzephyrproject\u002Fnanomq",[122,593,595],{"id":594},"qemu_x86-zephyr-e1000-driver-patch","qemu_x86: Zephyr e1000 Driver Patch",[17,597,533,598,600,601,604,605,608],{},[36,599,50],{}," demo requires a two-line patch to the Zephyr e1000 driver. ",[21,602,603],{},"This patch is required."," Without it, the broker starts normally and prints ",[36,606,607],{},"NanoMQ Broker is started successfully!",". However, no client can establish a connection.",[17,610,611,612,615],{},"The issue is caused by the QEMU e1000 device model clearing the RCTL register during reset, including ",[21,613,614],{},"RCTL_BAM"," (Broadcast Accept Mode, bit 15) — real hardware sets this bit by default, but the device model does not.",[17,617,618,619,622,623,626,627,630,631,111],{},"Zephyr's ",[36,620,621],{},"eth_e1000"," driver writes only ",[36,624,625],{},"RCTL_EN | RCTL_MPE"," and never sets BAM. As a result, ",[21,628,629],{},"all broadcast frames are silently dropped by the device model",". ARP resolution therefore fails, preventing SLIRP from delivering the initial TCP connection. This issue remains present in upstream Zephyr as of 4.4 (",[36,632,110],{},[17,634,635,636,639,640,643],{},"The patch must be applied to the ",[21,637,638],{},"Zephyr source tree itself"," (the ",[36,641,642],{},"zephyr\u002F"," directory in the workspace, not this repository).",[17,645,646,647,650],{},"Save the following diff as ",[36,648,649],{},"zephyr-e1000-bam.patch"," and apply it with:",[227,652,655],{"className":653,"code":654,"language":232,"meta":233},[230],"cd ~\u002Fzephyrproject\u002Fzephyr\ngit apply \u002Fpath\u002Fto\u002Fzephyr-e1000-bam.patch\n",[36,656,654],{"__ignoreMap":233},[227,658,661],{"className":659,"code":660,"language":232,"meta":233},[230],"diff --git a\u002Fdrivers\u002Fethernet\u002Feth_e1000.c b\u002Fdrivers\u002Fethernet\u002Feth_e1000.c\n--- a\u002Fdrivers\u002Fethernet\u002Feth_e1000.c\n+++ b\u002Fdrivers\u002Fethernet\u002Feth_e1000.c\n@@ -329,7 +329,7 @@ static const struct ethernet_api e1000_api = {\n                                    \\\n        irq_enable(DT_INST_IRQN(inst));             \\\n        iow32(dev, CTRL, CTRL_SLU); \u002F* Set link up *\u002F       \\\n-       iow32(dev, RCTL, RCTL_EN | RCTL_MPE | DT_INST_PROP(inst, rdmts) \u003C\u003C RDMTS_OFFSET); \\\n+       iow32(dev, RCTL, RCTL_EN | RCTL_MPE | RCTL_BAM | DT_INST_PROP(inst, rdmts) \u003C\u003C RDMTS_OFFSET); \\\n        iow32(dev, ITR, DT_INST_PROP(inst, itr) & (uint32_t)GENMASK(15, 0)); \\\n    }                               \\\n                                    \\\ndiff --git a\u002Fdrivers\u002Fethernet\u002Feth_e1000_priv.h b\u002Fdrivers\u002Fethernet\u002Feth_e1000_priv.h\n--- a\u002Fdrivers\u002Fethernet\u002Feth_e1000_priv.h\n+++ b\u002Fdrivers\u002Fethernet\u002Feth_e1000_priv.h\n@@ -27,6 +27,7 @@ extern \"C\" {\n #define IMS_RXT0   (1 \u003C\u003C 7) \u002F* Receiver Timer *\u002F\n\n #define RCTL_MPE   (1 \u003C\u003C 4) \u002F* Multicast Promiscuous Enabled *\u002F\n+#define RCTL_BAM   (1 \u003C\u003C 15) \u002F* Broadcast Accept Mode *\u002F\n\n #define TDESC_EOP       (1) \u002F* End Of Packet *\u002F\n #define TDESC_RS   (1 \u003C\u003C 3) \u002F* Report Status *\u002F\n",[36,662,660],{"__ignoreMap":233},[17,664,665,666,669,670,673],{},"Note that ",[36,667,668],{},"RCTL_MPE"," means ",[21,671,672],{},"Multicast"," Promiscuous Enabled (bit 4) and only affects multicast reception. It does not enable broadcast reception, so the driver's existing setting cannot substitute for BAM. This distinction is why the issue went unnoticed for so long.",[17,675,676,677],{},"This patch only affects the emulated QEMU network adapter. ",[21,678,679],{},"It is not required for the ESP32-S3 hardware demo.",[122,681,683],{"id":682},"ports-and-services","Ports and Services",[17,685,686,687,689],{},"With the configurations validated in this article, both demos expose the same MQTT, REST, and WebSocket ports. For ",[36,688,50],{},", actual reachability depends on the port-forwarding configuration described in the next section.",[260,691,692,710],{},[263,693,694],{},[266,695,696,699,702,705,707],{},[269,697,698],{},"Service",[269,700,701],{},"Port",[269,703,704],{},"ESP32-S3",[269,706,50],{},[269,708,709],{},"Authentication \u002F Encryption",[276,711,712,728,743],{},[266,713,714,717,720,723,725],{},[281,715,716],{},"MQTT over TCP",[281,718,719],{},"1883",[281,721,722],{},"Supported",[281,724,722],{},[281,726,727],{},"No authentication, no TLS",[266,729,730,733,736,738,740],{},[281,731,732],{},"REST API",[281,734,735],{},"8081",[281,737,722],{},[281,739,722],{},[281,741,742],{},"Basic authentication; credentials must be configured by you (there is no usable default), no TLS",[266,744,745,748,751,753,755],{},[281,746,747],{},"MQTT over WebSocket",[281,749,750],{},"8083",[281,752,722],{},[281,754,722],{},[281,756,757,758],{},"No authentication, no TLS; path ",[36,759,760],{},"\u002Fmqtt",[17,762,763,764,767,768,771,772,469,775,778,779,782,783,786,787,790,791,794,795,790,798,801],{},"The REST listener is enabled through the Kconfig option ",[36,765,766],{},"CONFIG_BROKER_REST_API",": ",[36,769,770],{},"main.c"," uses it to set ",[36,773,774],{},"http_server.enable",[36,776,777],{},"auth_type",", while the default value in ",[36,780,781],{},"conf_init"," is disabled. ",[21,784,785],{},"This switch alone is not enough, however"," — ",[36,788,789],{},"CONFIG_BROKER_REST_USER"," \u002F ",[36,792,793],{},"CONFIG_BROKER_REST_PASS"," both default to the empty string, which is to say there are no usable default credentials, and the publicly known upstream credentials ",[36,796,797],{},"admin",[36,799,800],{},"public"," are rejected as well. Only when both are set to credentials of your own does anything listen on 8081; otherwise the broker prints an explanatory line at startup and leaves the port closed. If you remove this switch from the configuration yourself, nothing will listen on port 8081 either, and clients will receive an RST. That is expected behavior rather than a failure.",[122,803,805],{"id":804},"security-considerations","Security Considerations",[17,807,808,809,812],{},"The default configuration of both demos assumes a ",[21,810,811],{},"controlled experimental environment",". Deploying it unchanged introduces security risks.",[260,814,815,825],{},[263,816,817],{},[266,818,819,822],{},[269,820,821],{},"Item",[269,823,824],{},"Current Configuration",[276,826,827,847,870,878],{},[266,828,829,832],{},[281,830,831],{},"Listening addresses",[281,833,834,835,838,839,842,843,846],{},"MQTT ",[36,836,837],{},"nmq-tcp:\u002F\u002F0.0.0.0:1883",", WebSocket ",[36,840,841],{},"nmq-ws:\u002F\u002F0.0.0.0:8083\u002Fmqtt",", and REST ",[36,844,845],{},"0.0.0.0:8081"," all listen on all network interfaces",[266,848,849,852],{},[281,850,851],{},"REST authentication",[281,853,854,855,790,857,860,861,864,865,790,867,869],{},"Basic authentication; credentials come from the Kconfig options ",[36,856,789],{},[36,858,859],{},"_PASS",", both of which ",[21,862,863],{},"default to the empty string (there are no usable default credentials)"," — when they are unset, or still set to the publicly known upstream ",[36,866,797],{},[36,868,800],{},", nothing listens on 8081",[266,871,872,875],{},[281,873,874],{},"MQTT authentication",[281,876,877],{},"Disabled; any client can connect, publish, and subscribe",[266,879,880,883],{},[281,881,882],{},"Transport encryption",[281,884,885],{},"None. TLS is not enabled in the NanoNNG Zephyr port, so MQTT, WebSocket, and REST traffic is sent in plaintext",[17,887,888,891],{},[21,889,890],{},"Basic authentication over plaintext HTTP is only Base64 encoding, not encryption."," Credentials can therefore be recovered by an observer on the same network path. It can prevent accidental access, but not a deliberate attack. Real access control still depends on the network boundary.",[17,893,894,895,790,897,899,900,903,904,906,907],{},"Before using the demo outside an experimental network, at minimum: restrict the listening addresses to the specific network interfaces you need, set your own REST credentials (the published ",[36,896,797],{},[36,898,800],{}," pair is now refused), and place the management interface behind a trusted network or an additional TLS proxy. As for MQTT authentication: if the authentication path of this port has been verified, it can be enabled directly; otherwise, enforce access control at the link layer or at a TLS proxy. ",[21,901,902],{},"Do not assume that the authentication settings of the general NanoMQ distribution can be applied directly to the embedded configuration used by these demos"," (the configuration sources differ; see ",[30,905,43],{"href":42},"). ",[21,908,909],{},"Do not expose the demo's default configuration to the public Internet or an untrusted network.",[12,911,913],{"id":912},"build-and-run-esp32-s3-hardware-demo","Build and Run: ESP32-S3 Hardware Demo",[122,915,917],{"id":916},"_1-configure-local-credentials-wi-fi-and-rest","1. Configure Local Credentials (Wi-Fi and REST)",[17,919,920,921,924,925,928,929,932],{},"Copy ",[36,922,923],{},"local.conf.example"," to ",[36,926,927],{},"local.conf",". The file is already listed in ",[36,930,931],{},".gitignore",", so your credentials will not be committed to the repository.",[227,934,937],{"className":935,"code":936,"language":232,"meta":233},[230],"cp demo\u002Fnanomq_zephyr_esp32s3\u002Flocal.conf.example \\\n   demo\u002Fnanomq_zephyr_esp32s3\u002Flocal.conf\n",[36,938,936],{"__ignoreMap":233},[17,940,941,942,945],{},"Edit ",[36,943,944],{},"demo\u002Fnanomq_zephyr_esp32s3\u002Flocal.conf"," and fill in the SSID and PSK:",[227,947,950],{"className":948,"code":949,"language":232,"meta":233},[230],"CONFIG_BROKER_WIFI_SSID=\"your-ssid\"\nCONFIG_BROKER_WIFI_PSK=\"your-passphrase\"\n\n# prj.conf already enables CONFIG_BROKER_REST_API, but 8081 only starts\n# listening once these two are set (and are not the public admin\u002Fpublic);\n# leaving them unset means the port stays closed.\nCONFIG_BROKER_REST_USER=\"your-rest-user\"\nCONFIG_BROKER_REST_PASS=\"your-rest-passphrase\"\n",[36,951,949],{"__ignoreMap":233},[122,953,955],{"id":954},"_2-build","2. Build",[17,957,958,961],{},[36,959,960],{},"EXTRA_CONF_FILE"," is resolved relative to the application directory:",[227,963,966],{"className":964,"code":965,"language":232,"meta":233},[230],"west build -b esp32s3_devkitc\u002Fesp32s3\u002Fprocpu -d build\u002Fesp32s3_nanomq \\\n    demo\u002Fnanomq_zephyr_esp32s3 -- -DEXTRA_CONF_FILE=local.conf\n",[36,967,965],{"__ignoreMap":233},[17,969,970],{},"For the configuration validated in this article, with MQTT, REST, and WebSocket enabled, a successful build produces the following memory report:",[227,972,975],{"className":973,"code":974,"language":232,"meta":233},[230],"Memory region         Used Size  Region Size  %age Used\n           FLASH:      961204 B   16776960 B      5.73%\n     iram0_0_seg:       54028 B     415492 B     13.00%\n     dram0_0_seg:      379640 B     399108 B     95.12%\n     irom0_0_seg:      686320 B        32 MB      2.05%\n     drom0_0_seg:      830132 B        32 MB      2.47%\n    ext_dram_seg:     5152752 B        32 MB     15.36%\n...\nSuccessfully created ESP32-S3 image.\n",[36,976,974],{"__ignoreMap":233},[17,978,979],{},"Two figures are particularly important here:",[71,981,982,992],{},[74,983,984,987,988,991],{},[36,985,986],{},"dram0_0_seg",", which represents internal SRAM, is already ",[21,989,990],{},"95.12% utilized",". This is the most constrained memory region and is why the broker heap and static pools are moved to PSRAM;",[74,993,994,997,998,1001,1002,1005,1006,1010],{},[36,995,996],{},"ext_dram_seg"," reports ",[21,999,1000],{},"5,152,752 B (≈5.2 MB)",", which is the amount ",[21,1003,1004],{},"placed in external PSRAM",", or 15.36% of that region. The relationship between this figure, the region's 32 MB size, and the module's 16 MB capacity is explained in ",[30,1007,1009],{"href":1008},"#about-the-development-board","About the Development Board"," below.",[122,1012,1014],{"id":1013},"_3-flash-the-firmware","3. Flash the Firmware",[17,1016,1017],{},"Connect the development board to the host computer over USB. Depending on whether the board uses an onboard USB-UART bridge or the ESP32-S3's native USB-JTAG\u002FSerial interface, the device may appear as either:",[71,1019,1020,1026],{},[74,1021,1022,1025],{},[36,1023,1024],{},"\u002Fdev\u002FttyUSB*"," — a separate USB-UART bridge such as CP2102 or CH34x",[74,1027,1028,1031],{},[36,1029,1030],{},"\u002Fdev\u002FttyACM*"," — native USB CDC, including the ESP32-S3 USB-JTAG\u002FSerial interface",[17,1033,1034,1035,1038],{},"This article uses ",[36,1036,1037],{},"\u002Fdev\u002FttyUSB0"," as an example. Check the actual device name on your system:",[227,1040,1043],{"className":1041,"code":1042,"language":232,"meta":233},[230],"ls \u002Fdev\u002FttyUSB* \u002Fdev\u002FttyACM*\n# Or check which device node appears when you connect the board\ndmesg | tail -5\n",[36,1044,1042],{"__ignoreMap":233},[17,1046,1047],{},"If flashing fails because of insufficient permissions, add the current user to the group that owns the device, then log in again for the change to take effect:",[227,1049,1052],{"className":1050,"code":1051,"language":232,"meta":233},[230],"ls -l \u002Fdev\u002FttyUSB0                 # The fourth column shows the group\nsudo usermod -aG \u003Cgroup> \"$USER\"\n",[36,1053,1051],{"__ignoreMap":233},[17,1055,1056,1057,1060,1061,1064,1065,1068],{},"The group is typically ",[36,1058,1059],{},"dialout"," on Debian\u002FUbuntu and Fedora, and ",[36,1062,1063],{},"uucp"," on Arch. Always use the group reported by ",[36,1066,1067],{},"ls -l"," rather than assuming the distribution-specific default.",[17,1070,1071,1072,1074],{},"Then flash the firmware, replacing ",[36,1073,1037],{}," with the actual device name:",[227,1076,1079],{"className":1077,"code":1078,"language":232,"meta":233},[230],"west flash -d build\u002Fesp32s3_nanomq --runner esp32 --esp-device \u002Fdev\u002FttyUSB0\n",[36,1080,1078],{"__ignoreMap":233},[122,1082,1084],{"id":1083},"_4-monitor-the-serial-console","4. Monitor the Serial Console",[17,1086,1087,1088,1090,1091,1094,1095,1097],{},"The baud rate is 115200. You can use ",[36,1089,390],{}," or ",[36,1092,1093],{},"miniterm",". Passing the ELF file to ",[36,1096,390],{}," enables binary log decoding:",[227,1099,1102],{"className":1100,"code":1101,"language":232,"meta":233},[230],"idf-monitor --port \u002Fdev\u002FttyUSB0 build\u002Fesp32s3_nanomq\u002Fzephyr\u002Fzephyr.elf\n",[36,1103,1101],{"__ignoreMap":233},[17,1105,1106],{},"A sample boot log from an actual run is shown below. Path prefixes are omitted, and the SSID is replaced with a placeholder:",[227,1108,1111],{"className":1109,"code":1110,"language":232,"meta":233},[230],"I (octal_psram): vendor id    : 0x0d (AP)\nI (octal_psram): density      : 0x05 (128 Mbit)\nI (esp_psram): Found 16MB PSRAM device\nI (esp_psram): Speed: 80MHz\nI (esp_psram): SPI SRAM memory test OK\n*** Booting Zephyr OS build v4.4.0-4779-g11a87708d415 ***\nwifi: connecting to \"\u003Cyour-ssid>\" (attempt 1)\nwifi: connected to \"\u003Cyour-ssid>\"\nwifi: connected — starting DHCPv4 client\nwifi: IPv4 address assigned (DHCPv4)\nsntp: ntp.aliyun.com: epoch=1789468030, realtime seeded\nnet: iface 0x3fc96fd8 dev=wifi up=1\nnet: ipv4 \u003Cboard-ip>\n2026-09-15 10:27:10 [0] INFO  web_server.c:548 start_rest_server: http:\u002F\u002F0.0.0.0:8081\u002Fapi\u002Fv4\n2026-09-15 10:27:10 [0] WARN  broker.c:1400 broker: NanoMQ (ver 0.25.6) Serving HTTP Server on http:\u002F\u002F(null):8081\nNanoMQ Broker is started successfully!\n",[36,1112,1110],{"__ignoreMap":233},[17,1114,533,1115,469,1118,1121,1122,1124,1125,1128],{},[36,1116,1117],{},"start_rest_server",[36,1119,1120],{},"Serving HTTP Server"," lines appear only when REST credentials were configured at build time (the ",[36,1123,927],{}," from step 1). Without them, this spot shows a single ",[36,1126,1127],{},"rest: REST API stays off - ..."," line instead, and nothing listens on 8081.",[17,1130,1131,1132,1135,1136,252],{},"The startup sequence is: ",[21,1133,1134],{},"PSRAM detection and memory test → Zephyr startup → Wi-Fi association → DHCP address assignment → SNTP time seeding → broker initialization."," The broker is ready when the final line appears: ",[36,1137,607],{},[122,1139,1141],{"id":1140},"_5-verify-the-connection","5. Verify the Connection",[17,1143,1144,1145,1147],{},"Once the broker is ready, run the following checks from a host on the same network. Test the REST API (the credentials are the pair you set in ",[36,1146,927],{},"; if you did not set them, nothing listens on 8081 and curl will simply fail to connect, which is expected):",[227,1149,1152],{"className":1150,"code":1151,"language":232,"meta":233},[230],"curl -u \u003Crest-user>:\u003Crest-pass> http:\u002F\u002F\u003Cboard-ip>:8081\u002Fapi\u002Fv4\u002Fbrokers\u002F\n",[36,1153,1151],{"__ignoreMap":233},[17,1155,1156],{},"To test MQTT messaging, subscribe with the Mosquitto client from any terminal:",[227,1158,1161],{"className":1159,"code":1160,"language":232,"meta":233},[230],"mosquitto_sub -h \u003Cboard-ip> -t 'test\u002F#' -v\n",[36,1162,1160],{"__ignoreMap":233},[17,1164,1165],{},"In another terminal, publish a message:",[227,1167,1170],{"className":1168,"code":1169,"language":232,"meta":233},[230],"mosquitto_pub -h \u003Cboard-ip> -t 'test\u002Fhello' -m 'hello from mosquitto' -q 1\n",[36,1171,1169],{"__ignoreMap":233},[17,1173,1174,1175],{},"The subscriber should receive: ",[36,1176,1177],{},"test\u002Fhello hello from mosquitto",[122,1179,1009],{"id":1180},"about-the-development-board",[17,1182,1183,1184,1189,1190,1193,1194,1197,1198,1201,1202,1205,1206,1209,1210,1213,1214,1217,1218,1220,1221,252],{},"The demo was validated on the ",[30,1185,1188],{"href":1186,"rel":1187},"https:\u002F\u002Fdocs.espressif.com\u002Fprojects\u002Fesp-dev-kits\u002Fen\u002Flatest\u002Fesp32s3\u002Fesp32-s3-lcd-ev-board\u002Fuser_guide.html#esp32-s3-lcd-ev-board-v1-5",[175],"ESP32-S3-LCD-EV-Board",", using an ESP32-S3-WROOM-1-",[21,1191,1192],{},"N16R16V"," module (16 MB flash and 16 MB ",[21,1195,1196],{},"octal"," PSRAM). The board configuration uses the same SoC definition as ",[36,1199,1200],{},"esp32s3_devkitc\u002Fesp32s3\u002Fprocpu",". Differences in the module configuration are expressed in two places: ",[36,1203,1204],{},"boards\u002Fesp32s3_devkitc_procpu.overlay"," (flash and PSRAM capacity) and ",[36,1207,1208],{},"CONFIG_SPIRAM_MODE_OCT"," in ",[36,1211,1212],{},"prj.conf"," (octal mode). ",[21,1215,1216],{},"If your module is not N16R16V, both must be changed."," Octal and quad PSRAM configurations cannot be interchanged simply by changing a parameter: a quad-PSRAM module also requires replacing ",[36,1219,1208],{}," with ",[36,1222,1223],{},"CONFIG_SPIRAM_MODE_QUAD",[17,1225,1226],{},"The link report shows: FLASH 961,204 B, internal SRAM 379,640 B \u002F 399,108 B (95.12%), and 5,152,752 B placed in external PSRAM.",[17,1228,1229,1230,1233,1234,1237,1238,1240,1241,1244,1245,1248,1249,1251,1252,1255,1256,1259,1260,1263],{},"The last figure (approximately 5.2 MB) is easy to misread as the PSRAM capacity or the size of its address window, so it is worth spelling out how these three values relate. The module's physical PSRAM capacity is ",[21,1231,1232],{},"16 MB"," (",[36,1235,1236],{},"CONFIG_ESP_SPIRAM_SIZE","). The linker script declares ",[36,1239,996],{}," as a ",[21,1242,1243],{},"32 MB region"," — a DCACHE0 address window it shares with ",[36,1246,1247],{},"drom0_0_seg",", which is an address-space limit rather than usable capacity. The ",[21,1250,1000],{}," shown in the link report is that region's ",[21,1253,1254],{},"Used Size",", the amount actually placed in PSRAM. It includes the 4 MB broker heap specified by ",[36,1257,1258],{},"CONFIG_ESP_SPIRAM_HEAP_SIZE",", as well as static pools moved out of internal SRAM, such as the Wi-Fi driver's ",[36,1261,1262],{},".noinit"," section, the net_buf pools, and the Zephyr POSIX object pools. In other words, 5.2 MB is neither the PSRAM capacity nor the size of its mapped window.",[17,1265,1266,1269,1270,1272,1273,1276],{},[21,1267,1268],{},"Clock."," The development board has no RTC. After DHCP completes, ",[36,1271,770],{}," performs a one-shot SNTP sync to seed ",[36,1274,1275],{},"CLOCK_REALTIME"," — up to two passes over two public SNTP servers, returning as soon as one answers — so the timestamps in the log are real UTC. This step is best-effort: if no server responds, the broker still starts normally, and only the timestamps remain at the Unix epoch (2 seconds per query and 1 second between the two passes, so the worst case adds roughly 9 seconds to startup).",[12,1278,1280],{"id":1279},"build-and-run-qemu_x86","Build and Run: qemu_x86",[122,1282,1284],{"id":1283},"_1-build","1. Build",[17,1286,1287,1288,1290],{},"After applying the e1000 patch (activate the Zephyr workspace venv from \"Environment Setup → Prerequisites\" first so that ",[36,1289,208],{}," is available; ESP-IDF is not needed):",[227,1292,1295],{"className":1293,"code":1294,"language":232,"meta":233},[230],"west build -b qemu_x86 -d build\u002Fnanomq_zephyr_qemu_x86 demo\u002Fnanomq_zephyr_qemu_x86\n",[36,1296,1294],{"__ignoreMap":233},[17,1298,1299],{},"A successful build ends with output similar to:",[227,1301,1304],{"className":1302,"code":1303,"language":232,"meta":233},[230],"[289\u002F290] Building C object zephyr\u002FCMakeFiles\u002Fzephyr_final.dir\u002Fmisc\u002Fempty_file.c.obj\n[290\u002F290] Linking C executable zephyr\u002Fzephyr.elf\nMemory region         Used Size  Region Size  %age Used\n             RAM:     2608020 B        31 MB      8.02%\n        IDT_LIST:           0 B         2 KB      0.00%\nGenerating files from build\u002Fnanomq_zephyr_qemu_x86\u002Fzephyr\u002Fzephyr.elf for board: qemu_x86\u002Fatom\n",[36,1305,1303],{"__ignoreMap":233},[17,1307,1308],{},"(This figure is sensitive to code layout: changing a few dozen bytes of code can push a section across a page boundary and shift the reported value by a whole 4 KB page. That is alignment padding, not real growth.)",[17,1310,533,1311,1314,1315,1318,1319,1321,1322,1324,1325,1328],{},[36,1312,1313],{},"qemu_x86\u002Fatom"," shown at the end is the fully qualified board name. ",[36,1316,1317],{},"atom"," is the SoC qualifier for ",[36,1320,50],{},", using the same board\u002FSoC qualification mechanism as ",[36,1323,1200],{}," on the ESP32-S3 side. It does not mean that ",[36,1326,1327],{},"-b qemu_x86"," selected the wrong board.",[122,1330,1332],{"id":1331},"_2-run","2. Run",[17,1334,1335],{},"Starting QEMU in the background and writing the serial console to a file is the recommended approach — it leaves the terminal free for the verification steps that follow:",[227,1337,1340],{"className":1338,"code":1339,"language":232,"meta":233},[230],"qemu-system-i386 -m 32 -cpu qemu32,+nx,+pae,sse,sse2,pni -machine q35 \\\n    -device isa-debug-exit,iobase=0xf4,iosize=0x04 -no-reboot -machine acpi=off \\\n    -serial file:\u002Ftmp\u002Fqemu.log -display none \\\n    -netdev user,id=n1,hostfwd=tcp:127.0.0.1:1883-:1883,hostfwd=tcp:127.0.0.1:8081-:8081,hostfwd=tcp:127.0.0.1:8083-:8083 \\\n    -device e1000,netdev=n1 \\\n    -kernel build\u002Fnanomq_zephyr_qemu_x86\u002Fzephyr\u002Fzephyr.elf &\n",[36,1341,1339],{"__ignoreMap":233},[17,1343,1344,1345,1209,1348,1350,1351,1354,1355],{},"The port forwarding is configured through ",[36,1346,1347],{},"CONFIG_NET_QEMU_USER_EXTRA_ARGS",[36,1349,1212],{},", which binds 1883\u002F8081\u002F8083 to ",[36,1352,1353],{},"127.0.0.1"," and matches the manual command above. Both follow the same rule: ",[21,1356,1357],{},"whoever runs QEMU owns the network interfaces its ports are bound to.",[17,1359,1360],{},"You can also let west start it directly:",[227,1362,1365],{"className":1363,"code":1364,"language":232,"meta":233},[230],"west build -d build\u002Fnanomq_zephyr_qemu_x86 -t run\n",[36,1366,1364],{"__ignoreMap":233},[17,1368,1369,1370,1372,1373,1376],{},"This picks up the three port forwards from ",[36,1371,1212],{}," automatically, ",[21,1374,1375],{},"but it attaches the serial console to stdio and occupies the current terminal",", so it is only suitable for watching the boot process.",[17,1378,1379,1382],{},[21,1380,1381],{},"Clear out old instances before restarting."," A leftover QEMU keeps the three ports occupied and the new instance fails immediately:",[227,1384,1387],{"className":1385,"code":1386,"language":232,"meta":233},[230],"qemu-system-i386: ... Could not set up host forwarding rule 'tcp:127.0.0.1:8083-:8083'\n",[36,1388,1386],{"__ignoreMap":233},[17,1390,1391,1394],{},[36,1392,1393],{},"-t run"," is especially prone to leaving such a process behind — stopping it does not necessarily take the QEMU instance it started with it. To clean up, look the PIDs up by port:",[227,1396,1399],{"className":1397,"code":1398,"language":232,"meta":233},[230],"ss -ltnp | grep -E ':(1883|8081|8083)' | grep -oP 'pid=\\K[0-9]+' | sort -u | xargs -r kill\n",[36,1400,1398],{"__ignoreMap":233},[17,1402,1403,1406,1407,1409,1410,1413,1414,1417,1418,252],{},[36,1404,1405],{},"qemu-system-i386"," can also be located by west itself (the host tools of the Zephyr SDK, or a distribution package on ",[36,1408,224],{},", such as Fedora's ",[36,1411,1412],{},"qemu-system-x86","). If running it manually reports ",[36,1415,1416],{},"command not found",", add the directory containing QEMU to ",[36,1419,224],{},[17,1421,533,1422,1425],{},[36,1423,1424],{},"-m 32"," option allocates 32 MB of memory to QEMU. Zephyr's linker script reserves part of this memory, so the usable RAM shown in the link report is approximately 31 MB (the image used in this article occupies about 2.5 MB, of which approximately 1 MB is the malloc arena).",[122,1427,1429],{"id":1428},"_3-startup-log","3. Startup Log",[17,1431,1432],{},"The following is an actual startup log (some DEBUG lines and path prefixes omitted):",[227,1434,1437],{"className":1435,"code":1436,"language":232,"meta":233},[230],"*** Booting Zephyr OS build v4.4.0-4779-g11a87708d415 ***\nrtc: CMOS clock 2026-09-15 10:27:23 UTC, realtime seeded\nnet: iface 0x19e94c dev=eth0 up=1\nnet: ipv4 10.0.2.15\n2026-09-15 10:27:23 [0] DEBUG broker.c:1102 broker: db init finished\n2026-09-15 10:27:23 [0] DEBUG broker.c:1111 broker: listener init finished\n2026-09-15 10:27:23 [0] DEBUG broker.c:1128 broker: HTTP init finished\n2026-09-15 10:27:23 [0] INFO  web_server.c:548 start_rest_server: http:\u002F\u002F0.0.0.0:8081\u002Fapi\u002Fv4\n2026-09-15 10:27:23 [0] WARN  broker.c:1400 broker: NanoMQ (ver 0.25.6) Serving HTTP Server on http:\u002F\u002F(null):8081\nNanoMQ Broker is started successfully!\n",[36,1438,1436],{"__ignoreMap":233},[17,1440,1441],{},"Those two REST lines appear only when credentials were configured at build time.",[17,1443,1444,1445,1447,1448,790,1450,1452,1453,1455],{},"The broker is ready when the final line appears: ",[36,1446,607],{},". You can then verify the REST API (provided that ",[36,1449,789],{},[36,1451,859],{}," were set through ",[36,1454,927],{}," at build time; without them nothing listens on 8081):",[227,1457,1460],{"className":1458,"code":1459,"language":232,"meta":233},[230],"curl -u \u003Crest-user>:\u003Crest-pass> http:\u002F\u002F127.0.0.1:8081\u002Fapi\u002Fv4\u002Fbrokers\u002F\n",[36,1461,1459],{"__ignoreMap":233},[122,1463,1465],{"id":1464},"about-the-malloc-arena","About the malloc Arena",[17,1467,1468,1470,1471,1474,1475,1478,1479,1482],{},[36,1469,50],{}," enables the MMU, and Zephyr's libc provides a default ",[36,1472,1473],{},"malloc"," arena of only 16 KB. The NNG Zephyr platform adaptation layer uses ",[36,1476,1477],{},"malloc()"," as its allocator, so the receive queue of a single pipe can exceed this limit as ",[36,1480,1481],{},"msq_len * 8"," bytes.",[17,1484,533,1485,1487,1488,1490],{},[36,1486,1212],{}," configuration therefore increases the arena to 1 MB. As a result, approximately 1 MB of the image's memory usage belongs to the ",[36,1489,1473],{}," arena rather than to the broker itself.",[12,1492,1494],{"id":1493},"functional-testing","Functional Testing",[17,1496,1497],{},"The repository includes a functional test suite that runs the same set of tests against both demos. Before running the tests, install the required Python dependencies (the runner checks for them and exits with an error if they are missing):",[227,1499,1502],{"className":1500,"code":1501,"language":232,"meta":233},[230],"python3 -m pip install paho-mqtt requests\n",[36,1503,1501],{"__ignoreMap":233},[17,1505,1506,1507,469,1510,1513,1514,1517,1518,252],{},"The system must also have the Mosquitto command-line clients (",[36,1508,1509],{},"mosquitto_pub",[36,1511,1512],{},"mosquitto_sub",") installed. On Debian\u002FUbuntu, install the ",[36,1515,1516],{},"mosquitto-clients"," package; on Fedora, install ",[36,1519,1520],{},"mosquitto",[17,1522,1523,1526],{},[36,1524,1525],{},"--list"," shows all test groups (this article covers 8 of them, listed in the table below):",[227,1528,1531],{"className":1529,"code":1530,"language":232,"meta":233},[230],"python3 demo\u002Fnanomq_zephyr_qemu_x86\u002Ffunction_test.py --list\n",[36,1532,1530],{"__ignoreMap":233},[17,1534,1535,1536,1539],{},"For a running hardware demo, use ",[36,1537,1538],{},"--no-manage"," to tell the runner that the broker is already running and should not be managed by the runner:",[227,1541,1544],{"className":1542,"code":1543,"language":232,"meta":233},[230],"python3 demo\u002Fnanomq_zephyr_qemu_x86\u002Ffunction_test.py --no-manage --addr \u003Cboard-ip>\n",[36,1545,1543],{"__ignoreMap":233},[17,1547,1548],{},"You can also run specific test groups:",[227,1550,1553],{"className":1551,"code":1552,"language":232,"meta":233},[230],"python3 demo\u002Fnanomq_zephyr_qemu_x86\u002Ffunction_test.py --no-manage --addr \u003Cboard-ip> \\\n    --group mqtt_v311 --group rest_get\n",[36,1554,1552],{"__ignoreMap":233},[17,1556,1557,1558,1561],{},"The following results were obtained on the ESP32-S3 hardware demo, running the 8 groups in the table below one at a time with ",[36,1559,1560],{},"--group",". Path prefixes are omitted for readability.",[227,1563,1566],{"className":1564,"code":1565,"language":232,"meta":233},[230],"========================================================================\nZephyr broker functional test suite — broker \u003Cboard-ip>:1883\ngroups: mqtt_v311, mqtt_v5, rest_get, ws_v311, ws_v5, capacity, ws_abort, survival\n========================================================================\n--no-manage: assuming a broker is already running; leftover mosquitto clients are NOT cleaned up\nbroker connect RTT 16.1 ms -> time-scale 4.0 (auto)\n[1\u002F8] mqtt_v311      PASS  (120.1s)\n[2\u002F8] mqtt_v5        PASS  (211.3s)\n[3\u002F8] rest_get       PASS  (2.9s)\n[4\u002F8] ws_v311        PASS  (279.0s)\n[5\u002F8] ws_v5          PASS  (15.9s)\n[6\u002F8] capacity       PASS  (13.1s)\n[7\u002F8] ws_abort       PASS  (31.9s)\n[8\u002F8] survival       PASS  (90.3s)\n------------------------------------------------------------------------\nRESULT: pass=8 fail=0\n",[36,1567,1565],{"__ignoreMap":233},[17,1569,533,1570,1573,1574,1576,1577,225],{},[36,1571,1572],{},"rest_get"," group additionally requires that the broker under test was built with REST credentials configured (see each demo's ",[36,1575,923],{},"). When they are not configured, this group does not fail but is reported as ",[21,1578,1579],{},"SKIP",[227,1581,1584],{"className":1582,"code":1583,"language":232,"meta":233},[230],"| [worker rest_get] SKIP: no REST API on \u003Caddr>:8081 — the demos leave it off\n| until CONFIG_BROKER_REST_USER\u002FPASS are set (local.conf); pass\n| --rest-user\u002F--rest-pass for a broker that has them\n",[36,1585,1583],{"__ignoreMap":233},[17,1587,1588,1589,790,1592,1595],{},"In other words, the suite distinguishes \"REST is not enabled\" from \"REST is broken\": the former is skipped with an explanation, and only the latter fails. Once the broker has credentials configured, pass the same pair to the suite with ",[36,1590,1591],{},"--rest-user",[36,1593,1594],{},"--rest-pass"," and the group runs normally.",[17,1597,1598],{},"The coverage and execution time for each test group on the hardware demo are shown below:",[260,1600,1601,1614],{},[263,1602,1603],{},[266,1604,1605,1608,1611],{},[269,1606,1607],{},"Test group",[269,1609,1610],{},"Coverage",[269,1612,1613],{},"Hardware runtime",[276,1615,1616,1629,1646,1658,1671,1684,1697,1710],{},[266,1617,1618,1623,1626],{},[281,1619,1620],{},[36,1621,1622],{},"mqtt_v311",[281,1624,1625],{},"Sessions, retained messages, and v4\u002Fv5 interoperability",[281,1627,1628],{},"120.1s",[266,1630,1631,1636,1643],{},[281,1632,1633],{},[36,1634,1635],{},"mqtt_v5",[281,1637,1638,1639,1642],{},"Session expiry, user properties, ",[36,1640,1641],{},"$share",", and topic aliases",[281,1644,1645],{},"211.3s",[266,1647,1648,1652,1655],{},[281,1649,1650],{},[36,1651,1572],{},[281,1653,1654],{},"All REST GET routes",[281,1656,1657],{},"2.9s",[266,1659,1660,1665,1668],{},[281,1661,1662],{},[36,1663,1664],{},"ws_v311",[281,1666,1667],{},"MQTT 3.1.1 over WebSocket",[281,1669,1670],{},"279.0s",[266,1672,1673,1678,1681],{},[281,1674,1675],{},[36,1676,1677],{},"ws_v5",[281,1679,1680],{},"MQTT 5 over WebSocket",[281,1682,1683],{},"15.9s",[266,1685,1686,1691,1694],{},[281,1687,1688],{},[36,1689,1690],{},"capacity",[281,1692,1693],{},"12 concurrent CONNECTs + QoS 1 loopback",[281,1695,1696],{},"13.1s",[266,1698,1699,1704,1707],{},[281,1700,1701],{},[36,1702,1703],{},"ws_abort",[281,1705,1706],{},"Connection-pool stability when WebSocket connections are aborted abnormally",[281,1708,1709],{},"31.9s",[266,1711,1712,1717,1723],{},[281,1713,1714],{},[36,1715,1716],{},"survival",[281,1718,1719,1720],{},"Scaled-down load\u002Fsession churn based on the upstream ",[36,1721,1722],{},"attack.py",[281,1724,1725],{},"90.3s",[17,1727,1728,1729,1732],{},"The runner adjusts its parameters automatically: it first measures the TCP round-trip time to the broker (below 1 ms on the local loopback, tens to hundreds of milliseconds over Wi-Fi on real hardware; 16.1 ms in this run), and on determining that it is not local, it switches to ",[36,1730,1731],{},"--time-scale 4"," by default, which extends the sleep intervals that the CI scripts calibrate for local-network latency.",[12,1734,1736],{"id":1735},"the-porting-process","The Porting Process",[17,1738,1739,1740,1743,1744,1747,1748,1750],{},"NanoMQ's layering already includes a platform adaptation layer (nng's ",[36,1741,1742],{},"nni_plat_*","), so most of the porting work is ",[21,1745,1746],{},"filling in that layer for Zephyr",". The real hard blockers, on the other hand, are in the application layer: a few places in ",[36,1749,38],{}," call POSIX directly and cannot be worked around. The sections below go layer by layer.",[122,1752,1754],{"id":1753},"_1-application-layer-trimming-posix-dependencies","1. Application Layer: Trimming POSIX Dependencies",[17,1756,1757,1758,1761],{},"The application layer's POSIX dependencies are concentrated in four places, all handled with ",[36,1759,1760],{},"__ZEPHYR__"," conditional compilation rather than a new abstraction layer:",[260,1763,1764,1777],{},[263,1765,1766],{},[266,1767,1768,1771,1774],{},[269,1769,1770],{},"File",[269,1772,1773],{},"Original POSIX dependency",[269,1775,1776],{},"Handling",[276,1778,1779,1801,1820,1840],{},[266,1780,1781,1786,1795],{},[281,1782,1783],{},[36,1784,1785],{},"apps\u002Fbroker.c",[281,1787,1788,790,1791,1794],{},[36,1789,1790],{},"signal()",[36,1792,1793],{},"sigaction"," for installing signal handlers",[281,1796,1797,1798],{},"The whole block is skipped. Zephyr has no POSIX signal semantics; an embedded broker's main loop terminates on its own conditions rather than on ",[36,1799,1800],{},"^C",[266,1802,1803,1808,1813],{},[281,1804,1805],{},[36,1806,1807],{},"nanomq.c",[281,1809,1810],{},[36,1811,1812],{},"#include \u003Csys\u002Fptrace.h>",[281,1814,1815,1816,1819],{},"Conditional compilation. ",[36,1817,1818],{},"check_trace()"," is only called on the CLI path, which an embedded broker never takes",[266,1821,1822,1827,1833],{},[281,1823,1824],{},[36,1825,1826],{},"mqtt_api.c",[281,1828,1829,1832],{},[36,1830,1831],{},"nng_access(dir, W_OK)"," (writability check of the file-log directory)",[281,1834,1835,1836,1839],{},"Conditional compilation. picolibc has no ",[36,1837,1838],{},"W_OK","; the file-log backend is always off on embedded, so skipping the check has no side effect",[266,1841,1842,1848,1862],{},[281,1843,1844,1847],{},[36,1845,1846],{},"process.c"," (the whole translation unit)",[281,1849,1850,790,1853,790,1856,383,1859],{},[36,1851,1852],{},"fork",[36,1854,1855],{},"kill",[36,1857,1858],{},"chdir",[36,1860,1861],{},"\u003Cpaths.h>",[281,1863,1864,1865,1868],{},"Not built; the demo provides ",[36,1866,1867],{},"process_stub.c"," supplying its public symbols (all returning -1)",[17,1870,1871,1873,1874,1877,1878,383,1881,1884,1885,1887,1888,790,1891,790,1894,1897,1898,1901,1902,469,1905,1220,1908,1911,1912,252],{},[36,1872,1867],{}," can \"pretend\" like this because the five symbols either have no call sites at all or are only reached from paths that never execute: both calls to ",[36,1875,1876],{},"process_daemonize()"," are guarded by ",[36,1879,1880],{},"daemon == true",[36,1882,1883],{},"process_send_signal()"," is called only by ",[36,1886,1818],{}," on the CLI path, and ",[36,1889,1890],{},"process_is_alive()",[36,1892,1893],{},"pidgrp_send_signal()",[36,1895,1896],{},"process_create_child()"," have ",[21,1899,1900],{},"no call sites anywhere in the application",". An embedded broker calls ",[36,1903,1904],{},"broker()",[36,1906,1907],{},"conf_init()",[36,1909,1910],{},"daemon"," defaulting to false, so none of those paths is reached — ",[21,1913,1914],{},"the stub's actual job is simply to let the link succeed",[122,1916,1918],{"id":1917},"_2-platform-adaptation-layer-interface-inventory-and-posix-differences","2. Platform Adaptation Layer: Interface Inventory and POSIX Differences",[17,1920,1921,1922,1925,1926,1929,1930,469,1933,1936],{},"This layer is nng's Zephyr implementation (",[36,1923,1924],{},"src\u002Fplatform\u002Fzephyr\u002F",", 22 files, 18 of them ",[36,1927,1928],{},".c",": 16 implementing the interface families below, plus 2 stubs — ",[36,1931,1932],{},"zephyr_peerid.c",[36,1934,1935],{},"zephyr_socketpair.c",", corresponding to the two unsupported facilities in the table). It is organized by nng's interface families, each mapped onto a Zephyr facility:",[17,1938,1939],{},[21,1940,1941],{},"Interface inventory",[260,1943,1944,1960],{},[263,1945,1946],{},[266,1947,1948,1951,1954,1957],{},[269,1949,1950],{},"Interface family",[269,1952,1953],{},"Implementation file",[269,1955,1956],{},"Corresponding POSIX facility",[269,1958,1959],{},"Zephyr implementation",[276,1961,1962,1989,2016,2037,2056,2086,2113,2139,2161,2184,2206,2224,2245,2271],{},[266,1963,1964,1967,1972,1979],{},[281,1965,1966],{},"Memory",[281,1968,1969],{},[36,1970,1971],{},"zephyr_alloc.c",[281,1973,1974,790,1976],{},[36,1975,1473],{},[36,1977,1978],{},"free",[281,1980,1981,1982,1985,1986,1988],{},"A ",[36,1983,1984],{},"k_heap"," on PSRAM, or the libc ",[36,1987,1477],{}," (per target)",[266,1990,1991,1994,1999,2007],{},[281,1992,1993],{},"Clock and sleep",[281,1995,1996],{},[36,1997,1998],{},"zephyr_clock.c",[281,2000,2001,790,2004],{},[36,2002,2003],{},"clock_gettime",[36,2005,2006],{},"nanosleep",[281,2008,2009,2012,2013],{},[36,2010,2011],{},"k_uptime_get()",", plus Zephyr's POSIX ",[36,2014,2015],{},"nanosleep()",[266,2017,2018,2021,2026,2032],{},[281,2019,2020],{},"Threads and synchronization",[281,2022,2023],{},[36,2024,2025],{},"zephyr_thread.c",[281,2027,2028,2031],{},[36,2029,2030],{},"pthread_*"," (mutexes \u002F rwlocks \u002F condition variables \u002F threads)",[281,2033,2034,2035,308],{},"Zephyr's pthread implementation (",[36,2036,103],{},[266,2038,2039,2042,2047,2053],{},[281,2040,2041],{},"Atomic operations",[281,2043,2044],{},[36,2045,2046],{},"zephyr_atomic.c",[281,2048,2049,2050],{},"C11 ",[36,2051,2052],{},"\u003Cstdatomic.h>",[281,2054,2055],{},"Native 64-bit atomics, or an embedded-mutex fallback",[266,2057,2058,2061,2066,2080],{},[281,2059,2060],{},"Debugging and error codes",[281,2062,2063],{},[36,2064,2065],{},"zephyr_debug.c",[281,2067,2068,790,2071,790,2074,790,2077],{},[36,2069,2070],{},"abort",[36,2072,2073],{},"printf",[36,2075,2076],{},"strerror",[36,2078,2079],{},"errno",[281,2081,2082,2085],{},[36,2083,2084],{},"printk",", libc, and the errno ↔ nng error-code mapping",[266,2087,2088,2091,2099,2110],{},[281,2089,2090],{},"TCP (client)",[281,2092,2093,383,2096],{},[36,2094,2095],{},"zephyr_tcpdial.c",[36,2097,2098],{},"zephyr_tcpconn.c",[281,2100,2101,790,2104,790,2107],{},[36,2102,2103],{},"socket",[36,2105,2106],{},"connect",[36,2108,2109],{},"setsockopt",[281,2111,2112],{},"The BSD-compatible sockets of Zephyr's network stack",[266,2114,2115,2118,2123,2136],{},[281,2116,2117],{},"TCP (server)",[281,2119,2120],{},[36,2121,2122],{},"zephyr_tcplisten.c",[281,2124,2125,790,2127,790,2130,790,2133],{},[36,2126,2103],{},[36,2128,2129],{},"bind",[36,2131,2132],{},"listen",[36,2134,2135],{},"accept",[281,2137,2138],{},"Same as above",[266,2140,2141,2144,2149,2159],{},[281,2142,2143],{},"UDP",[281,2145,2146],{},[36,2147,2148],{},"zephyr_udp.c",[281,2150,2151,790,2153,790,2156],{},[36,2152,2103],{},[36,2154,2155],{},"sendmsg",[36,2157,2158],{},"recvmsg",[281,2160,2138],{},[266,2162,2163,2166,2174,2180],{},[281,2164,2165],{},"Descriptors and the poll queue",[281,2167,2168,383,2171],{},[36,2169,2170],{},"zephyr_pollq_poll.c",[36,2172,2173],{},"zephyr_sockfd.c",[281,2175,2176,2179],{},[36,2177,2178],{},"poll()"," (POSIX additionally layers epoll \u002F kqueue \u002F eventfd on top)",[281,2181,618,2182],{},[36,2183,2178],{},[266,2185,2186,2189,2194,2201],{},[281,2187,2188],{},"Socket address conversion",[281,2190,2191],{},[36,2192,2193],{},"zephyr_sockaddr.c",[281,2195,2196,2197,2200],{},"Conversion between ",[36,2198,2199],{},"sockaddr"," and nng structures",[281,2202,618,2203,2205],{},[36,2204,2199],{}," (AF_UNIX removed)",[266,2207,2208,2211,2216,2221],{},[281,2209,2210],{},"DNS resolution",[281,2212,2213],{},[36,2214,2215],{},"zephyr_resolv_gai.c",[281,2217,2218],{},[36,2219,2220],{},"getaddrinfo()",[281,2222,2223],{},"Zephyr has the interface, but with different semantics (see the table below)",[266,2225,2226,2229,2234,2242],{},[281,2227,2228],{},"Random numbers",[281,2230,2231],{},[36,2232,2233],{},"zephyr_rand_urandom.c",[281,2235,2236,383,2239],{},[36,2237,2238],{},"\u002Fdev\u002Furandom",[36,2240,2241],{},"getrandom()",[281,2243,2244],{},"Branches on whether a hardware entropy source exists (see the table below)",[266,2246,2247,2250,2255,2264],{},[281,2248,2249],{},"Files",[281,2251,2252],{},[36,2253,2254],{},"zephyr_file.c",[281,2256,2257,383,2260,2263],{},[36,2258,2259],{},"stdio",[36,2261,2262],{},"stat",", file locking",[281,2265,2266,469,2268,2270],{},[36,2267,2259],{},[36,2269,2262],{}," are real implementations; the temp directory and file locking are fallbacks (see the table below)",[266,2272,2273,2276,2281,2289],{},[281,2274,2275],{},"Wakeup pipe",[281,2277,2278],{},[36,2279,2280],{},"zephyr_pipe.c",[281,2282,2283,790,2286],{},[36,2284,2285],{},"pipe()",[36,2287,2288],{},"eventfd()",[281,2290,2291],{},"Zephyr has no corresponding facility (see the table below)",[17,2293,2294],{},[21,2295,2296],{},"Where POSIX assumptions do not hold",[260,2298,2299,2311],{},[263,2300,2301],{},[266,2302,2303,2306,2309],{},[269,2304,2305],{},"POSIX assumption",[269,2307,2308],{},"Zephyr today",[269,2310,1776],{},[276,2312,2313,2339,2353,2373,2395,2413,2429,2445,2456,2471,2483],{},[266,2314,2315,2323,2329],{},[281,2316,2317,790,2320],{},[36,2318,2319],{},"readv()",[36,2321,2322],{},"writev()",[281,2324,2325,2326],{},"Not available, and there is no ",[36,2327,2328],{},"\u003Csys\u002Fuio.h>",[281,2330,2331,2332,790,2335,2338],{},"Emulated with ",[36,2333,2334],{},"read()",[36,2336,2337],{},"write()"," loops, with short-transfer semantics explicitly preserved (Detail 1)",[266,2340,2341,2347,2350],{},[281,2342,2343,790,2345],{},[36,2344,2285],{},[36,2346,2288],{},[281,2348,2349],{},"Not available",[281,2351,2352],{},"The poll queue registers no wakeup descriptor and advances on its own timeout instead",[266,2354,2355,2363,2366],{},[281,2356,2357,790,2360],{},[36,2358,2359],{},"flock()",[36,2361,2362],{},"lockf()",[281,2364,2365],{},"No file locking",[281,2367,2368,2369,2372],{},"Follows ",[36,2370,2371],{},"posix_file.c","'s own embedded fallback: succeeds, but does not lock",[266,2374,2375,2378,2381],{},[281,2376,2377],{},"Temp directory \u002F working directory",[281,2379,2380],{},"Neither concept exists",[281,2382,2383,2386,2387,2390,2391,2394],{},[36,2384,2385],{},"nni_plat_temp_dir()"," returns ",[36,2388,2389],{},"\"\u002Ftmp\""," (whether a filesystem is really mounted there is reported by the caller's own file operations); ",[36,2392,2393],{},"nni_plat_getcwd()"," returns NULL",[266,2396,2397,2403,2406],{},[281,2398,2399,2400],{},"Peer credentials such as ",[36,2401,2402],{},"SO_PEERCRED",[281,2404,2405],{},"Unavailable",[281,2407,2408,2409,2412],{},"Returns ",[36,2410,2411],{},"NNG_ENOTSUP",". Peer credentials only serve the AF_UNIX IPC transport, which does not exist on Zephyr",[266,2414,2415,2421,2424],{},[281,2416,2417,2420],{},[36,2418,2419],{},"socketpair()"," (AF_UNIX domain socket pairs)",[281,2422,2423],{},"No AF_UNIX",[281,2425,2408,2426,2428],{},[36,2427,2411],{},"; the IPC transport that depends on it is therefore unavailable on Zephyr (see Appendix A)",[266,2430,2431,2435,2438],{},[281,2432,2433],{},[36,2434,2238],{},[281,2436,2437],{},"No such device",[281,2439,2440,2441,2444],{},"Targets with a hardware entropy source (the ESP32-S3 TRNG) use ",[36,2442,2443],{},"sys_csrand_get()","; targets without one fall back to a software generator, annotated in the code as unsuitable for secret material",[266,2446,2447,2450,2453],{},[281,2448,2449],{},"64-bit atomic operations",[281,2451,2452],{},"32-bit non-x86 targets have no native support",[281,2454,2455],{},"Falls back to \"atomics built on an embedded mutex\" (Detail 2)",[266,2457,2458,2463,2466],{},[281,2459,2460],{},[36,2461,2462],{},"SO_BINDTODEVICE",[281,2464,2465],{},"No interface binding",[281,2467,2408,2468,2470],{},[36,2469,2411],{}," at the option-setting point, rather than accepting the option and silently using the default interface",[266,2472,2473,2477,2480],{},[281,2474,2475],{},[36,2476,2220],{},[281,2478,2479],{},"Present, but synchronous",[281,2481,2482],{},"Synchronous semantics are accepted: resolution happens on the caller's thread, with no worker thread introduced",[266,2484,2485,2490,2496],{},[281,2486,2487],{},[36,2488,2489],{},"pthread_condattr_setclock()",[281,2491,2492,2493],{},"Depends on ",[36,2494,2495],{},"CONFIG_POSIX_CLOCK_SELECTION",[281,2497,2498],{},"Validated at startup; if the monotonic clock is unavailable, the process terminates — otherwise every timed wait would silently time out immediately",[17,2500,2501,2514,2515,790,2517,2519,2520,2523,2524,2527,2528,2531],{},[21,2502,2503,2504,790,2509,252],{},"Detail 1: Short-transfer semantics of ",[21,2505,2506],{},[36,2507,2508],{},"readv",[21,2510,2511],{},[36,2512,2513],{},"writev"," Simply \"looping until all iovecs are written\" is wrong. POSIX ",[36,2516,2508],{},[36,2518,2513],{}," return at the ",[21,2521,2522],{},"first short transfer"," and report the bytes transferred, leaving the caller to resubmit the rest. An emulation that carries on past an iovec creates holes in the byte stream; one that returns -1 after a successful transfer but a subsequent ",[36,2525,2526],{},"EAGAIN"," makes the caller believe nothing was sent and resend it. The correct approach is to stop at the first short transfer and return the accumulated value — the comment in ",[36,2529,2530],{},"posix_sockfd.c"," (\"we didn't send all the data, the caller will resubmit\") is the contract the caller relies on.",[17,2533,2534,2537,2538,2541,2542,569,2545,1233,2548,2551,2552,2555,2556,2559,2560,2563],{},[21,2535,2536],{},"Detail 2: Why atomic operations do not use pthread mutexes."," The intuitive approach is to embed a ",[36,2539,2540],{},"pthread_mutex_t"," in every atomic variable, but Zephyr's ",[36,2543,2544],{},"pthread_mutex_init()",[21,2546,2547],{},"allocates from a fixed pool",[36,2549,2550],{},"posix_mutex_pool",", bitmap allocation, returning ",[36,2553,2554],{},"ENOMEM"," once the pool is exhausted). Of the 26 places where nng initializes an atomic variable, only 5 have a matching ",[36,2557,2558],{},"nni_atomic_fini*()"," call to hand the slot back (three on the pipe, the message refcount, and the inproc pair); the rest have no corresponding release call. Using the kernel's own ",[36,2561,2562],{},"struct k_mutex"," avoids the problem: it is embedded, takes nothing from the pool, and needs no release.",[17,2565,2566,2575,2576,2579,2580,2583,2584,2587,2588,2591,2592,2595,2596,2599,2600,2603,2604,2607,2608,252],{},[21,2567,2568,2569,2574],{},"Detail 3: ",[21,2570,2571],{},[36,2572,2573],{},"ENABLE_LOG"," must reach both sides."," Whether nanolib's ",[36,2577,2578],{},"conf.c"," initializes the log backend and whether ",[36,2581,2582],{},"log_*()"," is compiled into an entity is decided by ",[36,2585,2586],{},"-DENABLE_LOG",", and it must be passed to ",[21,2589,2590],{},"both"," the application and libnng. The reason lies in the build system: nng's CMake only recognizes cache variables of the form ",[36,2593,2594],{},"NNG_*",", so plain macros must be routed in through ",[36,2597,2598],{},"CMAKE_C_FLAGS",". The consequence of passing it to only one side is silent — the link succeeds and the broker runs, but not a single log line appears (",[36,2601,2602],{},"conf->log.type"," is left uninitialized). The same applies to ",[36,2605,2606],{},"ACL_SUPP",", where an unsatisfied condition is more dangerous still: mismatched definitions on the two sides misalign the layout of ",[36,2609,2610],{},"struct conf",[122,2612,2614],{"id":2613},"_3-compile-time-macro-contract","3. Compile-Time Macro Contract",[260,2616,2617,2630],{},[263,2618,2619],{},[266,2620,2621,2624,2627],{},[269,2622,2623],{},"Macro",[269,2625,2626],{},"Purpose",[269,2628,2629],{},"If unsatisfied",[276,2631,2632,2647,2662,2685],{},[266,2633,2634,2638,2644],{},[281,2635,2636],{},[36,2637,2573],{},[281,2639,2640,2641,2643],{},"Makes nanolib initialize the log backend and compile ",[36,2642,2582],{}," into an entity",[281,2645,2646],{},"Logging silently stops working",[266,2648,2649,2653,2659],{},[281,2650,2651],{},[36,2652,2606],{},[281,2654,2655,2656,2658],{},"Makes ",[36,2657,2610],{}," carry the ACL fields (on by default)",[281,2660,2661],{},"Mismatched definitions on the two sides → struct layout is misaligned",[266,2663,2664,2669,2682],{},[281,2665,2666],{},[36,2667,2668],{},"SUPP_NANO_LIB",[281,2670,2671,2672,2674,2675,2678,2679],{},"Hides ",[36,2673,1807],{},"'s ",[36,2676,2677],{},"main()"," while keeping ",[36,2680,2681],{},"get_cache_argc\u002Fargv",[281,2683,2684],{},"Conflicts with the demo's own entry point",[266,2686,2687,2692,2697],{},[281,2688,2689],{},[36,2690,2691],{},"SUPP_SYSLOG",[281,2693,2694],{},[21,2695,2696],{},"Deliberately left unset",[281,2698,2699,2700,2703],{},"Would look for ",[36,2701,2702],{},"syslog()",", which Zephyr does not have",[122,2705,2707],{"id":2706},"_4-two-framework-level-traps","4. Two Framework-Level Traps",[17,2709,2710],{},"Both issues described below come from the same type of problem: Zephyr's API behavior differs from what might be expected, and failures produce no error messages, making them expensive to diagnose.",[2712,2713,2715],"h4",{"id":2714},"_1-psram-heap-corruption-on-the-first-client-connection","1. PSRAM Heap Corruption on the First Client Connection",[17,2717,2718],{},"The ESP32-S3 has about 512 KB of internal SRAM, of which around 416 KB is available. The broker's data plane requires several megabytes, so PSRAM is necessary.",[17,2720,2721,2722,2725,2726,2729,2730,2733],{},"Zephyr provides ",[36,2723,2724],{},"shared_multi_heap"," for managing multiple heap regions, which initially seems like a good fit. However, ",[21,2727,2728],{},"it is not thread-safe"," because its underlying ",[36,2731,2732],{},"sys_heap"," is accessed without locking.",[17,2735,2736,2737,252],{},"NanoNNG allocates memory from multiple threads, including the poller, the taskq workers, and the aio associated with each connection. As a result, ",[21,2738,2739],{},"the heap is corrupted when the first client connection is established",[17,2741,2742,2743,2746,2747,2750,2751,2753],{},"The solution is to avoid ",[36,2744,2745],{},"shared_multi_heap_alloc()"," and instead use a regular ",[36,2748,2749],{},"struct k_heap"," with locking on the PSRAM window. Static pools that do not need to reside in SRAM, including the Wi-Fi driver's ",[36,2752,1262],{}," section, the net_buf pools, and the Zephyr POSIX object pools (about 60 KB), are also moved to PSRAM. This leaves the internal SRAM available for the parts that actually require it.",[2712,2755,2757,2758,2761],{"id":2756},"_2-net_mgmt-callback-masks-are-not-matched-bitwise","2. ",[36,2759,2760],{},"net_mgmt"," Callback Masks Are Not Matched Bitwise",[17,2763,2764],{},"The Wi-Fi connection process needs to listen for multiple events. It is intuitive to combine them into a single mask and register one callback:",[227,2766,2771],{"className":2767,"code":2769,"language":2770,"meta":233},[2768],"language-c","\u002F* Incorrect: the two events use different layer codes *\u002F\nnet_mgmt_init_event_callback(&cb, handler,\n    NET_EVENT_WIFI_CONNECT_RESULT | NET_EVENT_IPV4_DHCP_BOUND);\nnet_mgmt_add_event_callback(&cb);\n","c",[36,2772,2769],{"__ignoreMap":233},[17,2774,2775,2776,2779],{},"With this configuration, the callback ",[21,2777,2778],{},"is never triggered",", and no log or error code is generated.",[17,2781,2782,2783,1233,2786,2789,2790,2793],{},"The reason is in ",[36,2784,2785],{},"mgmt_run_slist_callbacks()",[36,2787,2788],{},"subsys\u002Fnet\u002Fip\u002Fnet_mgmt.c","). When checking whether a callback matches an event, it uses ",[21,2791,2792],{},"full layer-code equality",", rather than a bitwise AND:",[227,2795,2798],{"className":2796,"code":2797,"language":2770,"meta":233},[2768],"if (!(NET_MGMT_GET_LAYER(mgmt_event->event) ==\n      NET_MGMT_GET_LAYER(cb->event_mask)) ||\n    !(NET_MGMT_GET_LAYER_CODE(mgmt_event->event) ==\n      NET_MGMT_GET_LAYER_CODE(cb->event_mask)) || ...\n",[36,2799,2797],{"__ignoreMap":233},[17,2801,2802,2803,2806,2807,2810,2811,2814,2815,2818],{},"When events with different layer codes are combined into one mask, the layer code in ",[36,2804,2805],{},"cb->event_mask"," becomes the bitwise OR of the two codes, and so matches neither event. For these two events, the Wi-Fi event has a layer code of ",[36,2808,2809],{},"0x0D"," while IPv4 uses ",[36,2812,2813],{},"0x03",". Their bitwise OR is ",[36,2816,2817],{},"0x0F",", which is not equal to either value.",[17,2820,2821,2822,2825],{},"The correct approach is to ",[21,2823,2824],{},"register one callback for each event",". The same handler can be reused:",[227,2827,2830],{"className":2828,"code":2829,"language":2770,"meta":233},[2768],"\u002F* Correct: one callback structure per event *\u002F\nnet_mgmt_init_event_callback(&wifi_cb, handler, NET_EVENT_WIFI_CONNECT_RESULT);\nnet_mgmt_add_event_callback(&wifi_cb);\n\nnet_mgmt_init_event_callback(&dhcp_cb, handler, NET_EVENT_IPV4_DHCP_BOUND);\nnet_mgmt_add_event_callback(&dhcp_cb);\n",[36,2831,2829],{"__ignoreMap":233},[12,2833,2835],{"id":2834},"conclusion","Conclusion",[17,2837,2838],{},"The value of running the broker on an MCU lies in eliminating one edge gateway: the same development board already deployed in the field can run a broker on the side, consolidating heterogeneous devices into MQTT and buffering locally when the link goes down.",[122,2840,2842],{"id":2841},"where-this-approach-fits","Where This Approach Fits",[17,2844,2845,2846,2848],{},"Taken together with the list in ",[30,2847,43],{"href":42},", the current implementation suits small-scale edge aggregation and protocol conversion inside a controlled network. The following points should be settled before making a selection:",[71,2850,2851,2858,2861,2864],{},[74,2852,2853,2854,2857],{},"TLS, disk persistence, and MQTT-side authentication are not included (all three are marked as planned in the \"Future plan\" column of Appendix A), so ",[21,2855,2856],{},"at this stage it is not suitable as a broker on the public Internet or an untrusted network","; the demos in this article listen on all interfaces by default, and although REST provides Basic authentication, it is unencrypted and its credentials must be configured by you (there are no usable default credentials). See \"Security Considerations\".",[74,2859,2860],{},"There is no filesystem, so cached messages and persistent sessions do not survive a power loss.",[74,2862,2863],{},"Concurrency is constrained by the Zephyr thread quota. This article does not publish formal throughput or concurrency benchmarks; the official performance figures cited in Appendix A come from multicore POSIX environments and do not represent the performance of this demo.",[74,2865,2866,2867,2869],{},"Apart from ",[36,2868,50],{}," and ESP32-S3, no other boards have been validated.",[122,2871,2873],{"id":2872},"code-and-demos","Code and Demos",[17,2875,2876,2877,2879,2880,2885,2886,2889,2890,2895],{},"The port is available in the ",[36,2878,327],{}," branch of ",[30,2881,2884],{"href":2882,"rel":2883},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002Fnanomq\u002Ftree\u002Fzephyr-rtos",[175],"nanomq\u002Fnanomq",". The demos have moved to the ",[36,2887,2888],{},"examples\u002F"," directory of ",[30,2891,2894],{"href":2892,"rel":2893},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002FNanoMQ-Zephyr-Example\u002Ftree\u002Fmain\u002Fexamples",[175],"nanomq\u002FNanoMQ-Zephyr-Example",", and their READMEs document the complete bring-up process.",[17,2897,2898,2899,252],{},"If you validate the port on another board or encounter new issues, you are welcome to join the discussion in the ",[30,2900,2903],{"href":2901,"rel":2902},"https:\u002F\u002Fgithub.com\u002Fnanomq\u002Fnanomq\u002Fdiscussions",[175],"NanoMQ community",[12,2905,2907],{"id":2906},"appendix-a-differences-from-general-nanomq","Appendix A: Differences from General NanoMQ",[17,2909,2910,2911,2914],{},"This Zephyr port is ",[21,2912,2913],{},"not an equivalent replacement for general NanoMQ",". It differs from the general version in several areas, and these differences should be considered when evaluating the port for a specific use case.",[17,2916,2917,2920,2921,2923,2924,252],{},[21,2918,2919],{},"Baseline version."," The port is based on upstream ",[36,2922,307],{},", that is, NanoMQ ",[21,2925,2926],{},"0.25.6",[17,2928,2929,2932,2933,2936,2937,2940,2941,2943,2944,2947],{},[21,2930,2931],{},"Configuration source."," The general NanoMQ version reads its configuration from ",[36,2934,2935],{},"nanomq.conf",". The Zephyr version instead uses an ",[21,2938,2939],{},"embedded minimal configuration",": built-in defaults from ",[36,2942,1907],{}," are combined with startup code that directly sets key fields such as listener addresses, bypassing the configuration file parser. The ",[36,2945,2946],{},"conf"," structure and its semantics remain unchanged; only the configuration source changes from a file to in-memory construction.",[17,2949,2950,2953,2954,2957,2958,2961,2962,225],{},[21,2951,2952],{},"Feature Availability."," Peripheral features (REST, the rule engine, bridging, and so on) are ",[21,2955,2956],{},"fully compiled and controlled at runtime",", rather than being removed at compile time, which keeps the behavior of the same source code consistent across platforms. The following table lists the capabilities that are unavailable or unverified in this version, distinguishing ",[21,2959,2960],{},"work not yet finished (planned)"," from ",[21,2963,2964],{},"limited by the platform or its dependencies (not planned)",[260,2966,2967,2980],{},[263,2968,2969],{},[266,2970,2971,2974,2977],{},[269,2972,2973],{},"Capability",[269,2975,2976],{},"Status",[269,2978,2979],{},"Future plan",[276,2981,2982,2999,3015,3032,3052,3065,3078,3106],{},[266,2983,2984,2987,2994],{},[281,2985,2986],{},"Filesystem-related",[281,2988,2989,2990,2993],{},"Configuration, logs, and persistent sessions are not written to disk; ",[36,2991,2992],{},"$SYS"," only reflects runtime state",[281,2995,2996,2998],{},[21,2997,722],{},": mounting Zephyr's fs subsystem (fat \u002F ext2 \u002F fcb, etc.) is all that is needed",[266,3000,3001,3004,3010],{},[281,3002,3003],{},"SQLite persistence",[281,3005,3006,3007,308],{},"Not included (",[36,3008,3009],{},"NNG_ENABLE_SQLITE=OFF",[281,3011,3012,3014],{},[21,3013,722],{},": enabled together with the filesystem",[266,3016,3017,3020,3027],{},[281,3018,3019],{},"Parquet",[281,3021,3022,3023,3026],{},"Not included. Controlled by the ",[36,3024,3025],{},"SUPP_PARQUET"," macro (not a CMake option); undefined means it is not compiled in",[281,3028,3029,3031],{},[21,3030,722],{},": as above; depends on the filesystem",[266,3033,3034,3037,3047],{},[281,3035,3036],{},"TLS",[281,3038,3006,3039,3042,3043,3046],{},[36,3040,3041],{},"NNG_ENABLE_TLS=OFF","). Zephyr provides mbedTLS (enabled in this build) and the ",[36,3044,3045],{},"tls_credentials"," subsystem",[281,3048,3049,3051],{},[21,3050,722],{},": once enabled, MQTTS \u002F WSS \u002F HTTPS become available",[266,3053,3054,3057,3060],{},[281,3055,3056],{},"MQTT-side authentication (password \u002F ACL)",[281,3058,3059],{},"Neither wired up nor verified; any client can connect today",[281,3061,3062,3064],{},[21,3063,722],{},": a matter of wiring up the configuration and validating it",[266,3066,3067,3070,3073],{},[281,3068,3069],{},"Rule engine",[281,3071,3072],{},"The embedded configuration has no corresponding switch path; unverified",[281,3074,3075,3077],{},[21,3076,722],{},": a pure software module, with only the configuration wiring missing",[266,3079,3080,3083,3097],{},[281,3081,3082],{},"IPC transport",[281,3084,3085,3086,3088,3089,3092,3093,3096],{},"Turned off in the demos (",[36,3087,770],{}," sets ",[36,3090,3091],{},"ipc_internal = false","; a runtime assignment, not a build option), so the ",[36,3094,3095],{},"nanomq ctl"," management channel is unavailable",[281,3098,3099,3102,3103],{},[21,3100,3101],{},"Pending platform support",": requires named AF_UNIX, while Zephyr currently offers only anonymous ",[36,3104,3105],{},"socketpair",[266,3107,3108,3111,3117],{},[281,3109,3110],{},"QUIC",[281,3112,3006,3113,3116],{},[36,3114,3115],{},"NNG_ENABLE_QUIC=OFF","). nng's QUIC implementation depends on msquic",[281,3118,3119,3122],{},[21,3120,3121],{},"Not planned",": the dependency footprint does not fit an embedded target",[17,3124,3125,3128,3129,3132,3133,3136],{},[21,3126,3127],{},"Thread model."," The Linux version relies on a full POSIX dynamic thread pool. On Zephyr, the number of NanoNNG threads is fixed (taskq = 2 \u002F poller = 1 \u002F expire = 1), and together with the Zephyr POSIX thread pool limit of ",[36,3130,3131],{},"CONFIG_POSIX_THREAD_THREADS_MAX=16"," — with each pthread's stack fixed at 16 KB by ",[36,3134,3135],{},"CONFIG_DYNAMIC_THREAD_STACK_SIZE"," — broker concurrency is constrained accordingly.",[17,3138,3139,3142,3143,3145,3146,3149,3150,3153],{},[21,3140,3141],{},"Performance data."," The figures published by ",[30,3144,33],{"href":32}," include: a startup footprint of under 200 KB with a minimal feature set; throughput of up to millions of TPS; and performance up to 10 times faster than Mosquitto on multicore CPUs. It is important to stress the scope of these figures: they come from benchmarks in ",[21,3147,3148],{},"multicore POSIX environments",", the official page does not give the hardware and configuration details of the corresponding tests, and they ",[21,3151,3152],{},"do not represent the performance of the demos in this article"," — the thread quota on the Zephyr side is far smaller than that of a multicore Linux system (see the previous point), so the actual concurrency characteristics differ. No formal performance testing was conducted on the demos in this article.",[17,3155,3156,3159,3160,3162,3163,3165,3166,3169,3170,3172],{},[21,3157,3158],{},"Memory allocation."," The allocator differs by target: ",[36,3161,50],{}," uses the libc ",[36,3164,1477],{},", whose arena is the broker heap (see the qemu section); the ESP32-S3 cannot fit the data plane in internal SRAM, so ",[36,3167,3168],{},"NNG_ZEPHYR_ALLOC_SMH"," is defined to switch nng's allocation to a ",[36,3171,1984],{}," on PSRAM (see \"The Porting Process → 4. Two Framework-Level Traps\").",[17,3174,3175,3178,3179,3181],{},[21,3176,3177],{},"Time source."," Both demos use real UTC, but obtain it differently: ",[36,3180,50],{}," seeds it from the QEMU CMOS RTC, while the ESP32-S3 has no RTC and seeds it through SNTP. Zephyr does not include a timezone database, so displayed time is always UTC.",[17,3183,3184,3187,3188,3190,3191,790,3193,790,3195,3197],{},[21,3185,3186],{},"Removed module."," The broker's ",[36,3189,1846],{}," depends on ",[36,3192,1852],{},[36,3194,1855],{},[36,3196,1858],{}," and cannot be compiled on Zephyr; it is replaced by a stub in the demo that provides the same symbols. Those symbols are called either from daemon and CLI paths or not at all, so the embedded broker does not reach them (see the corresponding table entry in \"The Porting Process → 1. Application Layer\").",[17,3199,3200,569,3203,3205,3206,3209,3210,3213,3214,3217,3218,3221],{},[21,3201,3202],{},"Validated platforms.",[36,3204,50],{}," (32-bit) and ESP32-S3. The build system enables the atomic-operation fallback (",[36,3207,3208],{},"NNG_ZEPHYR_NO_STDATOMIC",", see Detail 2 in \"The Porting Process → 2. Platform Adaptation Layer\") for ",[21,3211,3212],{},"every 32-bit non-x86 target"," (ARM, RISC-V, Xtensa, and so on) — ",[21,3215,3216],{},"the ESP32-S3 is itself using it",", so that path is covered by the hardware validation. What remains unvalidated is the ARM \u002F RISC-V ",[21,3219,3220],{},"board"," itself (future plans: validating on ARM \u002F RISC-V and other boards, where the network driver, interrupts, and memory budget would all need to be revalidated).",[12,3223,3225],{"id":3224},"appendix-b-comparing-the-two-demos","Appendix B: Comparing the Two Demos",[260,3227,3228,3244],{},[263,3229,3230],{},[266,3231,3232,3234,3239],{},[269,3233],{},[269,3235,3236],{},[36,3237,3238],{},"demo\u002Fnanomq_zephyr_esp32s3",[269,3240,3241],{},[36,3242,3243],{},"demo\u002Fnanomq_zephyr_qemu_x86",[276,3245,3246,3260,3273,3286,3299,3312],{},[266,3247,3248,3253,3256],{},[281,3249,3250],{},[21,3251,3252],{},"Target platform",[281,3254,3255],{},"ESP32-S3 hardware",[281,3257,3258],{},[36,3259,50],{},[266,3261,3262,3267,3270],{},[281,3263,3264],{},[21,3265,3266],{},"Hardware requirement",[281,3268,3269],{},"ESP32-S3 development board",[281,3271,3272],{},"None",[266,3274,3275,3280,3283],{},[281,3276,3277],{},[21,3278,3279],{},"Network",[281,3281,3282],{},"Wi-Fi STA + DHCP",[281,3284,3285],{},"QEMU SLIRP user-mode networking",[266,3287,3288,3293,3296],{},[281,3289,3290],{},[21,3291,3292],{},"Memory \u002F storage",[281,3294,3295],{},"16 MB flash + octal PSRAM (data plane in PSRAM)",[281,3297,3298],{},"31 MB simulated RAM, no persistent storage",[266,3300,3301,3306,3309],{},[281,3302,3303],{},[21,3304,3305],{},"Additional prerequisite",[281,3307,3308],{},"Wi-Fi credentials",[281,3310,3311],{},"Zephyr e1000 driver patch",[266,3313,3314,3319,3322],{},[281,3315,3316],{},[21,3317,3318],{},"Typical use",[281,3320,3321],{},"Real-world evaluation and hardware testing",[281,3323,3324],{},"Quick verification during development",[17,3326,3327],{},"Both demos share the same application source and the same NanoNNG ExternalProject build; they differ only in board support and the network layer.",[17,3329,3330,3331,3333],{},"For development, ",[36,3332,50],{}," is recommended. A complete build-and-run cycle takes only a few seconds, making it significantly faster than flashing and resetting the hardware.",[17,3335,3336],{},"For real-world evaluation and hardware testing, use the ESP32-S3 demo.",[17,3338,3339],{},"The complete build and run procedures for both demos are provided in the corresponding sections of this article.",{"title":233,"searchDepth":3341,"depth":3341,"links":3342},2,[3343,3344,3345,3350,3358,3366,3372,3373,3379,3383,3384],{"id":14,"depth":3341,"text":15},{"id":62,"depth":3341,"text":63},{"id":114,"depth":3341,"text":115,"children":3346},[3347,3349],{"id":124,"depth":3348,"text":125},3,{"id":163,"depth":3348,"text":164},{"id":187,"depth":3341,"text":188,"children":3351},[3352,3353,3354,3355,3356,3357],{"id":191,"depth":3348,"text":192},{"id":351,"depth":3348,"text":352},{"id":523,"depth":3348,"text":524},{"id":594,"depth":3348,"text":595},{"id":682,"depth":3348,"text":683},{"id":804,"depth":3348,"text":805},{"id":912,"depth":3341,"text":913,"children":3359},[3360,3361,3362,3363,3364,3365],{"id":916,"depth":3348,"text":917},{"id":954,"depth":3348,"text":955},{"id":1013,"depth":3348,"text":1014},{"id":1083,"depth":3348,"text":1084},{"id":1140,"depth":3348,"text":1141},{"id":1180,"depth":3348,"text":1009},{"id":1279,"depth":3341,"text":1280,"children":3367},[3368,3369,3370,3371],{"id":1283,"depth":3348,"text":1284},{"id":1331,"depth":3348,"text":1332},{"id":1428,"depth":3348,"text":1429},{"id":1464,"depth":3348,"text":1465},{"id":1493,"depth":3341,"text":1494},{"id":1735,"depth":3341,"text":1736,"children":3374},[3375,3376,3377,3378],{"id":1753,"depth":3348,"text":1754},{"id":1917,"depth":3348,"text":1918},{"id":2613,"depth":3348,"text":2614},{"id":2706,"depth":3348,"text":2707},{"id":2834,"depth":3341,"text":2835,"children":3380},[3381,3382],{"id":2841,"depth":3348,"text":2842},{"id":2872,"depth":3348,"text":2873},{"id":2906,"depth":3341,"text":2907},{"id":3224,"depth":3341,"text":3225},"\u002Fimages\u002Fblog\u002Fen\u002Fnanomq-on-zephyr-rtos\u002Fcover.webp",684,1520,"2026-09-29","Run the NanoMQ MQTT broker directly on an MCU with Zephyr RTOS, without a separate Linux gateway. Covers the ESP32-S3 and qemu_x86 demos, the NanoNNG platform layer, and current limitations.",false,"md",{},true,"\u002Fblog\u002Fnanomq-on-zephyr-rtos",{"title":6,"description":3389},{"loc":3394,"lastmod":3388},"blog\u002Fnanomq-on-zephyr-rtos",[],"bFNvC-hCB3FctyEuI3LwlCTq8wzIEx2GiSXa9AFA0RE","\u002Fzh\u002Fblog\u002Fnanomq-on-zephyr-rtos",1794542896597]