This guide gets a fresh machine ready to create, build, simulate, and flash Gea apps through the npm-first GeaStack flow.
Start by running:
npx gea doctordoctor checks the active app, board configuration, and the toolchains below.
npx create-geastack my-app
cd my-app
npx gea setupFor simulator-only development:
- Node.js 20.19 or newer.
- npm.
- Emscripten SDK if you want
npx gea build --target weband the WASM simulator bundle path.
For ESP32 hardware:
- Node.js 20.19 or newer.
- npm.
- Python 3.
- ESP-IDF v6.0.2 (GeaStack's pinned default; see ESP-IDF For ESP32 Targets below).
- a board alias for your board (
~/.geastack/boards.jsonor the project's.gea/boards.json, see Board Configuration).
For the Waveshare ESP32-S3 AMOLED board, use ESP32-WAVESHARE-AMOLED-QUICKSTART.md.
For Apple targets:
- macOS.
- Xcode with iOS/macOS SDKs.
- Xcode command line tools.
- iOS Simulator runtime if building/running iOS simulator apps.
For Android targets:
- Android SDK command-line tools and platform tools.
adbonPATH.- A Java JDK with
javac. ANDROID_HOMEorANDROID_SDK_ROOTpointing at the Android SDK.
Install Node.js from the official download page or your normal version manager. GeaStack currently requires Node.js 20.19 or newer. Node 24 LTS is a good default on new machines.
Verify:
node --version
npm --versionThe embedded board scripts target ESP-IDF v6.0.2 by default. GeaStack resolves the version it installs or verifies dynamically, in this order:
--idf-version <tag>(e.g.npx gea setup --esp-idf --idf-version v6.1.0-rc1) to pin an exact release or try a release candidate.GEA_ESP_IDF_VERSION(same shape) when no--idf-versionis given.- The latest stable ESP-IDF release on GitHub, when it can be determined (release candidates and betas are ignored).
- The pinned default, v6.0.2, when nothing above applies (offline, GitHub unreachable, etc).
Verifying an existing install accepts any installed version whose major.minor is the same as or newer than the resolved target -- an installed 6.0.2 is never rejected just because the resolved target moved on to, say, 6.1.0, unless it genuinely trails it.
Command-line install:
npx gea setup --esp-idfEquivalent manual install:
mkdir -p "$HOME/esp"
cd "$HOME/esp"
git clone -b v6.0.2 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh esp32,esp32s3,esp32p4
. ./export.sh
idf.py --versionFor every new shell where you build or flash ESP32 firmware, source the export script:
. "$HOME/esp/esp-idf/export.sh"If ESP-IDF lives somewhere else, either source that location's export.sh before
running Gea commands or set:
export GEA_EMBEDDED_IDF_EXPORT="/path/to/esp-idf/export.sh"The board script also checks common locations such as $HOME/esp/esp-idf,
$HOME/esp32/esp-idf, and $HOME/esp32/esp-idf-v6.0.2.
Verify through GeaStack:
npx gea doctor
npx gea setup
npx gea build --board amoled --dry-runRemove --dry-run when your board config is ready.
The live web dev loop uses Vite. The WASM simulator build path additionally
needs emcc from the Emscripten SDK.
Install with emsdk:
git clone https://github.com/emscripten-core/emsdk.git "$HOME/emsdk"
cd "$HOME/emsdk"
./emsdk install latest
./emsdk activate latest
. ./emsdk_env.sh
emcc --versionFor every new shell where you run npx gea build --target web, source:
. "$HOME/emsdk/emsdk_env.sh"Verify:
npx gea doctor
npx gea build --target webInstall Xcode from the Mac App Store or Apple Developer resources. Then make sure command line tools point at the Xcode installation:
xcode-select --install
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcodebuild -versionOpen Xcode once to finish first-run setup and install any required simulator runtimes from Xcode Settings > Platforms.
For signed iOS device builds, set your Apple development team:
export GEA_IOS_DEVELOPMENT_TEAM=ABCDE12345Verify:
npx gea doctor
npx gea build --target macos
npx gea build --target ios --mode simulatorInstall Android Studio or the Android SDK command-line tools. Make sure the SDK has at least one platform and build-tools package installed, then export:
export ANDROID_HOME="$HOME/Library/Android/sdk"
export PATH="$ANDROID_HOME/platform-tools:$PATH"Verify:
adb version
javac --version
npx gea doctor
npx gea build css-3d-cube --target androidFor an attached Android device or board with USB debugging enabled:
npx gea build css-3d-cube --target android --mode deviceIf more than one adb is installed, point Gea at the one you want:
GEA_ANDROID_ADB="$ANDROID_HOME/platform-tools/adb" \
GEA_ANDROID_SERIAL="<adb-serial>" \
npx gea build css-3d-cube --target android --mode devicePython 3 is used by device helpers, serial helpers, screenshots, and GeaOS device scripts.
Verify:
python3 --versionIf your platform does not provide Python 3, install it with your OS package manager or from python.org.
A board alias names one physical unit: gea flash --board amoled resolves
amoled to a target, a USB serial and, optionally, an IP. Aliases are
machine-local configuration and never come from a package. The CLI merges two
files, project over home:
| file | holds | written by |
|---|---|---|
~/.geastack/boards.json |
every board on this machine (GEA_HOME relocates the directory) |
gea boards add --global, gea boards set |
<project>/.gea/boards.json |
aliases specific to one application; overrides a home alias of the same name | create-geastack (empty), gea boards add |
--boards-config <file> (or GEA_BOARDS_CONFIG) replaces both with exactly
that file. By default gea boards add writes to the project config when the
project has one and to the home config otherwise; an existing alias is always
edited where it lives.
npx gea boards discover # what is plugged in: alias, app, IP
npx gea boards add # register a board (guided)
npx gea boards list # every alias and which file it lives in
npx gea boards set amoled host 192.168.1.100
npx gea boards rename amoled desk-amoled
npx gea boards remove desk-amoled
npx gea doctorDevice control is transport-agnostic: gea devctl brightness [0-100],
gea devctl hbm [on|off] and gea devctl vsync [on|off] answer over USB
(GEADEV verbs) and over WiFi (POST /display/<knob>), and each reports the
current value when given none. devctl prefers the cable for these when the
board is attached, because an app with no network binding builds firmware
with WiFi switched off while the alias still records an OTA host.
gea boards discover sends one GEADEV PING to each serial device without
resetting it; a board running gea firmware answers with its app id, its IP
(when it has joined WiFi) and its MAC, and the CLI pairs the reply with an
alias by USB serial. --save writes a reported IP into
transports.ota.host, which is what gea ota, gea logs and
gea screenshot use over WiFi.
Every entry names a target (gea targets list) and its adapter, then the
transports the board offers. The USB serial is the stable identity: on ESP32
boards it is the station MAC, and the CLI resolves it to today's /dev port
at call time, so never record a port path.
{
"amoled": {
"target": "esp32-s3-touch-amoled-2.06",
"adapter": "esp32-idf",
"transports": {
"usbSerial": { "serial": "80:B5:4E:DA:73:88" },
"ota": { "host": "192.168.1.100" }
}
},
"rotary": {
"target": "esp32-s3-elecrow-rotary-2.1",
"adapter": "esp32-idf",
"transports": {
"usbSerial": { "serial": "14:C1:9F:26:65:08", "restartAfterFlash": "manual" }
}
},
"tufty": {
"target": "rp2350-tufty-2350",
"adapter": "rp2350-pico",
"transports": { "usbSerial": { "serial": "fa59949adbb4802f" } }
},
"linux": {
"target": "geaos",
"adapter": "geaos-linux",
"transports": {
"telnet": { "host": "192.168.7.2", "port": 2323 },
"fastboot": { "serial": "geaos001" }
}
},
"lokmat": {
"target": "lokmat-applp2max",
"adapter": "geaos-arm64",
"transports": {
"usbSerial": { "serial": "geaos01" },
"fastboot": { "serial": "0123456789ABCDEF" },
"mtk": { "workdir": "~/lokmat-root" }
}
},
"my-board": {
"target": "my-board",
"targetDefinition": "targets/my-board.json",
"adapter": "esp32-idf",
"transports": { "usbSerial": { "serial": "YOUR_BOARD_USB_SERIAL" } }
}
}restartAfterFlash: "manual"marks a board whose USB-Serial-JTAG port re-enters ROM download mode after a flash; the CLI stops and asks for a power cycle instead of pulsing the reset lines.targetDefinitionpoints at a custom target composed bygea setup/gea chips, relative to the file the alias lives in.
A composed target is a JSON definition that extends the esp32-s3 base and
picks its chips from the @geastack/chips catalog (gea chips list). Every
role is optional, so the definition describes the hardware that is there:
display, touch, power, imu, audio and expander. Some boards are
shipped composed in @geastack/targets and need no definition of your own,
for example esp32-s3-devkit-n16r8 (a bare module) and
esp32-s3-waveshare-touch-lcd-7.
Two kinds of display are understood. A QSPI controller (co5300) takes the
spiHost and cs/pclk/data0-3/reset/te pins. A bare parallel RGB
panel (rgb_panel) has no controller: it takes the panel geometry, the pixel
clock and sync timings, and the de/hsync/vsync/pclk plus sixteen
data0-15 pins (blue in 0-4, green in 5-10, red in 11-15). The Waveshare
7-inch board is the reference:
{
"id": "my-rgb-board",
"extends": "esp32-s3",
"adapter": "esp32-idf",
"mcu": "esp32s3",
"flashSize": "16MB",
"psram": { "mode": "octal", "speed": 80 },
"buses": { "i2c": { "sda": 8, "scl": 9 } },
"chips": {
"display": {
"driver": "rgb_panel", "interface": "rgb",
"width": 800, "height": 480, "pclkHz": 16000000,
"hsyncPulseWidth": 4, "hsyncBackPorch": 8, "hsyncFrontPorch": 8,
"vsyncPulseWidth": 4, "vsyncBackPorch": 8, "vsyncFrontPorch": 8,
"pclkActiveNeg": true,
"pins": {
"de": 5, "hsync": 46, "vsync": 3, "pclk": 7,
"data0": 14, "data1": 38, "data2": 18, "data3": 17, "data4": 10,
"data5": 39, "data6": 0, "data7": 45, "data8": 48, "data9": 47, "data10": 21,
"data11": 1, "data12": 2, "data13": 42, "data14": 41, "data15": 40
}
},
"touch": { "driver": "gt911", "interface": "i2c", "pins": { "interrupt": 4 } },
"expander": {
"driver": "ch422g", "interface": "i2c",
"initialOutputs": 30,
"outputs": { "touchReset": 1, "backlight": 2, "displayReset": 3 }
}
}
}The expander role is for boards that route slow control lines through an
I2C I/O expander instead of GPIOs. outputs names the expander pin that
carries each line; a display with no pins.backlight lights up through
outputs.backlight, and a touch controller with no pins.reset is reset
through outputs.touchReset. initialOutputs is the level mask latched at
boot.
After installing tools:
npx gea doctor
npx gea inspect
npx gea dev
npx gea build --target web
npx gea setup
npx gea build --board amoled --dry-run
npx gea flash --board amoled --monitorUse --dry-run first when checking command routing. Remove it when the target
toolchain and board config are ready.
- ESP-IDF Programming Guide: https://docs.espressif.com/projects/esp-idf/
- Emscripten SDK downloads: https://emscripten.org/docs/getting_started/downloads.html
- Node.js downloads: https://nodejs.org/en/download
- Xcode resources: https://developer.apple.com/xcode/resources/