This is an unofficial fork of Blockstream/Jade,
not affiliated with or endorsed by Blockstream. It adds an optional
chess app that is off by default. With the app disabled -- which is
the default -- builds follow upstream; every change is gated behind
CONFIG_CHESS_APP. For anything other than playing with the chess app, use
upstream.
- DO NOT ATTEMPT TO BUILD/FLASH WITH OFFICIAL BLOCKSTREAM JADE HARDWARE UNITS
The below instructions are for developers with access to Jade development devices, or for those wanting to build and flash their own esp32/esp32s3 DIY consumer devices such as M5Stack or TTGO T-Display boards.
Official Blockstream Jade hardware units can only be updated via a supported companion app, or by using the Firmware Update Instructions.
For information about suitable DIY hardware, as well as suggested configuration profiles and notes on secure boot, see the DIY Guide
Cmake and ninja are needed to build Jade firmware images.
On Debian based distributions, install with with:
sudo apt install cmake ninja-build
On MacOS:
brew install cmake ninja
Make sure to use a recent Python version (Python 3.11+) when running the commands below. Failure to do so may result in problems installing Python dependencies.
Jade requires the esp-idf SDK. You can use our docker image to build or install the esp-idf toolchain locally using the commands below.
See the Espressif official guide for more information on the available tooling.
There are currently four official Jade device targets:
- jade: The original Jade 1.0 with a selection wheel.
- jade_v1_1: Jade 1.1, with a rocker/jog-wheel instead of the selection wheel.
- jade_v2: Jade Plus, with a larger screen and left/right selection buttons instead of a wheel or rocker.
- jade_v2c: Jade Core, Jade Plus without camera and battery.
Change jade in any calls to the switch_to.sh script below to the
appropriate device you wish to target from the list above.
The serial port pseudo-tty file for Jade access via USB is usually
/dev/ttyACM0 or /dev/ttyUSB0 (or /dev/cu.SLAB_USBtoUART on MacOS).
In order to have permission to use USB to install firmware, your user should
be in the dialout group on Debian based distributions. Other distributions
may use a different group name: Check the group of the serial port pseudo-tty
file using ls -l to determine the required group, e.g:
$ ls -l /dev/ttyACM0
crw-rw----+ 1 root dialout 166, 0 Apr 15 14:37 /dev/ttyACM0
The group name (dialout here) is shown after the owner (root). You can check
that dialout appears in your user groups by running:
$ groups
docker libvirt dialout storage kvm wheel plugdev
If not present you should add your user to the group:
sudo usermod -aG dialout $USER
You should then login/logout or reboot for the group changes to take effect.
NOTE: For docker builds no group changes are usually required as the docker image is privileged.
You should set the environment variable JADESERIALPORT to the Jade USB
device to default its value when running development scripts.
NOTE: MacOS users should set up the environment locally as detailed below to avoid issues with device access. For more information see this article.
Blockstream provides the blockstream/jade_builder docker image, which provides
the idf tooling and other dependencies required to build.
The simplest way to use it is the docker-compose.yml file in this repository,
which mounts the current repository source code into the container and pins the
image by digest:
$ # Run a shell inside the builder, with this repository mounted
$ docker compose run dev bash
(docker)$ # Set up idf environment
(docker)$ get_idf
(docker)$ # Make the serial device available to internal scripts, for example:
(docker)$ export JADESERIALPORT=/dev/ttyACM0
To run the image directly instead, use the same digest that docker-compose.yml
pins. NOTE: there is no latest tag published for this image, so
blockstream/jade_builder:latest will not resolve:
$ docker run -it $(grep -o 'blockstream/jade_builder@sha256:[0-9a-f]*' docker-compose.yml) bash
Run get_idf within the container to enable the idf tools.
You can then build and flash as detailed below.
Install the esp-idf SDK and required tools. From a checked-out Jade git repository, run the following commands:
$ export ESP_IDF_BRANCH=$(grep ESP_IDF_BRANCH Dockerfile | sed 's/.*=//g')
$ mkdir ~/esp
$ cd ~/esp
$ git clone --recursive https://github.com/espressif/esp-idf.git
$ cd esp-idf
$ git checkout $ESP_IDF_BRANCH
$ ./install.sh --enable-gdbgui esp32 esp32s3
$ python ./tools/idf_tools.py install qemu-xtensa
Set up the idf environment to make the idf.py command available,
and then install the Jade dependencies into the idf environment:
$ . ~/esp/esp-idf/export.sh
$ pip install --require-hashes -r ./requirements.txt
You can then build and flash as detailed below.
First, you'll need the Jade source code including its sub-modules checked out:
git clone --recursive https://github.com/juja256/Jade.git $HOME/jade
cd $HOME/jade
git submodule update --init --recursive
Choose your configuration. For official Jade devices, the script tools/switch_to.sh allows
choosing the device and features you want. Run tools/switch_to.sh --help to see the
available options. A standard development Jade Plus build for example would use something
like:
$ ./tools/switch_to.sh jade_v2 --dev --log --jtag [--noradio]
For other devices, copy (and modify if desired) a suitable config from the configs
directory to sdkconfig.defaults. You should also run e.g. idf.py set-target esp32 or
idf.py set-target esp32s3 once initially to ensure you are targeting the correct
toolchain for your hardware. So for example for the TTGO T-Display:
$ cp configs/sdkconfig_display_ttgo_tdisplay.defaults sdkconfig.defaults
To build the firmware, run:
$ idf.py all
To flash the resulting build to your device, run:
$ idf.py -p $JADESERIALPORT flash [monitor]
Some hardware configurations (e.g. M5StickC-Plus) may not support the default baud
rate and so won't be detected. If this occurs you can force a specific baud rate
for flash/monitor by using the -b argument, e.g:
idf.py -p $JADESERIALPORT -b 115200 flash [monitor]
If you have errors relating to unknown bytes when flashing, place your device into download mode. This is device specific, for Jade development devices, turn off the device, then hold the select and power buttons for 10 seconds. Note the device screen will stay blank when in download mode.
If you switch between JTAG and non-JTAG builds in particular, you will need to flash from download mode.
If you flash multiple device types, or make changes to the sdkconfig.config file,
delete the sdkconfig file that gets created from sdkconfig.defaults between
builds. Otherwise, your changes will not get picked up when re-building/flashing
the firmware.
Beyond the build configurations in the configs/ directory, you can edit the config
manually with the menuconfig tool:
$ idf.py menuconfig
NOTE: for any but the simplest CI-like build with no GUI, no camera, no
user-interaction etc. it is recommended that PSRAM (available under
Component Config -> ESP-32 specific -> Support external SPI connected RAM
is available and enabled.
Virtualenv and bluez-tools are required, you can install them on Debian based distributions with:
sudo apt install virtualenv bluez-tools
Then to run the tests:
$ cd $HOME/jade
$ # Create and activate a Python virtualenv
$ virtualenv -p python3 venv3
$ source venv3/bin/activate
$ pip install -r requirements.txt
$ pip install -r pinserver/requirements.txt
$ # Run the tests
$ python test_jade.py --serialport $JADESERIALPORT
$ # Cleanup
$ deactivate
The tests require a CI build; this is a configuration that automatically
accepts the default action without requiring user interaction. This is enabled using
the --ci argument to switch_to.sh or by setting CONFIG_DEBUG_UNATTENDED_CI=y
in sdkconfig.defaults.
Debug support is also required to expose debug functions for testing. Use --debug
or set CONFIG_DEBUG_MODE=y to enable this.
The firmware can be built and run under emulation via qemu:
$ # Build the default Jade emulation image (debug CI build)
$ docker build -t jade-qemu -f Dockerfile.qemu .
$
$ # Pass switch_to.sh args via QEMU_CONFIG_ARGS, e.g. for a no-psram build:
$ docker build -t jade-qemu -f Dockerfile.qemu . --build-arg QEMU_CONFIG_ARGS="--dev --ci"
$
$ # Pass `--build-arg QEMU_GDB="--gdb"` to enable gdb debugging.
$
$ # For a web-enabled 'virtual Jade':
$ docker build -t jade-qemu -f Dockerfile.qemu . --build-arg QEMU_CONFIG_ARGS="--dev --psram --webdisplay"
$
$ # Point your browser to http://localhost:30122 to interface with the virtual Jade.
$ # Replace --webdisplay with --webdisplay-larger for a larger display.
Run any of the above images with e.g.:
$ # Note you can remove `-p 30122:30122` if not using --webdisplay
$ docker run --rm -p 30121:30121 -p 30122:30122 -it jade-qemu
The jadepy python package can talk to the emulated jade via serial over tcp.
Pass the device string "tcp:localhost:30121" when connecting, e.g.:
python -c "from jadepy.jade import JadeAPI; jade = JadeAPI.create_serial(device='tcp:localhost:30121'); jade.connect(); print(jade.get_version_info()); jade.disconnect()"
This fork adds an optional chess app, playable on the device against an on-board engine. It is off by default and is not part of upstream Jade.
This is a toy, and it is the only reason this fork exists. It links a chess engine into hardware wallet firmware: new attack surface, no security benefit, and it perturbs reproducible builds. Do not enable it on a device that holds funds, and do not use this fork as a wallet. It is intended for development devices and DIY boards.
The design and its rationale are written up in docs/superpowers/specs/2026-07-16-chess-app-design.md.
The board is a fixed 160x160 (eight 20px squares), so the app requires a display of at least 240x160. Jade Plus (320x170) qualifies; the original Jade and Jade 1.1 (240x135) do not, and neither does the default qemu board. Enabling it on too small a display is a compile-time error, not a clipped board.
Jade Plus has three inputs -- two navigation buttons and a front select button
(both navigation buttons pressed together also selects). Rather than walk a
cursor over 64 squares, the app cycles through legal moves only: select a
piece that can move, then cycle its legal destinations. Illegal moves cannot be
expressed, and most moves take two or three clicks. < Back, Resign and
Exit are entries in that same cycle, as the firmware provides no cancel
gesture.
The app appears in the Options menu. It needs no wallet, and is available
whether or not the device is initialised or unlocked.
Opening it shows a setup menu: choose your colour (White, Black or Random)
and a difficulty level (Lv1--Lv5, roughly depth 3 to 7, shown with a rough
ELO estimate), then Play. The play screen shows the current level. New game
after a result returns to this menu so you can adjust before replaying; Exit
leaves the app. The chosen colour and level are remembered for the session only
(reset on reboot). The engine uses a transposition table and iterative deepening,
so the higher levels search deep enough to see basic tactics -- Lv5 (depth 7) is
deliberately slow, several seconds to tens of seconds per move on device.
Pass --chess to switch_to.sh, e.g. for a Jade Plus development build:
$ ./tools/switch_to.sh jade_v2 --dev --log --chess
$ idf.py all
$ idf.py -p $JADESERIALPORT flash monitor
Or enable CONFIG_CHESS_APP under Blockstream Jade in idf.py menuconfig.
NOTE: --webdisplay-larger is required, not --webdisplay. The default
qemu board is 240x135, which is too small for the board; --webdisplay-larger
is 320x170, matching Jade Plus exactly.
$ docker build -t jade-chess -f Dockerfile.qemu . \
--build-arg QEMU_CONFIG_ARGS="--dev --psram --webdisplay-larger --chess"
$ docker run --rm -p 30121:30121 -p 30122:30122 -it jade-chess
Then point a browser at http://localhost:30122.
libjade runs the firmware natively, and is the fastest way to work on the app. See libjade.
$ ./tools/switch_to.sh jade --dev --noradio # NOTE: required first, see libjade/README.md
$ ./libjade/make_libjade.sh Debug --chess --no-ci
$ ./libjade/run_libjade_gui.sh --chess
Use --no-ci for interactive play, or the firmware auto-clicks its own buttons.
libjade's display is 320x200 rather than Jade Plus's 320x170, so it is the wrong
place to judge layout -- use qemu --webdisplay-larger for that.
libjade can also be driven programmatically via its send_input and
get_display_bytes RPCs, which is how the app was developed and tested without
hardware:
jade._jadeRpc('libjade_request', {'request': 'send_input', 'event': 'right'})
frame = jade._jadeRpc('libjade_request', {'request': 'get_display_bytes'})The engine, board rendering and move-cycling state machine depend on nothing outside libc and are tested on the host -- no device, no esp-idf, no docker:
$ ./main/chess/test/run_tests.sh [--asan]
This runs perft (the standard move-generation correctness test, against
published node counts), search and terminal-state tests, rendering and SAN
tests, and full games driven through the state machine's public API. It also
compiles every chess source with esp-idf's own warning flags, since the firmware
builds -Werror=all.
See REPRODUCIBLE.md for instructions on locally reproducing the official Blockstream Jade firmware images (minus the Blockstream signature block).
NOTE: enabling the chess app changes the firmware binary, so a build with
--chess will not reproduce the official images.
Seen working on M5 Stack gray/black/FIRE, M5 Stick Plus, Core 2, Core S3, LilyGO T-Display, T-DisplayS3, RPI Zero + display shield (via QEMU), and Desktop via Qemu (browser for display/webcam).
A python client is available to communicate with genuine or DIY Jade units:
pip install jade-client
This installs the jadepy directory from this repository. See jade-client-requirements.txt and jade-client-requirements.txt.asc
See libjade For a local build setup that allows for initial feature development and debugging off-device.
The collection is subject to GPL3 but individual source components can be used under their specific licenses.