Meshtastic Firmware Source Code Practical Tutorial
This tutorial is intended for users who are just getting started with the Meshtastic firmware source code. It includes common workflows for both Windows and macOS. The goal is straightforward: clone the official repository, complete a successful build, make one simple UI change, and flash the modified firmware to the device for verification.
If you are already familiar with Git, Python, or PlatformIO, you can skip the corresponding sections and jump directly to the hands-on part.
This guide includes common commands for both Windows and macOS. Most screenshots are still taken from a Windows environment, but the overall workflow on macOS is very similar.
Prerequisites
Before you begin, prepare the following tools:
- Git
- Python 3
- VS Code
- PlatformIO
1. Install Git
- Windows
- macOS
Open the official Git for Windows download page:
The installer usually starts downloading automatically when you open the page. After the download is complete, double-click the installer and follow the setup wizard.
During installation, the most important step is Adjusting your PATH environment. Choose:
Git from the command line and also from 3rd-party software
For the other options, the default values are usually fine. Just keep clicking Next.

Wait until the installation finishes.
After installation, close all current PowerShell and VS Code terminal windows, then open a new PowerShell window and run:
& "C:\Program Files\Git\cmd\git.exe" --version

If a Git version number is displayed, Git has been installed successfully.
If the git command is still unavailable
You can first run the following commands in PowerShell to confirm the default Git installation paths:
$gitCmd = "C:\Program Files\Git\cmd"
$gitBin = "C:\Program Files\Git\bin"
Write-Host $gitCmd
Write-Host $gitBin

Then manually add Git to the system environment variables.
GUI fix steps
- Press
Win - Search for "Edit the system environment variables"
- Open it and click Environment Variables
- Find
Pathunder System variables - Click Edit
- Click New and add the following two paths:
C:\Program Files\Git\cmd
C:\Program Files\Git\bin
- Click OK all the way to save

After saving, you still need to:
- Close all PowerShell windows
- Open PowerShell again
Then run:
git --version

If a version number appears, the installation is complete.
On macOS, Git can be installed in more than one way, but using Homebrew is usually the easiest option:
- Install the Command Line Tools first:
xcode-select --install
- If Homebrew is not installed yet, install it first:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- Install Git:
brew install git
- Check the installed version:
git --version
If your terminal already returns a valid Git version, you do not need to install it again.
Configure your Git identity
Next, configure your Git user information. Replace the example values with your own name and email address:
git config --global user.name "your name"
git config --global user.email "your [email protected]"
Then run:
git config --global --list
to confirm the configuration has taken effect.
2. Install Python 3
Install Python from the command line
- Windows
- macOS
Run the following commands in the terminal:
winget search --id Python.Python.3.13 --source winget
winget install -e --id Python.Python.3.13 --source winget
If the first command can find Python, the second one should normally install it directly.
After installation, close the terminal and open it again, then run:
python --version
pip --version

If version numbers are shown, Python and pip are ready to use.
macOS often already includes a Python environment. Before installing a new version, check whether python3 and pip3 are already available:
python3 --version
pip3 --version
If they are not available, or if you want a newer version, install Python with Homebrew:
brew install python
After installation, reopen the terminal and run:
python3 --version
pip3 --version
If you prefer using python and pip, you can set shell aliases yourself. On macOS, however, using python3 and pip3 is usually the more reliable choice.
3. Install PlatformIO
This step may feel less beginner-friendly because PlatformIO downloads many dependencies automatically, and the installation can take some time. If errors appear during installation, it is usually best to wait patiently and troubleshoot one issue at a time. Using AI tools to help inspect the error messages can also save time.
Search for PlatformIO in the VS Code Extensions marketplace and install it.

After installation, an ant-shaped icon usually appears in the left toolbar.

4. Clone the Meshtastic firmware repository
The official Meshtastic firmware repository is meshtastic/firmware.
- Windows
- macOS
Run the following commands in your working directory terminal:
git clone https://github.com/meshtastic/firmware.git
cd firmware
git submodule update --init
If your project directory is on a different drive or under a different path, switch to that location first.


If the output looks similar to the screenshots above, the repository has been cloned successfully.
Run the following commands in your working directory terminal:
cd ~/workplace
git clone https://github.com/meshtastic/firmware.git
cd firmware
git submodule update --init
If ~/workplace does not exist yet, create it first:
mkdir -p ~/workplace
If the commands complete normally, the repository has been cloned successfully.
5. Hands-on practice
At this stage, do not rush into editing the code. First, make sure the project can run through the complete build process successfully.
It is recommended to start with three tasks:
- Open
firmware - Check
platformio.ini - Find the build environment for your target board
One important detail: do not focus only on the root platformio.ini. It actually includes additional configuration files, for example:
extra_configs =
variants/*/*.ini
variants/*/*/platformio.ini
variants/*/diy/*/platformio.ini
That means the real board-level environment definitions are usually located under variants/.../platformio.ini.
When identifying the target board, pay special attention to these two directories:
variants/boards/
Here we use Wio Tracker L1 Pro as the example target.

