Start by building a minimal Zephyr application that enables the MQTT shell backend.
The application doesn’t need networking code in main.c. Zephyr starts the shell, network stack, DHCP client, and MQTT shell backend from configuration options in prj.conf.
When CONFIG_SHELL_BACKEND_MQTT=y is enabled, Zephyr’s MQTT shell backend connects to a broker and uses MQTT topics to carry shell traffic.
The default topic pattern is:
| Direction | Topic |
|---|---|
| Commands sent to the board | <device_id>/sh/rx |
| Responses sent from the board | <device_id>/sh/tx |
The backend derives <device_id> from the hardware device ID, so each board uses a unique topic prefix. You’ll read this ID from the broker after the board connects for the first time.
To create an application project in Workbench for Zephyr:
zephyr).zephyr-sdk-1.0.1.frdm_mcxn947/mcxn947/cpu0).hello_world.mqtt_shell_backend.zephyr/apps).
Workbench for Zephyr application project wizard
The hello_world sample provides a working CMakeLists.txt, prj.conf, and src/main.c. Leave CMakeLists.txt unchanged, and replace prj.conf and src/main.c.
Replace the contents of prj.conf with the following text:
# Shell and MQTT backend
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_MQTT=y
# Networking
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_TCP=y
CONFIG_NET_DHCPV4=y
CONFIG_NET_CONFIG_SETTINGS=y
# MQTT broker address
CONFIG_SHELL_MQTT_SERVER_ADDR="192.168.1.233"
# Optional shell modules and logging
CONFIG_NET_SHELL=y
CONFIG_LOG=y
# Resource tuning
CONFIG_HEAP_MEM_POOL_SIZE=8192
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_NET_PKT_RX_COUNT=16
CONFIG_NET_BUF_RX_COUNT=32
Replace 192.168.1.233 with the IP address of the host running Mosquitto, as seen from the board’s Ethernet network.
The values under # Resource tuning keep the shell footprint small on Cortex-M and allow the network stack and MQTT client to operate reliably.
DHCP is convenient, but it’s not required. To use a static address, remove:
CONFIG_NET_DHCPV4=y
Then, add values that match your network:
CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.168.1.50"
CONFIG_NET_CONFIG_MY_IPV4_NETMASK="255.255.255.0"
CONFIG_NET_CONFIG_MY_IPV4_GW="192.168.1.1"
Keep CONFIG_NET_CONFIG_SETTINGS=y enabled so Zephyr applies the network configuration at boot.
The shell and MQTT backend start from Zephyr initialization hooks.
Replace the contents of main.c with the following code:
int main(void)
{
return 0;
}
In the Workbench for Zephyr panel, select your project and build configuration. Select Build, then select Flash.
The FRDM-MCXN947 uses NXP LinkServer as the debug runner. If LinkServer isn’t installed, follow the Workbench for Zephyr prompt to install or configure it.
After building the Zephyr application, you’ll run an Eclipse Mosquitto broker in Docker.
Create a directory named mosquitto_shell on your host computer with the following structure:
mosquitto_shell/
|-- docker-compose.yml
`-- mosquitto/
`-- config/
`-- mosquitto.conf
Edit the docker-compose.yml file and add the following text:
services:
mosquitto:
image: eclipse-mosquitto:latest
container_name: mosquitto-mqtt
ports:
- "1883:1883"
volumes:
- ./mosquitto/config:/mosquitto/config:ro
- ./mosquitto/data:/mosquitto/data
- ./mosquitto/log:/mosquitto/log
restart: unless-stopped
Edit the mosquitto.conf file and add the following text:
listener 1883 0.0.0.0
allow_anonymous true
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log
log_dest stdout
log_type all
From the mosquitto_shell directory, start the broker:
docker compose up -d
Check that the container is running:
docker compose ps
The output is similar to:
NAME IMAGE STATUS
mosquitto-mqtt eclipse-mosquitto:latest Up
The eclipse-mosquitto image includes mosquitto_pub and mosquitto_sub, so you can run both tools from inside the container with docker exec.
If you prefer not to use Docker, you can install Mosquitto directly on your host computer. This gives you the mosquitto broker, mosquitto_pub, and mosquitto_sub commands locally.
Install Mosquitto with your operating system package manager.
On Linux (Debian or Ubuntu):
sudo apt install mosquitto mosquitto-clients
On macOS, use Homebrew:
brew install mosquitto
On Windows, use winget:
winget install EclipseFoundation.Mosquitto
After installation on Windows, add the Mosquitto install directory (typically C:\Program Files\mosquitto) to your PATH environment variable so that mosquitto_pub and mosquitto_sub are available from the command prompt.
After installing Mosquitto directly, start the broker using your operating system’s service manager, or run it manually with a local configuration file.
The remaining commands in this section use docker exec to run mosquitto_sub and mosquitto_pub inside the container. If you installed Mosquitto directly, drop the docker exec -it mosquitto-mqtt prefix and run the tools directly instead.
For example, this container command:
docker exec -it mosquitto-mqtt mosquitto_sub -h localhost -t "1a2b3c/sh/tx" -v
becomes:
mosquitto_sub -h localhost -t "1a2b3c/sh/tx" -v
The same substitution applies to every mosquitto_pub command later in this section.
The MQTT shell backend derives its client ID from the board’s hardware UID, so each board connects with topics under <device_id>/sh/rx and <device_id>/sh/tx.
There are two ways to read this ID:
If you have a serial terminal open on the board’s UART, the MQTT backend logs the topics it subscribes to and publishes from at boot.
A typical FRDM-MCXN947 boot log looks like this:
*** Booting Zephyr OS build v4.4.0 ***
[00:00:01.654,000] <inf> phy_mii: PHY (0) Link speed 100 Mb, full duplex
[00:00:01.654,000] <inf> eth_nxp_enet_qos_mac: Link is up
[00:00:01.656,000] <wrn> shell_mqtt: Network connected
[00:00:01.656,000] <inf> app_main: MQTT shell backend demo on frdm_mcxn947
[00:00:02.656,000] <inf> shell_mqtt: DNS resolved for 192.168.1.31:1883
[00:00:02.656,000] <err> shell_mqtt: mqtt_connect error: -22
[00:00:03.656,000] <inf> shell_mqtt: DNS resolved for 192.168.1.31:1883
[00:00:03.656,000] <err> shell_mqtt: mqtt_connect error: -22
[00:00:04.656,000] <inf> shell_mqtt: DNS resolved for 192.168.1.31:1883
[00:00:04.656,000] <err> shell_mqtt: mqtt_connect error: -22
[00:00:05.656,000] <inf> shell_mqtt: DNS resolved for 192.168.1.31:1883
[00:00:05.656,000] <err> shell_mqtt: mqtt_connect error: -22
[00:00:06.656,000] <inf> shell_mqtt: DNS resolved for 192.168.1.31:1883
[00:00:06.818,000] <wrn> shell_mqtt: MQTT client connected!
[00:00:07.823,000] <wrn> shell_mqtt: MQTT subscribe: ok
[00:00:07.823,000] <inf> shell_mqtt: Logs will be published to: 1a2b3c/sh/tx
[00:00:07.823,000] <inf> shell_mqtt: Subscribing shell cmds from: 1a2b3c/sh/rx
The two <inf> shell_mqtt: lines in the last two lines of the log give you the device ID directly. In this example, it’s 1a2b3c.
The mqtt_connect error: -22 lines you see during the first few seconds are expected. The Zephyr MQTT client retries the connect handshake until the broker accepts it, usually within a few attempts. The connection completes when you see MQTT client connected!, followed by MQTT subscribe: ok.
If you don’t have a serial terminal open, watch every shell-output topic with a wildcard:
docker exec -it mosquitto-mqtt mosquitto_sub -h localhost -t "+/sh/tx" -v
When the board connects, it publishes its shell prompt to <device_id>/sh/tx. The topic prefix on the first message is the device ID:
1a2b3c/sh/tx mqtt:~$
You can also confirm the same value from the broker’s connection log:
docker logs mosquitto-mqtt | grep "New client connected"
The device ID appears in the client connection entry:
New client connected from 192.168.65.1:65496 as 1a2b3c (p4, c0, k60).
Narrow the subscription to your board’s response topic only. Replace 1a2b3c with your device ID:
docker exec -it mosquitto-mqtt mosquitto_sub -h localhost -t "1a2b3c/sh/tx" -v
Keep this terminal open. Every shell response from the board will print here.
You’ll now send shell commands to the board using the Mosquitto command-line tools.
Open a second terminal to publish commands to the board:
printf 'kernel version\n' | docker exec -i mosquitto-mqtt mosquitto_pub -h localhost -t "1a2b3c/sh/rx" -s
The command is published to Mosquitto and the board executes it.
The response appears in the first terminal:
1a2b3c/sh/tx Zephyr version 4.4.0
Try the following commands:
| Command | Description |
|---|---|
kernel version | Print the Zephyr version string. |
kernel uptime | Print the time since boot. |
kernel thread list | Print the thread list and stack usage. |
net iface | Print network interface configuration, including the assigned IPv4 address. |
net ping <ip> | Send ICMP echo requests to a reachable host (for example, your gateway). |
device list | Print registered Zephyr devices. |
help | Print available shell modules and commands. |
The net commands are available because you set CONFIG_NET_SHELL=y in prj.conf. Without that option, only the kernel, device, and shell modules are exposed.
Example output for kernel uptime:
1a2b3c/sh/tx Uptime: 55310 ms
Truncated example output for kernel thread list on the FRDM-MCXN947 (the full list shows the network and Ethernet driver threads):
1a2b3c/sh/tx Scheduler: 1046 since last call
Threads:
*0x300015d0 shell_mqtt
options: 0x0, priority: 14 timeout: 0
state: queued, entry: 0x100069e5
stack size 2048, unused 1096, usage 952 / 2048 (46 %)
0x30001c30 sh_mqtt_workq
options: 0x0, priority: -9 timeout: 800
state: pending, entry: 0x100223cd
stack size 2048, unused 1160, usage 888 / 2048 (43 %)
0x30002688 conn_mgr_monitor
options: 0x0, priority: -1 timeout: 0
state: pending, entry: 0x1001ea0d
stack size 512, unused 264, usage 248 / 512 (48 %)
0x300027c0 ENETQOS_RX
options: 0x0, priority: -13 timeout: 0
state: pending, entry: 0x100223cd
stack size 1024, unused 752, usage 272 / 1024 (26 %)
0x300028b0 idle
options: 0x1, priority: 15 timeout: 0
state: , entry: 0x1003a335
stack size 320, unused 256, usage 64 / 320 (20 %)
The * next to shell_mqtt marks the running thread, which is the shell that executed the command. Note that responses larger than the MQTT buffer are split across multiple publishes. mosquitto_sub -v reassembles them sequentially under the 1a2b3c/sh/tx topic prefix.
Example output for net iface on the FRDM-MCXN947, truncated to the IPv4 section:
1a2b3c/sh/tx Default interface: 1
Interface eth0 (0x30000c70) (Ethernet) [1]
===================================
Link addr : XX:XX:XX:XX:XX:XX
MTU : 1500
Flags : AUTO_START,IPv4,IPv6
Status : oper=UP, admin=UP, carrier=ON
Ethernet link speed: 100 Mbits full-duplex
IPv4 unicast addresses (max 1):
192.168.1.41/255.255.255.0 DHCP preferred
IPv4 gateway : 192.168.1.1
DHCPv4 state : bound
DHCPv4 server : 192.168.1.1
Next, run net ping with the gateway address shown by net iface. The board sends three ICMP echo requests by default.
Example output for net ping 192.168.1.1 (board pinging its gateway):
1a2b3c/sh/tx PING 192.168.1.1
28 bytes from 192.168.1.1 to 192.168.1.41: icmp_seq=1 ttl=64 time=0 ms
28 bytes from 192.168.1.1 to 192.168.1.41: icmp_seq=2 ttl=64 time=0 ms
28 bytes from 192.168.1.1 to 192.168.1.41: icmp_seq=3 ttl=64 time=0 ms
The MQTT shell backend executes a command after it receives a newline character. The printf 'kernel version\n' | ... mosquitto_pub -s form sends the command with the required newline.
The application is portable across any Zephyr-supported board with Ethernet, because the MQTT backend is selected through Kconfig and there’s no board-specific code in main.c. You don’t need to recreate the project to test it on another board.
To change the target board on an existing project:
After the pristine build completes, flash the board as before. The same prj.conf and main.c work without changes, and the MQTT backend connects as soon as the new board acquires an IPv4 address.
You need a pristine build when you change the board because Workbench for Zephyr caches board-specific generated files (device tree, Kconfig, linker script) in the build directory. Without a clean rebuild, the previous board’s configuration leaks into the new build and produces incorrect binaries.
You now have a working Zephyr shell over MQTT on the FRDM-MCXN947.
In the next section, you’ll enable the UART shell backend on the FRDM-MCXN947. You’ll interact with the shell through a USB serial connection.