Skip to content

Conversation

@MattsGitCode
Copy link

This PR adds a feature to the crates for building the OpenThread library in FTD mode. To support this, it also:

  • Conditionally sets the OpenThread CMake defines for MTD and FTD
  • Conditionally links the MTD and FTD libs in the build script, depending on the feature flag state
  • Adds a new flag to the xtask binary to build the FTD libs
  • Implements an additional callback, otPlatRadioEnableSrcMatch, however the required function is not exposed in the HAL at this time, so it's just a no-op

I've tested this works on a couple of ESP32-C6 boards, one running with the FTD feature. The FTD board successfully promotes itself to leader, and the MTD board is able to become a child.

@ivmarkov
Copy link
Collaborator

Nice! Afk right now, please be patient...

@MattsGitCode
Copy link
Author

As I've continued to add more FTD feature support (SRP server, DNS-SD server) I realise that the possible combinations of OpenThread configurations isn't really compatible with the approach of precompiling the libraries.

The approach I've got working locally right now is to use the pre-compiled libraries only when the combination of feature flags matches the default configuration, otherwise compile fresh ones. This seems more flexible as more OpenThread features are conditionally added, but I'm not sure if this approach aligns with the project's goals and would welcome guidance.

For ESP32 it's trivial to get a working toolchain using Espressif's espup, which is installable from Cargo. I don't know about the NRF targets, but then I also don't know if the intention is to keep support for those targets, given this repo is under esp-rs now.

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jan 4, 2026

Hey @MattsGitCode keep up the good work! :)
I'm back.

As I've continued to add more FTD feature support (SRP server, DNS-SD server) I realise that the possible combinations of OpenThread configurations isn't really compatible with the approach of precompiling the libraries.

Yes.
This would lead to a combinatoric explosion of precompiled libraries.
The idea had always been to have pre-compiled libraries only for a list of "sane defaults". If these defaults are not met, then the library should compile itself on the fly.

It is trivial to check the enabled cargo features in build.rs and fallback to building on-the-fly if the selected features do not match the ones with which the precompiled libraries were build.

The approach I've got working locally right now is to use the pre-compiled libraries only when the combination of feature flags matches the default configuration, otherwise compile fresh ones. This seems more flexible as more OpenThread features are conditionally added, but I'm not sure if this approach aligns with the project's goals and would welcome guidance.

It does. As per above, ideally you could add the check to build.rs so that the build falls-back to building on-the-fly if the selected features are not the "default" ones.

For ESP32 it's trivial to get a working toolchain using Espressif's espup, which is installable from Cargo. I don't know about the NRF targets,

For NRF it is also relatively trivial to get the ARM GCC toolchain installed.
We need (a) GCC toolchain, because
(a) we are compiling currently OpenThread with GCC rather than clang, but that might change or we might provide an option to the user - I'm in fact implementing such an option in the (esp-)mbedtls sibling project as we speak.
(b) OpenThread does need a sysroot (a path to the system .h files) and only GCC can provide the sysroot. Clang comes "sysrootless" so to say and relies on the user somehow installing the sysroot somehow (for cross-builds this means installing the GCC crosstoolchain) and then pointing clang to the sysroot. We might get rid of the need for sysroot if we device our own small sysroot directly in the openthread-sys crate with a few custom-crafted .h files. (at link time we don't really need the sysroot because of reasons); but hand-crafting .h files might or might not be a huge effort.

but then I also don't know if the intention is to keep support for those targets, given this repo is under esp-rs now.

The project was always under the esp-rs namespace. Originally, it was esp-specific (and named esp-openthread), but no more.

If you have a better idea for an org to host the project, I'm all ears, but esp-rs is relatively popular, so unless something brilliant comes up, I intend to keep it there, even if it is cross-platform in the meantime.

(Ditto for (esp-)mbedtls BTW which I'm "cross-platforming" as we speak.)

@MattsGitCode
Copy link
Author

MattsGitCode commented Jan 4, 2026

Thanks @ivmarkov,

The idea had always been to have pre-compiled libraries only for a list of "sane defaults".
It does. As per above, ideally you could add the check to build.rs so that the build falls-back to building on-the-fly if the selected features are not the "default" ones.

That sounds sensible, I'll tidy up my local spike and get it pushed.

The project was always under the esp-rs namespace. Originally, it was esp-specific (and named esp-openthread), but no more.

Ah, thank you for the clarifying, I saw #27 and somehow inferred a reverse direction of travel.

@MattsGitCode
Copy link
Author

I've pushed up the change for this now, @ivmarkov.

Since the ability to use pre-generated libraries and bindings depends on OpenThread's configuration, rather than the crate's feature flags, I've used them to drive the conditional. In doing so, I've pulled out the definition of OpenThread config flags from builder.rs to allow them to be more easily used.

I've encoded a hash of the OpenThread configuration into the path of the pre-generated bindings and libraries in order to provide a bit of safety moving forwards. This also leaves the door open to providing another set of "sane defaults" in the future.

I've got some failing CI checks that don't fail locally, I'll take a look at those when I can.

@MattsGitCode
Copy link
Author

There seems to be something introduced in Rust nightly-2025-12-27 that causes it to incorrectly flag some existing code as having identical if/then branches.

I can't see that anything requires nightly rust, and so I propose that the CI workflows use stable Rust instead.

@MattsGitCode
Copy link
Author

I've updated the README to expand on when the build script will kick in.
I've also added a note about how to build the GNU toolchain for RISCV in order to do a FTD build.

@ivmarkov, this is ready for review when you have time

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jan 7, 2026

@MattsGitCode Sorry for providing feedback so late but I was literally swamped with other PRs upon my return, and I also had to work on esp-mbedtls.

In general, I do like the code in the PR, so no objections there. Just a few small nits we can address later.

My (major) concern is the following - and I must apologize as I framed it wrongly when typing on my phone - when I was explaining here that we have a combinatoric explosion issue I guess I named it wrongly. What I meant was not really "how to manage the various precompiled builds of openthread for all the possible features we might have". This you have figured out very well.

My concern was rather a size constraint concern. I.e.:

  • given X platforms we would like to support with precompiled builds (where X would likely be > 6)
  • and given Y possible permutations of precompiled builds (i.e. FTD, MTD, whatever)

... we end up with a X * Y precompiled versions of the library, where this number might be 14 in the best case, and more like 24 or more in reality.

Now, once we publish the openthread-sys crate on crates.io all these libs will end up inside openthread-sys. That is, together with the full source-code of OpenThread itself, for supporting on-the-fly builds.

What is giving me "uncomfort" is therefore this.

Would we be able to fit in the 10MB limit - compressed - if we have too many precompiled variations? And again, keep in mind we have to count towards the compressed crate bundle not just the .a libs, but also the source code of openthread (Rust) and... OpenThread (the C GH submodule)

  • If yes, then sure, the whole hashing of selected features => config.txt => pregenerated_ under the library directory name makes sense
  • If no, then we have to constrain ourselves to just two permutations: MTD (and maybe FTD), but then we don't really need the whole "pregenerated" infrastructure you invented just for two variations

So the question is a bit, can we estimate the size of the problem before we continue further?

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jan 7, 2026

Small correction: the supported platforms might be < 7 or ~ 7. Namely:

  • riscv32imc-unknown-none-elf - for esp32c3 (EDIT: Oh my, this is out as the c3 does not have a radio)
  • riscv32imac-unknown-none-elf - for all other esp32cXX and esp32hXX. NOTE: we might pass on that as to my knowledge OpenThread does not use atomics, so the code generated for the riscv32imc-unnown-none-elf might also work for the h2, c5, c6 and so on
  • the thumbv7 - this is NRF52
  • more Cortex-Ms (??!!) for NRF53, 54...
  • Any other MCU with IEEE802.15.4 radio???

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jan 7, 2026

More info on the targets:

I just figured out the above might not be the only targets... :(
What if we want to support the use case where the Radio is on the actual IEEE 802.15.4 MCU (say, an esp32c6), while the actual OpenThread code runs on another MCU (say, esp32s3) and then they communicate over - say - UART with the radio frames serialized/deserialized over UART?

This would increase the number of platforms for which we would need precompiled binaries by at least 2 or 3 (esp32s2, esp32s3, esp32) or even more.

But this also brings the question of... do we really want to do our own Rust-based serde between the actual radio and the OpenThread running on the other MCU (I'm inclined towards this)

... or do we want to buy into the (what was the terminology in OpenThread again?) their NCP/RCP framework. Which would mean another permutation => even more .a libs => more crate size bloat.

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jan 7, 2026

Yeah of the two we want RCP it seems.
But then again, this "Spinel" protocol they invented for that... what is this? do we really need it? why can't we just send/recv over UART a bunch of IEEE802.15.4 frames in pure Rust and be done with it?

I.e. a UartRadioProxy implementation of sorts on the MCU driving the OpenThread stack, and a UartRadioStub of sorts on the MCU running just the IEEE802.15.4 radio...

@MattsGitCode
Copy link
Author

All very valid concerns.

What I meant was not really "how to manage the various precompiled builds of openthread for all the possible features we might have".

I understood your meaning, I think. My primary goal with encoding the hash into the file path wasn't to allow multiple variants, but rather to provide safety for future changes in this crate. If the precompiled libs got out of step with the "sane defaults" defined in the build script, then it would lead to potential frustration for consumers of this crate. The hash just offers a safety net so that even if things get out of step, they will recompiled on-the-fly.

A gzip of the openthread-sys directory today is about 8.7MB, so it's not going to take much to knock it over the 10MB limit.

Personally I think it might be better to remove the precompiled libs and just provide a better story for getting a toolchain up and running. It took me the better part of two evenings to figure out how to get the GNU RISCV toolchain built correctly. For ESP targets, it could favour the ESP-provided toolchain if present, which is much more idiot (me) proof. I imagine other vendors offer similar idiot proof toolchains too. In all cases it could fall back to the GNU toolchain if needed.

If multiple precompiled lib configurations are desired, there could always be multiple crates:

  • openthread-sys-mtd - which has a set of sane defaults for MTDs, perhaps with quite a few features enabled
  • openthread-sys-ftd - same but for FTDs
  • openthread-sys - which has nothing precompiled, and can be used for other variants, such as a slimmer version of one of the above

but then we don't really need the whole "pregenerated" infrastructure you invented just for two variations

You're absolutely right, if there's never going to be more than one or two, it might seem a bit overkill. That said, even without the hashing, I'd argue that the OpenThreadConfig is a nice way to get from Cargo feature flags to OpenThread config flags, and it encapsulates the concept of the sane defaults neatly.

@MattsGitCode
Copy link
Author

But then again, this "Spinel" protocol they invented for that... what is this? do we really need it? why can't we just send/recv over UART a bunch of IEEE802.15.4 frames in pure Rust and be done with it?

I've been considering a similar question about using Rust crates for features that OpenThread provides, specifically a DNS client supporting service discovery queries). I'm leaning towards OpenThread being battle-tested, and already pulled in as a dependency, so why not just use that.

@MattsGitCode
Copy link
Author

Thinking further on my multiple crates comment above. This would align better with Cargo's features-are-additive requirement. As it stands today these crates cannot be used safely in a workspace that needs multiple variants.

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jan 8, 2026

A gzip of the openthread-sys directory today is about 8.7MB, so it's not going to take much to knock it over the 10MB limit.

did you remove the /target dir before checking?

Personally I think it might be better to remove the precompiled libs and just provide a better story for getting a toolchain up and running. It took me the better part of two evenings to figure out how to get the GNU RISCV toolchain built correctly. For ESP targets, it could favour the ESP-provided toolchain if present, which is much more idiot (me) proof. I imagine other vendors offer similar idiot proof toolchains too. In all cases it could fall back to the GNU toolchain if needed.

My feeling is that would be a disaster. Judging from my experience with the esp-idf-* crates, which do compile on the fly. And which even do download the correct GCC cross toolchain for the user. Rust novices just can't be bothered with with C toolchains that's the harsh reality. If we can get rid of the sysroot, then maybe we can switch the C compilation step to clang, that might help as clang is often pre-installed, and it is a single cross-compiler. That would work for everything but xrtensa which needs a patched clang.

If multiple precompiled lib configurations are desired, there could always be multiple crates:

  • openthread-sys-mtd - which has a set of sane defaults for MTDs, perhaps with quite a few features enabled
  • openthread-sys-ftd - same but for FTDs
  • openthread-sys - which has nothing precompiled, and can be used for other variants, such as a slimmer version of one of the above

That might be an idea if indeed we are at 8.7MB.
We can even do it in a way where the specialized openthread-sys-* do depend on the base openthread-sys if necessary.

but then we don't really need the whole "pregenerated" infrastructure you invented just for two variations

You're absolutely right, if there's never going to be more than one or two, it might seem a bit overkill. That said, even without the hashing, I'd argue that the OpenThreadConfig is a nice way to get from Cargo feature flags to OpenThread config flags, and it encapsulates the concept of the sane defaults neatly.

That's true.

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jan 8, 2026

Thinking further on my multiple crates comment above. This would align better with Cargo's features-are-additive requirement. As it stands today these crates cannot be used safely in a workspace that needs multiple variants.

Not sure I follow your comment here?
Can you elaborate? I think the crates are perfectly usable, it is just that if an extra feature is enabled, or a feature is disabled, the build automatically switches to on-the-fly build for OpenThread, which is the desired outcome anyway.

@MattsGitCode
Copy link
Author

did you remove the /target dir before checking?

I thought I had but I'll try again later.

My feeling is that would be a disaster. Judging from my experience with the esp-idf-* crates

That's fair. I'm coming back to embedded Rust after 6 or 7 years away from it, so far the experience has been considerably smoother than back then. Perhaps I was too optimistic :)

Thinking further on my multiple crates comment above. This would align better with Cargo's features-are-additive requirement. As it stands today these crates cannot be used safely in a workspace that needs multiple variants.

Not sure I follow your comment here?
Can you elaborate? I think the crates are perfectly usable, it is just that if an extra feature is enabled, or a feature is disabled, the build automatically switches to on-the-fly build for OpenThread, which is the desired outcome anyway.

Apologies, this was an ill-thought-out comment based on half-baked assumptions. Everything in main is fine in workspaces. It's the introduction of on-the-fly build configurations that introduces an issue. I've put together an example to confirm those assumptions here.
If you build this with -vv you'll notice only a single build of OpenThread, with the union of features requested by the ftd and mtd packages. This means that the MTD device will also operate as an FTD.

That said, it's an existing problem in the embedded space. If you change the default feature of one of those workspace packages to be the H2 instead, you get build errors because ESP-HAL also has feature flags intended to be mutually exclusive. So based on this, there should probably be no expectation of openthread-sys to be any different.

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.

2 participants