Skip to content

feat: 8MB app + 7MB data partition, add recovery mode for OTA#951

Draft
ngxson wants to merge 5 commits intocrosspoint-reader:masterfrom
ngxson:xsn/15MB_app
Draft

feat: 8MB app + 7MB data partition, add recovery mode for OTA#951
ngxson wants to merge 5 commits intocrosspoint-reader:masterfrom
ngxson:xsn/15MB_app

Conversation

@ngxson
Copy link
Contributor

@ngxson ngxson commented Feb 17, 2026

Summary

This PR is currently a MVP to illustrate the discussion: #862 (reply in thread)

It allows allocating 15MB flash (8MB app + 7MB data) for crosspoint and 1MB for recovery, the same idea as "recovery mode" on android.

The reason why we cannot use the entire 15MB for app is because the linker refuse to link such app (I suppose it won't load-able either.) The 7MB data can be accessed either by doing mmap (demo in #596), or by using esp_flash_read()

To try this PR

  1. Build the default profile in platformio, then flash it
  2. Build the recovery profile in platformio, then flash it
  3. Go to Settings > System > Boot to recovery
  4. (NOTE: flashing is not yet implemented for now)

The procedure for OTA will be as followed:

  1. Firmware is either downloaded or copied into sdcard firmware.bin
  2. Device rebooted into recovery
  3. firmware.bin is picked up, verify hash
    • If hash verification fails, stop, reboot back to normal mode
    • If hash OK, proceed to flash
    • If process is interrupted is interrupted (for ex. device is restarted or crash), next boot will go back into recovery
  4. Verify flashed app
  5. Boot back to normal
  6. Upon boot completed, esp_ota_mark_app_valid_cancel_rollback is called to cancel rollback to recovery mode. If it's not called (the firmware failed to boot), it goes back to recovery

To discuss:

  • Any side effects?
  • Migration plan: This will break existing OTA implementation. May need a "migration firmware" just to modify the partition table and flash the recovery

TODO:

Bonus:


AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? NO

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ngxson
Copy link
Contributor Author

ngxson commented Feb 17, 2026

Before I proceed further, would like to tag @crosspoint-reader/firmware-maintainers for discussion. Would like to see if everyone is ok with this direction.

@znelson
Copy link
Contributor

znelson commented Feb 17, 2026

I'm not a maintainer, but this sounds like a great direction to me. Keeping half of the flash empty most of the time plus an unused spiffs partition is leaving CrossPoint unnecessarily constrained.

@Uri-Tauber
Copy link
Contributor

Bonus:

  • See how much we can benefit from 15MB of space: Enough to fit Core CJK + Hiragana + Katakana + Latin-Ext, but will need to somehow align to 4-byte, otherwise drom0_0_seg won't fit it due to MMU limitation

Is there enough room left to fit the Hebrew block (U+0590–U+05FF)?

@ngxson
Copy link
Contributor Author

ngxson commented Feb 17, 2026

Is there enough room left to fit the Hebrew block (U+0590–U+05FF)?

That's ~100 characters, we should have more than enough space.

The whole Core Unified Ideographs CJK (0x4E00, 0x9FFF) only takes 2MB

However, I'm running into issue that it fails to link the ELF if the total program size is more than 8MB. I should be onto something undocumented... The official docs tells that 16MB mapping is possible, but I doubt if it's actually correct

@ngxson
Copy link
Contributor Author

ngxson commented Feb 17, 2026

The whole Core Unified Ideographs CJK (0x4E00, 0x9FFF) only takes ~2MB

Update: that was not true. Turns out, the included ttf files doesn't even support CJK

@ngxson
Copy link
Contributor Author

ngxson commented Feb 17, 2026

Ok so after some more trials and error, I found out that:

  1. It is impossible to have app > 8MB, the esp32c3 simply doesn't accept it (see my test code below). I'm not sure if the app image format supports kinda non-mmap section or not, need to dig more
  2. Existing font seems to only support Latin-based languages. CJK fonts are quite rare, see: https://fonts.google.com/?lang=zh_Hans --> the better method to support them is to consider them as single dedicated font. AFAICT, bookerly doesn't have chinese version anyway.
Test code
#define ROW_16_BYTES 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10
#define ROW_32_BYTES ROW_16_BYTES, ROW_16_BYTES
#define ROW_64_BYTES ROW_32_BYTES, ROW_32_BYTES
#define ROW_128_BYTES ROW_64_BYTES, ROW_64_BYTES
#define ROW_256_BYTES ROW_128_BYTES, ROW_128_BYTES
#define ROW_512_BYTES ROW_256_BYTES, ROW_256_BYTES
#define ROW_1KB ROW_512_BYTES, ROW_512_BYTES
#define ROW_2KB ROW_1KB, ROW_1KB
#define ROW_4KB ROW_2KB, ROW_2KB
#define ROW_8KB ROW_4KB, ROW_4KB
#define ROW_16KB ROW_8KB, ROW_8KB
#define ROW_32KB ROW_16KB, ROW_16KB
#define ROW_64KB ROW_32KB, ROW_32KB
#define ROW_128KB ROW_64KB, ROW_64KB
#define ROW_256KB ROW_128KB, ROW_128KB

static const char TEST[] = {
  ROW_256KB ,
  ROW_256KB ,
  ROW_128KB ,
  ROW_64KB ,
  ROW_32KB ,
  ROW_16KB , // add or remove them to see
};

void setup() {
  t1 = millis();
  assert(TEST[millis() % sizeof(TEST)] != 0); // make sure it's not optimized out by the compiler
...

@znelson
Copy link
Contributor

znelson commented Feb 17, 2026

Ah, good find. This reference document, page 95, spells out that we get 8 MB for instruction bus and 8 MB for data bus. I bet it is possible for us to use esp_partition_mmap to access data like fonts in the other partition.

@ngxson
Copy link
Contributor Author

ngxson commented Feb 17, 2026

@znelson yes I'm aware of the limitation of 8MB inst + 8MB data. I wanted to try force-loading the font as instruction by adding __attribute__((section(".flash.text"))) for it, but the compiler still refuses to link the ELF.

Re. esp_partition_mmap I had a demo in #596 . The main problem is that I don't want to have yet another partition because it may be quite complicated to resize it in the future.

I'm thinking about an alternative: We can have a notion of "logical partition" which allow app partition to contain both app + assets, something like: cat firmware.bin assets.bin > final_firmware.bin ; just need to figure out how to store the offset

@ngxson
Copy link
Contributor Author

ngxson commented Feb 18, 2026

Alright, I put together a demo that allow concatenate firmware + assets as explained in the last message: https://github.com/ngxson/crosspoint-reader/tree/xsn/15MB_app_font

I added Latin-Ext codepoints which already allow reading Vietnamese books. This was not possible if I compiled with the normal way (font bitmap as header), the linker won't allow linking due to running out of space on drom0_0_seg, even with plenty of space left.

All 3 fonts take 5.71MB. No addition delay between page turns, even though I wasn't using mmap.

Firmware size: 3510928 bytes
Assets size: 5992079 bytes
Total size: 9503007 bytes
Size usage: [============        ]  9503007 of 15597568 bytes (60.93%)

I'll try experimenting with CJK next, a bit more complicated as explained above, as the current ttf files technically doesn't support CJK.

Demo vietnamese book (The story of a seagull and the cat who taught her to fly / Luis Sepúlveda):

image

@znelson
Copy link
Contributor

znelson commented Feb 18, 2026

I see. You're a few steps ahead 🙂

Edit: Oops, I was replying to your previous comment with "I don't want to have yet another partition".

Seems like it wouldn't be so bad to have 8 MB executable + 8 MB data/fonts (or 7 MB executable + 1 MB recovery + 8 MB data/fonts). But either way I'm interested to see what you figure out.

I hadn't seen your #596 until now. I pushed a quick (AI assisted) PoC of splitting fonts on to a data partition in #960.

@ngxson
Copy link
Contributor Author

ngxson commented Feb 18, 2026

@znelson Just note that my poc can also allow putting font into dedicated partition. Indeed, esptool has a command called "merge-bin" that add a 0-padding between app/data partitions, so when upload they will be in the correct place. My poc work that way, the main difference is that it doesn't add any padding.

Btw, one of my goals is to have app+data in one single file so it will be easier to validate the hash.

@ngxson
Copy link
Contributor Author

ngxson commented Feb 18, 2026

After some more testings, I concluded that CJK is not fit-able inside the flash. The whole "core" CJK is still to big. Just one single variant of Noto Sans SC regular, 12pt already took 6MB. So fitting 4 combinations: (bold, regular) x (12pt, 14pt) is impossible.

I experimented with filtering only simplified chinese from CJK, but on second thought, it's not very practical.

Also, CJK fonts are usually a dedicated font, they also don't have italic variant. So, it makes more sense to distribute them as user-installable font, something like: #455

However, given the benefit of the current PR is to allow using flash for other things: support latin-ext, icons, hyphenations, bluetooth stack, better image decoder, etc. I think it's still worth to proceed in this direction. Though I still would like to hear other maintainers' thoughts on this.

@daveallie
Copy link
Member

daveallie commented Feb 19, 2026

One constraining factor I've kept to date is the ability to revert back to the official firmware quickly by retaining the same partition structure as XTOS and just re-applying the latest OTA.

This would obviously relax that, and while it's something I'm definitely open to considering, I think we'd need to communicate the change, and update https://github.com/crosspoint-reader/xteink-flasher to suit.

@ngxson
Copy link
Contributor Author

ngxson commented Feb 19, 2026

One constraining factor I've kept to date is the ability to revert back to the official firmware quickly by retaining the same partition structure as XTOS and just re-applying the latest OTA.

I do consider this, more specifically, I think you're referring to the ability of having crosspoint in app0 and stock firmware in app1, right?

Indeed, this new partition layout will still work if you try to flash the stock firmware to offset 0x10000 of the flash. It just doesn't allow all both co-habit on the same flash. But I think it will be acceptable if we somehow support CJK in near future.

Here is the command I used for flashing stock fw:

esptool write-flash --flash-mode dio --flash-size 16MB 0x10000 ~/Downloads/V3.1.9_CH_X4_0117.bin

The only thing stock firmware need is the SPIFFS partition to store settings, but I will add it to the partition table as @znelson suggested earlier, mostly because I want to avoid misconception that app can be larger than 8MB (will explain more about this a bit later)

@ngxson ngxson changed the title feat: 15MB app partition, add recovery mode for OTA feat: 8MB app + 7MB data partition, add recovery mode for OTA Feb 21, 2026
@ngxson
Copy link
Contributor Author

ngxson commented Feb 22, 2026

Given the recent improvement in font storage usage, I'm a bit doubt if it still worth proceeding with this PR.

As explained in #951 (comment) and #1059 (comment) , it is not practically possible to add CJK to the firmware, mostly due to poor user experience. We could in theory, support just one CJK font with 4 variants (bold, regular) x (14pt, 16pt), but then user will eventually need both the serif and sans-serif options.

I will pause this PR a bit to see what are other benefits of having a bigger flash size. Will eventually close it if I cannot find any. If you have any ideas, feel free to comment.

@Uri-Tauber
Copy link
Contributor

Uri-Tauber commented Feb 23, 2026

I think it's worth continuing down this path a bit longer before calling it.

Yes, we have ~2MB free right now — but that headroom tends to erode faster than expected. New features, UI additions, community contributions... it adds up. Getting the partition layout right while we have the chance is just good planning.

And even if full CJK support may stays out of reach, a larger flash layout pays off in plenty of other ways:

  • Future features – less pressure when new functionality comes in.
  • Hyphenation dictionaries – these aren't always small, and supporting more languages meaningfully improves reading experience.
  • UI translations – room to localize properly without constantly bumping into limits.
  • Font coverage – Latin-Ext, Hebrew, Arabic, etc. would already help users who don't need CJK at all.
  • Graphics/UI assets – space for better or additional UI resources.

Even if CJK never makes it onto flash, more headroom is a long-term win. Margin is rarely wasted.

P.S. while I'm eagerly waiting for to custom user fonts, I do think we should always keep at least one font baked into the flash — and it should cover as much of Unicode as we can reasonably fit.

You're doing amazing job! Thank you so much for your contributions.

@osteotek
Copy link
Member

osteotek commented Feb 23, 2026

Papyrix supports CJK, arabic, thai, vietnamese in same firmware.bin, all while keeping same partition size. So it's doable

@Uri-Tauber
Copy link
Contributor

@osteotek I believe Papyrix stores all CJK, Arabic and Thai fonts on the SD card, While having one reader font and one UI font embedded in the firmware.

@osteotek
Copy link
Member

osteotek commented Feb 23, 2026

@osteotek I believe Papyrix stores all CJK, Arabic and Thai fonts on the SD card, While having one reader font and one UI font embedded in the firmware.

Right. I just flashed Papyrix and was able to open arabic epub but not chinese, looks like external font is required for CJK.

One difference Papyrix has is they are keeping only one reader font in one size embedded in the firmware. We may have to do the same if we want as much Unicode support as possible out of the box

@ngxson
Copy link
Contributor Author

ngxson commented Feb 23, 2026

IMHO, opendyslexic should be outside of the firmware once custom font is supported in the future. I feel like it may not help much on a device with such small screen like this. IIRC, my friend how has dyslexia only uses a ruler when reading text on A4 paper; reading on a mobile phone screen is fine.

And from my own experience: opendyslexic on kindle looks good, but on xteink it looks too crammed

@Uri-Tauber
Copy link
Contributor

With #1157, flash usage will be at

Flash: [========= ] 89.4% (used 5,856,181 bytes from 6,553,600 bytes)

This really reinforces my earlier point about how urgently we need additional space. Expanding the layout sooner rather than later would give us breathing room and prevent us from constantly optimizing under pressure.

@ngxson
Copy link
Contributor Author

ngxson commented Feb 26, 2026

Yes I'll go back to this after #1016

@ngxson
Copy link
Contributor Author

ngxson commented Mar 2, 2026

Annoyingly, it becomes too difficult to fit the recovery firmware into 1MB.

I had to (almost) completely separate its source code into a sub-project, with a dedicated font (only ASCII, no unicode) And yet it's barely fit into 983KB

And board_upload.offset_address is broken too, pioarduino/platform-espressif32#137, making the recovery to always be flashed to the wrong offset when using pio command

@ngxson
Copy link
Contributor Author

ngxson commented Mar 2, 2026

@Jason2866 tagging you since you said that you're the maintainer of pioarduino: is there any method to flash the custom recovery image (same idea as safeboot) to the correct board_upload.offset_address? AFAIK this option has been removed from pioarduino

@Jason2866
Copy link
Contributor

Jason2866 commented Mar 2, 2026

@ngxson Add your own boards.json. This declutters the platformio.ini and you can flash everything you want.
See https://github.com/arendst/Tasmota/blob/development/boards/esp32c3.json as an example the recovery firmware is at 0x10000 and the normal firmware is at 0xe0000. Using a custom partitions.json https://github.com/arendst/Tasmota/blob/development/partitions/esp32_partition_app2880k_fs320k.csv

Tasmota does use a small factory (recovery) firmware for flashing. In https://github.com/arendst/Tasmota/blob/development/pio-tools/post_esp32.py the factory (safeboot) firmware is added for flash process

Btw board_upload.offset_address is not broken. It is changed by design in pioarduino. The Platformio way is to limited.

To have an recovery firmware under 1MB is no problem. Take a look at the size of the safeboot (recovery) firmware of Tasmota. Honestly the project includes debatable features for example WebDAV. This is for embedded devices a fail. Furthermore it more or less works only with Windows.

Another edit: Factory firmware has to be at 0x10000 not the normal firmware. It is the fallback address where the underlying IDF code tries to start when stuff goes wrong.

@ngxson
Copy link
Contributor Author

ngxson commented Mar 2, 2026

To have an recovery firmware under 1MB is no problem. Take a look at the size of the safeboot (recovery) firmware of Tasmota. Honestly the project includes debatable features for example WebDAV. This is for embedded devices a fail. Furthermore it more or less works only with Windows.

safeboot has the advantage to be a dedicated project, while here I'm trying to reuse as much code as possible to make a MVP.

I can easily split it this into a dedicated minimal project that fit into < 800KB though.

Btw board_upload.offset_address is not broken. It is changed by design in pioarduino. The Platformio way is to limited.

agree, it's not broken, I said that "it's removed"

Add your own boards.json. This declutters the platformio.ini and you can flash everything you want.
See https://github.com/arendst/Tasmota/blob/development/boards/esp32c3.json as an example the recovery firmware is at 0x10000 and the normal firmware is at 0xe0000. Using a custom partitions.json https://github.com/arendst/Tasmota/blob/development/partitions/esp32_partition_app2880k_fs320k.csv

if I understand correctly, the info you present is the case where safeboot is already compiled to .bin and the board.json is configured such that it only flash the pre-compiled tasmota32c3-safeboot.bin, but not to compile it.

however, since this is an MVP, I need to compile and flash the recovery app in a simple way. now that you confirmed that there is no other way to get around the fact that board_upload.offset_address is no longer support, I assume that I now need to hook into post_build or even simpler, make my own flash_recovery.sh script.

ofc, other (proper) methods can be considered, but I'm doing a MVP, and simplicity is the main point here.

@Jason2866
Copy link
Contributor

The Tasmota scripts fetches precompiled safeboot bin if no locally build is found, and the post script combines them to a full factory image which gets automatically uploaded from VSC. The safeboot is a env and can be build as the normal firmware.

@Jason2866
Copy link
Contributor

@ngxson
Copy link
Contributor Author

ngxson commented Mar 2, 2026

@Jason2866 I highly recommend you to read the description of this PR, as long as #862 (reply in thread) for the context.

the post script combines them to a full factory image

I did consider this way, given that we will surely need to combine the app+spiffs in the future.

However, my point still stands: the ability to compile and flash only the recovery image of < 1MB makes the development much easier.

@ngxson Do you know this https://github.com/mathieucarbou/MycilaSafeBoot

That's basically what I referred to by saying:

safeboot has the advantage to be a dedicated project, while here I'm trying to reuse as much code as possible to make a MVP.

The main differences between safeboot and crosspoint's recovery mode are:

  • user can interact with recovery mode via the einkscreen
  • sd card is supported in recovery, and firmware.bin can be placed inside sd card
  • when a new firmware version is downloaded, it's downloaded to sd card first --> reboot to recovery --> flash --> reboot back

The TL;DR is that this is the same thing as recovery mode on android, hence the name

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants