All the embedded code that runs on the glasses themselves.
After cloning this repo from GitHub you'll need to pull the most recent submodules. You'll need to rerun this command periodically to get the latest changes whenever the submodules change.
git submodule update --init --recursiveThe primary way to build the firmware itself is with Espressif's official esp-idf toolchain. In some other directory (not in this repo), run:
mkdir ~/esp
cd ~/esp
git clone -b v5.3.2 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh esp32s3If you get errors about cmake you might need to install cmake first (brew install cmake on macOS / sudo apt install cmake on Ubuntu).
Now from the root of this repo, you can run (replace /YOUR/PATH/TO with the path where you put esp-idf):
. ~/esp/esp-idf/export.sh
make build flash monitorThis will build the latest firmware, flash it to the device, and open a serial monitor. You can also use idf.py menuconfig to configure the build if you need to change build options.
See also the official esp-idf installation instructions for more details.
You have two ways of working with Claude Code and IDF. One is to already have IDF set up when running claude. Just source your export.sh script before running claude.
The other option is to only set IDF_PATH in your .zshrc/.bashrc. Then Claude can use the instructions in CLAUDE.md to be able to find/run the export.sh script itself.
ln -s /YOUR/PATH/TO/esp-idf ~/esp/esp-idfBy default we build a debug build for the glasses. But we support other build targets. To specific a different target, you can use the TARGET environment variable. For example, to build a release build, you can use:
TARGET=glasses-release build flash monitorBy defualt idf.py monitor will reboot the MCU as soon as it connects so that you can get logs from the very start of execution. If you want to connect to the MCU without rebooting it, you can use idf.py monitor --no-reset -p [port]. Here's a handy command to connect to the first device named /dev/cu.usbmodem*.
idf.py monitor --no-reset -p $(ls /dev/cu.usbmodem* 2> /dev/null | head -n 1)- Visual Studio Code (
brew install visual-studio-code) - CMake (
brew install cmake, or on Ubuntusudo apt install cmake) - clang-format (
brew install clang-format)
Make sure you have clang-format installed (brew install clang-format). Then if you're using VSCode, and install the recommended workspace extensions, your code should be formatted automatically for you on save (or with shift-option-f). Alternately you can force formatting of all code with:
find src -iname "*.hpp" -o -iname "*.h" -o -iname "*.cpp" -o -iname "*.c" | xargs clang-format -iYou can run clang-tidy to do some more advance C++ static analysis and fixes. Make sure you've set up IDF, then run the following to automatically fix things and show warnings:
make clang-tidyIssues that will be fixed automatically will be fixed by clang-tidy, warnings will mark any suggestions that clang-tidy couldn't fix directly.
The simulator compiles in <20s from a clean build, and even less on an incremental build. This makes it fast for us to iterate on anything that doesn't need the physical hardware.
You'll need SDL2, openssl for it to work: brew install sdl2 openssl (on Ubuntu sudo apt install libsdl2-dev libssl-dev). Then you can run make sim to run it (or make sim8 for the high-res version). After building once, you can also debug with lldb at the command line: lldb lib/Simulator/build/simulator.
Run the simulator at least once to create your simprefs.ini file. Then add the line to that file:
cloud-token=<TOKEN>
You can get your access token by monitoring logs on the dev board or glasses when pairing with the app. Then you can run "Sync Content" from the simulator just like you would on the glasses.
You can use a debugging proxy (like Charles.app with the Simulator). Just set the HTTP_PROXY environment variable before running make sim:
export HTTP_PROXY=http://localhost:8888
make simThis repo has unit tests using Catch2. Run them with
make testThe test suite has some simple unit tests as well as view tests. View tests run the app, and record the rendered screens and compare them with known good versions to indicate if anything has changed unexpectedly. If views change the failure messages will tell you how to update the known-good images if necessary.
You can erase NVS/Preferences from the command line using esptool. You can either erase the entire internal flash with:
esptool.py erase_flashOr you can erase a region. The appropriate flash region comes from esp32s3-64MB.csv, and it may be worth a check there to confirm this documentation is still up to date. For example, this probably erases NVS:
esptool.py erase_region 0x9000 0x5000You can save the most recent core dump when connected over serial. This only works if you still have the .elf from the crashing version. With glasses connected, use the following command:
espcoredump.py -p /dev/tty.usb* info_corefile build/embedded-app.elfNote that if you weren't connected to USB when the crash happened, we may queue the trace to get sent to the server, and it will be unavailable to espcoredump.py
See also the Core Dump docs.
You can use OpenOCD (included in ESP-IDF) to debug the firmware on the ESP32-S3 using the built-in USB JTAG interface.
Make sure you have idf.py set up (see above) and make sure to idf.py build the code you want to debug. Then, with your glasses / dev board connect you can run:
openocd -f board/esp32s3-builtin.cfgThen in a separate terminal, connect gdb:
xtensa-esp32s3-elf-gdb -x scripts/gdbinit build/embedded-app.elfA very useful GDB command is catch throw which sets a breakpoint at any C++ exceptions. You might also try break filename.cpp:55 to set a breakpoint at a file and line number. Then use continue (or c) to continue execution until the breakpoint is hit. Here's a handy GDB cheat sheet.
You can also use openocd to flash firmware over the JTAG interface. I don't think there's any advantage to this vs using esptool.py:
openocd -f board/esp32s3-builtin.cfg -c "program_esp build/embedded-app.bin 0x10000 verify exit"It's possible to use connect to OpenOCD from inside VSCode. You'll need to add a block like this to your launch.json file:
{
"name": "OpenOCD Attach",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/embedded-app.elf",
"MIMode": "gdb",
// NOTE: This might change as the toolchain changes
"miDebuggerPath": "${env:HOME}/.espressif/tools/xtensa-esp-elf-gdb/12.1_20221002/xtensa-esp-elf-gdb/bin/xtensa-esp32s3-elf-gdb",
"miDebuggerArgs": "${workspaceFolder}/build/embedded-app.elf",
// This disables adding a breakpoint on `main` though it seems VSCode likes to add it anyways sometimes
"stopAtEntry": false,
// Same commands from the scripts/gdbinit file
"setupCommands": [
{"text": "target extended-remote :3333"},
{"text": "set remote hardware-watchpoint-limit 2"},
{"text": "mon reset halt"},
{"text": "flushregs"},
{"text": "thb app_main"},
],
"cwd": "${cwd}",
// Uncomment to enable logging the GDB commands sent
// "logging": {
// "engineLogging": true
// },
}Run the openocd -f board/esp32s3-builtin.cfg command in the terminal, as above, then hit the "Run -> Start Debugging" button in VSCode. You should see the debugger connect and stop at the app_main function.
TODO: This is kinda clunky and it'd be nice to have this easier to setup and run.
Since every individual device has its own unique serial number that can't be changed by the user, we need to burn in the serial number before it arrives at their door. Note that offset is in bytes, not bits, and comes from EFusesCustomTable.csv.
NOTE: The offset for the serial number is set to 8 bytes because the burn_block_data command requires the entire block after the offset to be written, so by offsetting to a right-justified serial number, we can reserve the preceding bits for future use without explicitly burning them as leading zeros.
#
echo -n SOL01224400001 > serial_number.bin
espefuse.py --debug --port /dev/cu.usbmodem* --chip esp32s3 burn_block_data --offset 8 BLOCK_USR_DATA serial_number.binVerify the serial number was fused:
espefuse.py --port /dev/cu.usbmodem* --chip esp32s3 dumpIf you change the fuses custom table in EFusesCustomTable.csv, you'll need to update the EFusesCustomTable.c definitions:
idf.py efuse-custom-tableNote that when you run the above command, the resulting include/EFusesCustomTable.h does not have the correct includes. You must add them manually to that file:
#include <esp_efuse.h>This is fixed in the pre-release version of ESP-IDF 5.0.
The USB serial connection protocol supports file transfers, device control commands, and debugging commands. The key commands include starting file transfers, device programming, restarting the device, retrieving device information, and controlling the device's user interface. On macs, /dev/cu.usbmodemsolreader1 will always be available for accepting commands and file transfers. /dev/cu.usbmodemsolreader3 will be available for logging.
Note that now logging to serial is OFF by default. To easily toggle that once you are connected via usb, issue this command:
echo "LOGGING_ON" | tee /dev/cu.usbmodemsolreader1
Command Handling The protocol is implemented using a command handler that processes incoming commands and performs corresponding actions. The following sections detail each command's functionality.
File Transfer Commands
START_FILE_TRANSFER
Initiates a file transfer process.
Action: Sets a passthrough data handler to handle file transfer data chunks.
Device Control Commands
PROGRAM
Disables logging and initializes JTAG for programming.
RESTART
Sends a success message and restarts the device.
DEVICE_INFO
Sends the device's serial number, MAC address, and free space.
MIRROR
Toggles the mirroring to the serial. This will send the framebuffer over serial on each display update.
Debug Commands The debug commands control the user interface for testing purposes. These commands include:
GRID: Displays a test alignment screen with maximum brightness.
UP: Sends an UP command to the user interface.
LEFT: Sends a LEFT command to the user interface.
RIGHT: Sends a RIGHT command to the user interface.
DOWN: Sends a DOWN command to the user interface.
ENTER: Sends an ENTER command to the user interface.
BACK: Sends a BACK command to the user interface.
LOGGING_ON: Turns on serial logging (on macs, this will be /dev/cu.usbmodemsolreader3)
LOGGING_OFF: Turns off serial logging
To change the glyphs in the Sol Sans character map:
- Download the IBMF Font Editor repo: https://github.com/turgu1/IBMFFontEditor
- Build the editor:
mkdir build
cd build
cmake ..
make -j
open IBMFFontEditor.app-
Commit both changes:
- src/UI/Fonts/IBMFFonts/SolSans_75.h
- src/UI/Fonts/IBMFFonts/SolSans_75.ibmf
A script is included to convert PNG images to XBM format. To use it, you'll need to install the Python requirements (Pillow). I recommend using a Python Virtual Environment to keep your system Python clean.
python3 -m venv venv
source venv/bin/activate
pip install PillowNow you can convert all .png images in src/UI/Bitmaps into .xbm files:
for f in src/UI/Bitmaps/*.png; do echo "Converting $f"; venv/bin/python scripts/image_to_xbm.py "$f" "${f%.png}.xbm"; doneIf you need to regenerate the app download qr code image, do it like this
qrencode -s 1 -l Q -m 0 -t PNG32 -o src/UI/Bitmaps/DownloadAppQr.png https://solreader.com/appYou might after a while want to clean up unused header bitmap files. You can do that with:
for f in src/UI/Bitmaps/*.hpp; do b=$(basename $f); git grep -q "Bitmaps/$b" || echo "$b is unused"; doneYou might also want to trace where the bitmaps are used:
for f in src/UI/Bitmaps/*.hpp; do b=$(basename $f); echo $f is used in; git grep -l "Bitmaps/$b"; doneStrip out non-dithered images (at least until we support grayscale)
for f in src/Storage/EPubs/*.epub; do \
echo "Stripping images from $f"; \
unzip "$f" -d "${f%.epub}"; \
find "${f%.epub}" -iname "*-greyscale.bmp" | xargs rm; \
rm "$f"; \
pushd "${f%.epub}"; \
zip -r "../$(basename $f)" *; \
popd; \
rm -r "${f%.epub}"; \
donefor f in src/Storage/EPubs/*.epub; do echo "Converting $f"; venv/bin/python scripts/file_to_header.py "$f" -o "${f}.hpp"; doneSimply tag the current commit with a version number like release/v1.2.3 and push tags. Github Actions will build the firmware and create a release with the firmware binaries attached.
Note: Don't use multiple tags on the same commit or git describe will get confused. Use this command to preview what IDF will use to generate the version string:
git describe --always --tags --dirty | cut -c 1-31To add a new esp-idf build target you need to do a few things:
- Add a new
TARGETif statement to the top of theMakefile(let's call itmy-new-targetfor this example). - Create a new
sdkconfig.defaults.my-new-targetfile. - Add any new build options as needed to the
src/Kconfig.projbuildfile.
See also the ESP-IDF docs on multi-target builds.