This shows that, in Meshtastic, the build target for Wio Tracker L1 / L1 Pro is seeed_wio_tracker_L1.
Minimal modification summary
If you only want to complete one minimal end-to-end practice run, focus on these key steps:
- Install Git, Python 3, VS Code, and PlatformIO.
- Clone the
meshtastic/firmwarerepository and initialize the submodules. - Use
pio run -e seeed_wio_tracker_L1to confirm the original project builds successfully. - Modify the display logic in
src/graphics/SharedUIDisplay.cpp. - Rebuild the firmware and flash the generated UF2 file to the device for verification.
Step 1: Confirm that the project builds successfully
Here we use the PlatformIO Core CLI for building.

For the first build, it is recommended to run the following command:
- Windows
- macOS
cd D:\workplace\firmware # Adjust to your actual project path
pio run -e seeed_wio_tracker_L1
cd ~/workplace/firmware # Adjust to your actual project path
pio run -e seeed_wio_tracker_L1

If the interface looks similar to the screenshot above, the build process has started correctly. The first build often takes a while, so be patient.
If the build fails
When a build fails, you can first ask PlatformIO to install the dependencies required by the current environment:
- Windows
- macOS
cd D:\workplace\firmware # Adjust to your actual project path
pio pkg install -e seeed_wio_tracker_L1
cd ~/workplace/firmware # Adjust to your actual project path
pio pkg install -e seeed_wio_tracker_L1
This approach has several benefits:
- It installs dependencies only, without immediately starting a full build.
- It makes it easier to see which package is causing the problem.
- The error messages are usually more focused and easier to troubleshoot.
After the dependencies are installed, run:
- Windows
- macOS
pio run -e seeed_wio_tracker_L1 -v
pio run -e seeed_wio_tracker_L1 -v

Once dependency installation is complete, run the normal build again:
- Windows
- macOS
pio run -e seeed_wio_tracker_L1
pio run -e seeed_wio_tracker_L1

If the build passes at this point, your firmware output has been generated successfully.

Step 2: Modify the code
Practice 1: Modify the UI display
Start by tracing the display implementation from the board-level configuration. You can first check:
variants/nrf52840/seeed_wio_tracker_L1/platformio.inivariants/nrf52840/seeed_wio_tracker_L1/variant.h

From these configuration files, you can see that L1 defines HAS_SCREEN and USE_SSD1306. That means it uses the standard OLED display pipeline, not a screenless configuration and not an E-Ink solution.
If you continue tracing the display logic, most of the related code is located under:
src/graphics/src/graphics/draw/
Exactly how you modify it depends on your ability to read the source code. Here we start with a very simple example: modifying the home screen UI.
Change 1: Record the right edge of the battery text
Before / After
// Before
int batteryX = 1;
int batteryY = HEADER_OFFSET_Y + 1;
// After
int batteryX = 1;
int batteryY = HEADER_OFFSET_Y + 1;
int batteryTextEndX = batteryX - 1;
src/graphics/SharedUIDisplay.cpp:157
This adds batteryTextEndX, which records the end position of the battery percentage text. That makes it easier to append custom text after the battery information later.
Change 2: Calculate the right boundary while drawing the battery percentage
// Before
if (chargePercent != 101) {
char chargeStr[4];
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
int chargeNumWidth = display->getStringWidth(chargeStr);
display->drawString(batteryX, textY, chargeStr);
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
if (isBold) {
display->drawString(batteryX + 1, textY, chargeStr);
display->drawString(batteryX + chargeNumWidth, textY, "%");
}
}
// After
if (chargePercent != 101) {
char chargeStr[4];
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
int chargeNumWidth = display->getStringWidth(chargeStr);
int percentWidth = display->getStringWidth("%");
display->drawString(batteryX, textY, chargeStr);
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
if (isBold) {
display->drawString(batteryX + 1, textY, chargeStr);
display->drawString(batteryX + chargeNumWidth, textY, "%");
}
batteryTextEndX = batteryX + chargeNumWidth + percentWidth - 1 + (isBold ? 1 : 0);
} else {
batteryTextEndX = batteryX - 1;
}
src/graphics/SharedUIDisplay.cpp:204
This code sits inside the battery percentage drawing logic. In addition to displaying the battery level normally, it also calculates the right boundary of the text area so that custom labels can be placed after the battery information.
Change 3: Reserve a boundary for the icon area on the right
// Before
int iconRightEdge = timeX - 2;
// After
int iconRightEdge = timeX - 2;
int headerLabelRight = timeX - 4;
src/graphics/SharedUIDisplay.cpp:263
This part handles the area used by the time, mail, mute, and other icons on the right side. I added headerLabelRight to limit the maximum right boundary of the center text and prevent overlap with the right-side content.
Change 4: Draw a custom label when the title is empty
// Newly added core logic
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK)
if (titleStr && titleStr[0] == '\0') {
static const char *yclLabel = "made by AE";
int labelWidth = display->getStringWidth(yclLabel);
int labelLeft = batteryTextEndX + 4;
if (labelLeft + labelWidth <= headerLabelRight) {
int labelX = labelLeft + ((headerLabelRight - labelLeft) - labelWidth) / 2;
display->drawString(labelX, textY, yclLabel);
if (isBold)
display->drawString(labelX + 1, textY, yclLabel);
}
}
#endif
src/graphics/SharedUIDisplay.cpp:350
This is the core logic of the modification. It only applies to SEEED_WIO_TRACKER_L1 and explicitly excludes the E-Ink variant. It centers the text made by AE in the blank space between the battery information and the time display.
Change 5: Handle the branch where no time is displayed
// Add the same boundary control for the no-time branch
int iconRightEdge = screenW - xOffset;
int headerLabelRight = screenW - xOffset - 2;
src/graphics/SharedUIDisplay.cpp:377
This is the branch used when no time value is displayed. The same boundary control needs to be added here as well.
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK)
if (titleStr && titleStr[0] == '\0') {
static const char *yclLabel = "made by AE";
int labelWidth = display->getStringWidth(yclLabel);
int labelLeft = batteryTextEndX + 4;
if (labelLeft + labelWidth <= headerLabelRight) {
int labelX = labelLeft + ((headerLabelRight - labelLeft) - labelWidth) / 2;
display->drawString(labelX, textY, yclLabel);
if (isBold)
display->drawString(labelX + 1, textY, yclLabel);
}
}
#endif
src/graphics/SharedUIDisplay.cpp:426
This is the implementation for drawing made by AE in the no-time branch.
You can find the complete code here:
Step 3: Build your own firmware
After finishing the modification, return to the project root and build the same target again:
- Windows
- macOS
cd D:\workplace\firmware # Adjust to your actual project path
pio run -e seeed_wio_tracker_L1
cd ~/workplace/firmware # Adjust to your actual project path
pio run -e seeed_wio_tracker_L1
The display logic has changed, but the build target is still the same:
seeed_wio_tracker_L1
After a successful build, the output is usually located in:
- Windows
- macOS
D:\workplace\firmware\.pio\build\seeed_wio_tracker_L1\
~/workplace/firmware/.pio/build/seeed_wio_tracker_L1/
The file you should confirm has been updated is:
firmware-seeed_wio_tracker_L1-*.uf2
6. Flash the firmware
After the build is complete, open the official flashing page:
In most cases, you should perform an erase operation first.

Then select the firmware file you just built and flash it to the device.


At this point, the practical Meshtastic source code exercise is complete. You have gone through the full workflow: environment setup, repository cloning, board configuration discovery, firmware compilation, display logic modification, and final flashing verification.
If you want to go further, you can continue exploring these directions:
- Modify more elements on the home screen
- Adjust the behavior of buttons, GPS, Bluetooth, and other modules
- Add an independent
variantfor your own board - Continue tracing the relationships between
src/,variants/, andboards/
Common issues
The git command is unavailable
- On Windows, first check whether Git has been added to
PATH. - On macOS, run
git --versionfirst. If the system asks you to install the Command Line Tools, follow the prompt.
python3 or pip3 is unavailable
- On Windows, confirm that Python was added to
PATH, or reopen the terminal and try again. - On macOS, first check whether
python3/pip3already exists, and install Python with Homebrew only if needed.
The pio command is unavailable
- Run
pio --versionfirst. - If the command is still unavailable, restart VS Code and the terminal, then try again.
- If necessary, reinstall the PlatformIO extension and confirm that PlatformIO Core has been initialized correctly.
The code still looks incomplete after git submodule update --init
- First make sure you are in the root directory of the
firmwarerepository. - If the network connection is unstable, try again with:
git submodule update --init --recursive
The first build takes too long
- It is normal for the first build to download many dependencies.
- If it seems stuck for too long, try installing the packages separately first:
pio pkg install -e seeed_wio_tracker_L1
Then run the build again.