Using the Moddable SDK with Zephyr
Copyright 2025-2026 Moddable Tech, Inc.
Updated: February 11, 2026
This document is a guide to building Moddable apps with the Zephyr Project SDK.
Before you can build applications, you need to:
- Install the Moddable SDK and build its tools
- Install the Zephyr SDK and Tools
The instructions below verify your setup by running the helloworld example on your device using mcconfig, the command line tool to build and run applications using the Moddable SDK.
See the Tools documentation for more information about mcconfig
To build for a Zephyr board, run mcconfig with zephyr/<board> for the platform identifier. For example, to build for the ST Nucleo F413ZH:
mcconfig -d -m -p zephyr/nucleo_f413zh
Zephyr supports many boards. To add Moddable support for a board that has not yet been tested, see Adding a new board.
You may also need to install tools for your specific target device. For example, devices from STMicroelectronics require the STM32CubeProgrammer software.
The Moddable SDK on Zephyr supports three kinds of builds: debug, instrumented, and release. Each is appropriate for a different stage in the product development process. Select which kind of build you want from the command line when running mcconfig.
A debug build is used for debugging JavaScript. In a debug build, the device attempts to connect to xsbug at startup over USB or serial depending on the device configuration. Symbols will be included for native gdb debugging.
The -d option on the mcconfig command line selects a debug build.
An instrumented build is used for debugging native code. In an instrumented build, the JavaScript debugger is disabled. The instrumentation data displayed graphically in xsbug is instead output to the serial console once a second.
The -i option on the mcconfig command line selects an instrumented build.
A release build is for production. In a release build, the JavaScript debugger is disabled, instrumentation statistics are not collected, and serial console output is not used.
Omitting both the -d and -i options on the mcconfig command line selects a release. Note that -r specifies display rotation rather than selecting a release build.
The Moddable SDK build uses Zephyr SDK v4.3 or later.
-
Install the Moddable SDK tools by following the instructions in the Getting Started document.
-
Create an zephyrproject directory in your home directory at ~/zephyrproject for required third party SDKs and tools.
mkdir ~/zephyrproject
cd ~/zephyrproject
-
Set the environment ZEPHYR_BASE
export ZEPHYR_BASE=~/zephyrproject/zephyr
-
Install Zephyr requirements:
brew install cmake ninja gperf python3 python-tk ccache qemu dtc libmagic wget openocd
-
Create a new virtual environment (one time):
python3 -m venv ~/zephyrproject/.venv
-
Activate the virtual environment (for each new shell):
source ~/zephyrproject/.venv/bin/activate
Note: You can deactivate the virtual environment by typing deactivate in your shell.
-
Install the west tool:
-
Get the Zephyr SDK
west init ~/zephyrproject
cd ~/zephyrproject
west update
-
Install Python dependencies using west packages.
west packages pip --install
-
Install the Zephyr SDK
cd ~/zephyrproject/zephyr
west sdk install
-
Verify Zephyr SDK installation by building the blinky sample for your board.
west build -p always -b nucleo_f413zh samples/basic/blinky
You can then flash the software to run it.
-
Verify the complete setup by building helloworld for your device target:
cd ${MODDABLE}/examples/helloworld
mcconfig -d -m -p zephyr/nucleo_f413zh
-
The device should connect to xsbug and stop at the debugger statement.
Different silicon families connect in different ways. For many ST boards, for example, the Moddable SDK build can automatically detect the correct port. For ESP boards, you specify the serial port by setting the DEBUGGER_PORT environment variable:
DEBUGGER_PORT=/dev/tty.usbserial-1410 mcconfig -d -m -p zephyr/esp32_ethernet_kit
Other silicon families may have different requirements.
-
Install the Moddable SDK tools by following the instructions in the Getting Started document.
-
Install Zephyr for Windows by following the instructions in the Zephyr Getting Started Guide. A summary is presented below:
Note: Moddable tools use the CMD shell. When installing Zephyr from the instructions, choose the Batchfile instructions.
-
Install Zephyr requirements:
winget install Kitware.CMake Ninja-build.Ninja oss-winget.gperf Python.Python.3.12 Git.Git oss-winget.dtc wget 7zip.7zip
-
Create an zephyrproject directory in your home directory at %USERPROFILE%\zephyrproject for required third party SDKs and tools.
cd %USERPROFILE%
mkdir zephyrproject
cd zephyrproject
-
Create a new virtual environment (one time):
-
Activate the virtual environment (for each new shell):
zephyrproject\.venv\Scripts\activate.bat
Note: You can deactivate the virtual environment by typing deactivate in your shell.
-
Install the west tool:
-
Get the Zephyr source code
cd %USERPROFILE%
west init zephyrproject
cd zephyrproject
west update
-
The Zephyr west extension command, west packages can be used to install Python dependencies.
cmd /c zephyr\scripts\utils\west-packages-pip-install.cmd
-
Install the Zephyr SDK
cd %USERPROFILE%\zephyrproject\zephyr
west sdk install
-
Set the environment variables ZEPHYR_BASE and DEBUGGER_PORT using the instructions below:
Open the "Environment Variables" dialog of the Control Panel app by following these instructions. From that dialog:
-
Create a User Variable called ZEPHYR_BASE and set it to %USERPROFILE%\zephyrproject\zephyr
- Variable name:
ZEPHYR_BASE
- Variable value (Use the "Browse Directory..." button to make this selection):
C:\Users\<user>\zephyrproject\zephyr
-
Create a User Variable called DEBUGGER_PORT: the COM port for your device, e.g. COM3. This is used to connect your device to the xsbug JavaScript debugger.
To identify the correct serial port, launch the Windows Device Manager. Open the "Ports (COM & LPT)" section, verify the Serial port adapter is displayed, and note the associated COM port (e.g. COM3).
-
Verify Zephyr SDK installation by building the blinky sample for your board.
cd %USERPROFILE%\zephyrproject\zephyr
west build -p always -b nucleo_f413zh samples/basic/blinky
You can then flash the software to run it.
-
Verify the complete setup by building helloworld for your device target:
cd %MODDABLE%\examples\helloworld
mcconfig -d -m -p zephyr/nucleo_f413zh
-
The device should connect to xsbug and stop at the debugger statement.
Note: Make sure you have built the Moddable tools in the Moddable Getting Started step.
-
Install the Moddable SDK tools by following the instructions in the Getting Started document.
-
Create an zephyrproject directory in your home directory at ~/zephyrproject for required third party SDKs and tools.
mkdir ~/zephyrproject
cd ~/zephyrproject
-
Set the environment ZEPHYR_BASE
export ZEPHYR_BASE=~/zephyrproject/zephyr
-
Install Zephyr requirements:
sudo apt install --no-install-recommends git cmake ninja-build gperf \
ccache dfu-util device-tree-compiler wget python3-dev python3-venv python3-tk
xz-utils file make gcc gcc-multilib g++-multilib libsdl2-dev libmagic1
```
-
Create a new virtual environment (one time):
python3 -m venv ~/zephyrproject/.venv
-
Activate the virtual environment (for each new shell):
source ~/zephyrproject/.venv/bin/activate
Note: You can deactivate the virtual environment by typing deactivate in your shell.
-
Install the west tool:
-
Get the Zephyr source code
west init ~/zephyrproject
cd ~/zephyrproject
west update
-
The Zephyr west extension command, west packages can be used to install Python dependencies.
west packages pip --install
-
Install the Zephyr SDK
cd ~/zephyrproject/zephyr
west sdk install
-
Verify Zephyr SDK installation by building the blinky sample for your board.
west build -p always -b nucleo_f413zh samples/basic/blinky
You can then flash the software to run it.
-
Verify the complete setup by building helloworld for your device target:
cd ${MODDABLE}/examples/helloworld
mcconfig -d -m -p zephyr/nucleo_f413zh
-
The device should connect to xsbug and stop at the debugger statement.
Installation failure
If there is an error with libusb when trying to install, change the permissions on the device:
- west flash: using runner stm32cubeprogrammer
-------------------------------------------------------------------
STM32CubeProgrammer v2.20.0
-------------------------------------------------------------------
libusb: error [get_usbfs_fd] libusb couldn't open USB device /dev/bus/usb/001/077, errno=13
libusb: error [get_usbfs_fd] libusb requires write access to USB device nodes
Change permissions on the device noted and try again.
sudo chmod a+rw /dev/bus/usb/001/077
Using Zephyr Device Tree in JavaScript
Most embedded operating systems start with C code. The header files define how software accesses the hardware. Not so with Zephyr. Instead, Zephyr begins with a Device Tree stored in a *.dts file. The Device Tree uses its own file format, similar to XML, JSON, and YAML. It defines all the IO from clocks, timers, and GPIOs to Wi-Fi, sensors, and displays. The Device Tree was adapted from Linux.
Zephyr uses the Device Tree to generate extremely efficient C code. For example, all IO is accessed through static data structures created by C macros. If a project tries to use IO that doesn't exist, it won't even run - it fails to compile or link.
As powerful as the Device Tree is, it creates some challenges for a dynamic language like JavaScript. For example, JavaScript code expects to be able to discover IO capabilities at runtime by inspecting JavaScript objects. But, the capabilities depend on the development board. Fortunately, ECMA-419 has a solution for this in the opaquely named Host Provider Instance, commonly known in JavaScript as the device global variable.
For the Zephyr port, our mcdevicetree tool generates the JavaScript code and TypeScript declarations for the device global from the Device Tree. Let's look at how the Device Tree works in JavaScript. Many Zephyr development boards have a button that is mapped to the name sw0. Here's the JavaScript code to receive notifications when the button is pressed and released:
new device.button.sw0({
onReadable() {
trace(`sw0 state: ${this.read()}\n`);
}
});
Zephyr's blinky sample in JavaScript is equally straightforward:
const led = new device.led.led0();
let state = 1;
Timer.repeat(() => {
state = !state;
led.write(state);
}, 1000);
The Device Tree takes care of details like the GPIO port name and pin number to use, whether the GPIO is active high or low, and whether a pull-up resistor is needed.
This simplicity extends to other kinds of IO. Here's a serial echo app (Zephyr C version here):
new device.serial.usart6({
onReadable() {
this.write(this.read());
}
});
The names usart6, sw0, and led0 are from the Device Tree. Both labels (e.g. usart6) and aliases (e.g. sw0) are available in JavaScript. You can look these up in the Device Tree source file like a C programmer. Or your JavaScript code can discover them. For example, to list all the serial ports available, use the standard JavaScript for-in statement:
for (const port in device.serial)
trace(port, "\n");
Even better, you can display them interactively in xsbug, our JavaScript debugger, by inspecting the device global variable.
If your Moddable SDK project manifest includes networking support, for example by including manifest_net.json, mcdevicetree includes access to all Wi-Fi and Ethernet interfaces. These objects implement the ECMA-419 Wi-Fi and Ethernet APIs.
If you are using Wi-Fi, you don't need to do anything special to connect to the network beyond providing the SSID and password for your Wi-Fi access point when building:
mcconfig -d -m -p zephyr/board_with_wifi ssid="My Wi-Fi" password="secret"
If you are using Ethernet, the Moddable SDK runtime automatically attempts to connect when launched, before your scripts are run.
When using Wi-Fi and Ethernet, both set the device clock via NTP immediately after connecting. This is essential for secure network communication using TLS. If your device sets the time some other way, such as a Real-Time Clock peripheral, NTP clock synchronization is not done.
If your project uses a display, typically by including manifest_piu.json or manifest_commodetto.json, the mcdevicetree tool automatically creates a Screen global for the board's display driver, if any. This allows the Piu user interface framework and Commodetto's Poco renderer to access the screen. For boards with multiple displays, the display specified in the chosen section of the device tree is used.
To access flash memory partitions, include the ECMA-419 flash module manifest $MODDABLE/modules/io/flash/manifest.json in your project's manifest. That creates the device.flash.open function to access flash partitions by name:
const partition = device.flash.open({path: "partition_name"});
To access a file system, include the ECMA-419 file module manifest $MODDABLE/modules/io/files/manifest.json in your project's manifest. That creates the device.files property to access the file system. Your Zephyr Device Tree must have a valid file system defined in the Zephyr Device Tree at root.children.fstab.
const file = device.files.openFile({path: "directory/file.txt"});
To access the Zephyr key-value pair store (settings), include the ECMA-419 storage module manifest $MODDABLE/modules/io/storage/manifest.json in your project's manifest. That creates the device.keyValue.open function to access the key-value store. Your Zephyr Device Tree must have a valid settings subsystem defined in the Zephyr Device Tree.
const domain = device.keyValue.open({path: "wifi"});
The device global is different for every Zephyr board, because every board has unique hardware capabiliites and confirmations. As a result, a universal TypeScript declarations file (a .d.ts file) cannot precisely define the device global for any single build. Instead, mcdevicetree generates the TypeScript declarations file for the board. These declarations are automatically used when building TypeScript code for Zephyr, ensuring that your code accesses only the hardware available on your development board.
As with all Moddable platforms, you can debug script code using xsbug over the USB serial interface with the Zephyr device. For more information, see the xsbug documentation.
For native code source level debugging, you can use GDB.
Note: These instructions are for macOS and Linux.
Use the -t debug target to start the debugger:
mcconfig -d -m -p zephyr/nucleo_f413zh -t debug
Note: if you receive the following error, you may have to start openocd in another window if the gdb connection fails. We have encountered this on the Nucleo L4A6ZG board running with a macOS host.
:3333: Operation timed out.
You can't do that when your target is `exec'
Start openocd:
> openocd -s ~/zephyrproject/zephyr/boards/st/nucleo_f413zh/support
and try to build the -t debug target.
mcconfig -d -m -p zephyr/nucleo_f413zh -t debug
Zephyr supports hundreds of development boards. The Moddable SDK includes support for some of them.
To add new board to Moddable:
-
Select a similar board from $MODDABLE/build/devices/zephyr/targets and duplicate its directory.
cd $MODDABLE/build/devices/zephyr/targets
cp -r nucleo_f413zh new_board
-
Edit the manifest.json file and change the ZEPHYR_BOARD specifier to match the Zephyr board name (same as "new_board" above). The ZEPHYR_BOARD must match the name used in the Zephyr SDK for your board.
-
Use the new target:
cd $MODDABLE/examples/helloworld
mcconfig -d -m -p zephyr/new_board
Zephyr uses .dts files files to define hardware IO configuration.
Moddable uses .overlay files specified in the zephyrOverlay section of manifest.json.
The Nordic nrf52840dk device device manifest demonstrates:
Use port and channel to choose which analog channel to sample from. Different devices may use a different port naming schemes.
const analogInput = new device.io.Analog({
port: "adc",
channel: 0
});
Use an object with port and pin to describe the pin of a Digital object.
const led = new device.io.Digital({
...options,
pin: {port: "gpioe", pin: 1},
mode: Digital.Output,
});
Use port and channel to specify what zephyr configuration to use. You can also specify the frequncy by the hz property, and resolution (in number of bits) with the resolution property.
const led1 = new device.io.PWM({
port: "pwm0",
hz: "100",
resolution: "10",
channel: "0"
});
Zephyr display configuration are found in shield definition files. For example, the stm32u5a9j_dk target's manifest.json contains: