Open-source project
usetrmnl/trmnl-firmware avatar
usetrmnl/trmnl-firmware

TRMNL firmware wakes an e-ink display, asks for a picture and goes back to sleep

TRMNL ePaper device firmware

1,192 stars267 forksCGPL-3.0

At a glance

What is it?
The TRMNL firmware is the C software behind a small e-ink display device, and its whole design is a duty cycle: wake, connect, fetch one bitmap, show it, sleep. The README documents the algorithm as a state machine, the two web endpoints the device talks to, and the power numbers that result from the deep sleep approach.
Who is it for?
Adopt the TRMNL firmware if you are building a similar e-ink terminal, because the state machine, the two server endpoints and the power profile are documented in enough detail to copy. Do not flash the main branch to a device you depend on, since the README states it is under constant development and may contain breaking changes, and use a tagged release or the Flash Assistant instead.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The device talks to two endpoints and nothing else

The web server section is the protocol, and it is short. Following the Wi-Fi connection via the captive portal, the device swaps its MAC address for an API key and a friendly ID from the server, and those are saved on the device. The first request is a setup call, and the request carries the device's identifier in a header named `ID` whose value is the MAC address:

curl
GET /api/setup

headers = {
  'ID' => 'XX:XX:XX:XX:XX' # mac address
}

The success response returns a status, the api_key, the friendly_id, an image_url and a filename, and the failure case is a 404 with the same fields set to nil, which is how a device that the server does not know about is told so. After setup, all further requests are for image and display content only, and those carry the access token in an `Access-Token` header along with the device's state: a refresh rate, a battery voltage, a firmware version and an RSSI value. A device that the server cannot find for that token gets a 500 with a device not found error.

The display response can also ask for a firmware update

The `api/display` response is where the device is told what to do, and it carries more than an image. A status of 0 means success, and the documentation notes it will be 202 if no user_id is attached to the device. There is an image_url, which points at a bitmap hosted in object storage, and a filename. Then there are two booleans, `update_firmware` with a `firmware_url` to download from and `reset_firmware` for a soft reset, plus a `refresh_rate` that the device is expected to honour. The examples show the combinations, a plain success with no update, a success that needs a soft reset, and a success that needs a firmware update with the URL of a binary to fetch. The mechanism for that URL is stated at the end: if the `FW-Version` header the device sent does not match the server's configured firmware download URL, the server includes an absolute URL for the firmware to download.

The state machine is published as a diagram

The algorithm is documented as a flowchart, and reading it explains the firmware better than a paragraph would. It starts with initialising the peripherals and then asks whether the reset button was pressed for more than 5000 ms, which clears the Wi-Fi credentials. Otherwise it initialises the display and asks why the device woke, by the user or by the timer. A user wake clears the display, and a timer wake checks whether Wi-Fi is configured, showing a set-up message and starting the config portal if not, and that portal can itself trigger a device reset which wipes the API key, the friendly ID and the Wi-Fi settings before rebooting. The main body then tries to connect with five attempts, shows a connection error and sleeps if that fails, checks the clock synchronisation, and verifies that the API key and friendly ID exist, pinging `/api/setup` if they do not and showing a setup error on failure.

The update path is decided by pinging the server

The last part of the state machine is the firmware update, and the way it is triggered is unusual. Rather than the device checking a version endpoint of its own, the device pings the server and the server answers. If the ping succeeds, the firmware asks whether the device needs to reset, and if it does it wipes the configuration and reboots, using the same path as the factory reset. If not, it asks whether an update is needed, and if so it downloads and updates before sleeping again. The wake-and-check happens on every cycle, so the update mechanism costs nothing extra in power, because the device is already awake and connected to check the clock. There is also a log endpoint, `POST /api/log`, to which the device sends logs if it detects an issue with the response data from the display endpoint.

Deep sleep and a button on a GPIO are what make the battery last

The power section is the engineering argument for the whole design. The ESP32-C3 inside the TRMNL OG is described as one of Espressif's newer, more efficient microcontrollers, and for battery powered applications it is designed to be put to sleep when the project does not need it to be active. There are two sleep modes, light and deep, and deep conserves the most power but at the cost of losing the contents of main memory. The lowest possible power consumption is about 4uA at 3V with a timed wakeup, but TRMNL has to be able to wake on a button press, and keeping the GPIO active during deep sleep to detect that press uses about 100uA on average. That works out to a 2500mAh battery keeping the device powered in this state for approximately 25,000 hours. The trade is explicit: a GPIO that never sleeps is what you pay for a physical button.

The update cycle and its cost in coulombs

The full cycle is described with a power profile, and the numbers are specific. A normal display update is a timed wake, sending the device status, fetching the new image and showing it on the e-paper display, and the update cycle takes about 10.5 seconds. The peaks and valleys in the graph are the variation in electrical current drawn by the ESP32-C3 at different points in that cycle, with the majority of the energy used while Wi-Fi is active between the 3 and 9 second marks, and the higher frequency peaks at the end coming from the e-paper display cycling through its update, where the average power is quite low. The total electrical charge needed for the update is given in the lower right corner as 0.67 C, which the documentation converts into 0.186111 mAh and explains as the number of electrons that have moved through the circuit.

The repository is an ESP-IDF project with a hardware directory

The top level of the repository shows a firmware project rather than a single application. There is a `CMakeLists.txt` and a `platformio.ini`, so the project can be driven by either build system. The `boards/` directory holds the board definitions, `components/` the reusable parts, `include/` and `src/` the code, `lib/` the bundled dependencies, `sdkconfigs/` the per-target SDK configuration, `spec/` the specification, `test/` the tests with a `test.bmp` fixture, `data/` and `scripts/` for data and tooling, and `pics/` for the power profile images used in the README. There are separate dependency lock files for the three targets, `dependencies.lock.esp32c3`, `dependencies.lock.esp32c5` and `dependencies.lock.esp32s3`, which tells you the firmware is built for more than one Espressif part. The licence is GPL-3.0, and the release history shows a steady patch line with v1.8.17 from 2026-10-02, v1.8.16 from 2026-08-27 and v1.8.15 from 2026-08-21.

Editorial conclusion

Adopt the TRMNL firmware if you are building a similar e-ink terminal, because the state machine, the two server endpoints and the power profile are documented in enough detail to copy. Do not flash the main branch to a device you depend on, since the README states it is under constant development and may contain breaking changes, and use a tagged release or the Flash Assistant instead. Verify first which ESP32 variant you have, since the repository carries separate dependency lock files for the C3, C5 and S3, and that your server implements both the setup and display endpoints with the headers the firmware sends.

Frequently asked questions

How does the TRMNL device get its API key?

After the captive portal Wi-Fi connection, the device calls GET /api/setup with its MAC address in an ID header, and the server answers with an api_key, a friendly_id and an image_url. The device saves the key and friendly ID, and all later requests use the key in an Access-Token header.

How does the TRMNL device get a firmware update?

The server decides. On every cycle the device pings the server, and the api/display response carries update_firmware and firmware_url flags. If the FW-Version header the device sent does not match the server's configured firmware download URL, the server includes an absolute URL for the firmware binary.

How long does the TRMNL battery last?

Keeping the GPIO active to detect a button press during deep sleep uses about 100uA on average, so a 2500mAh battery could theoretically keep the device powered in that state for approximately 25,000 hours. The lowest possible consumption is about 4uA at 3V with a timed wakeup.

Should I flash the main branch of the TRMNL firmware?

The README warns that the main branch is under constant development and may contain breaking changes, and to flash at your own risk. For production-ready firmware it points at the tagged releases or the Flash Assistant.

What licence is the TRMNL firmware released under?

The GPL-3.0 licence, with the licence file in the repository root. The current release is v1.8.17 from 2026-10-02, and the last push to the main branch was on 2026-10-15.

Official sources

  1. License: GPL-3.0
  2. Project website
  3. README
  4. Releases
  5. usetrmnl/trmnl-firmware on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/usetrmnl-trmnl-firmware.svg)](https://hysenlabs.com/projects/usetrmnl-trmnl-firmware)

Was this useful?