InfiniTimeOrg / InfiniTime

Firmware for Pinetime smartwatch written in C++ and based on FreeRTOS
GNU General Public License v3.0
2.64k stars 902 forks source link

Applications selection at build time #1894

Closed JF002 closed 7 months ago

JF002 commented 8 months ago

Why ?

InfiniTime is a monolithic firmware : everything (core OS, UI lib, BLE stack, apps) is built into a single firmware image. The current implementation does not allow to easily add new applications and select which apps must be built into the firmware. This PR tries to improve those points.

Select which apps must be built into the firmware:

You can easily select the applications that will be built into the firmware by adding/removing app from UserAppTypes:

    using UserAppTypes = TypeList<Apps::Alarm,
                                  Apps::HeartRate,
                                  Apps::Paint,
                                  Apps::Metronome,
                                  Apps::Music,
                                  Apps::Navigation,
                                  Apps::Paddle,
                                  Apps::Steps,
                                  Apps::StopWatch,
                                  Apps::Timer,
                                  Apps::Twos
                                  >;
  }

All the apps that are passed as template parameters to UserAppTypes will be built into the firmware. Moreover, the order in this list represents the order of the apps in the application menu.

The menu is automatically sized (number of pages and number of apps per page) to fit all the apps: InfiniSim_2023-10-23_204019 InfiniSim_2023-10-23_204100 InfiniSim_2023-10-23_204138

Add a new app

As always, start by implementing your app in a class that derives from Pinetime::Applications::Screens::Screen.

Add the .CPP file in the list of source files in the CMake file of the project.

Then, add a new enum value in Pinetime::Application::Apps

Additionally, declare a new AppTraits corresponding to your app. Here is an example from the Metronome app: You simply need to define the name, an icon, and a method Create() that creates the application:

    template <>
    struct AppTraits<Apps::Metronome> {
      static constexpr Apps app = Apps::Metronome;
      static constexpr const char* icon = Screens::Symbols::drum;
      static Screens::Screen* Create(AppControllers& controllers) {
        return new Screens::Metronome(controllers.motorController, *controllers.systemTask);
      };
    };

Finally, add the enum of your application in the template parameters of UserAppTypes

How ?

This is made possible by a bit of template meta programming magic in UserApps.h. Basically, the function CreateAppDescriptions() creates a std::array of AppDescription at build time according to the enum values listed in UserAppTypes. It means that this array is created at build time and then stored in flash memory. It doesn't cost anything at runtime.

Applications are now either "System applications" or "User applications". System applications are apps that are expected to be always built in the firmware (notifications, clock, settings,...). InfiniTime does not work properly if those apps are not present. User applications are applications that are optionally built into the firmware.

App creation

When a new app must be loaded, DisplayApp first checks if the app is a System app, in which case it simply creates it. If it's not a System app, it looks into the list of User applications and creates it if it's found:

const auto* d = std::find_if(userApps.begin(), userApps.end(), [app](const AppDescription& appDescription) {
        return appDescription.app == app;
      });
      if (d != userApps.end())
        currentScreen.reset(d->create(controllers));

App menu

The App menu, ApplicationList now receives the list of apps as parameters. This list is generated by DisplayApp from the array of AppDescription generated at build time. It builds the menu pages according to this list of Apps.

Results

Developers can now add their application more easily than before, and no data is duplicated.

Developers and fork-maintainers can also select the apps they want to build in their firmware by editing a single line of code. This allows for easier customization of the firmware.

Firmware image sizes:

What's next ?

github-actions[bot] commented 8 months ago
Build size and comparison to main: Section Size Difference
text 377616B 88B
data 940B 0B
bss 63492B 72B
minacode commented 8 months ago

Nice work! Having tried around in that area a few times in the last year, I came roughly to the same design conclusions, so I obviously like this PR! This is also way better C++, I guess 😄

One more what's next idea that is in my mind: dependency relations to settings and controllers of apps. In a perfectly modular system, each app would bundle its settings and background tasks/controllers that get registered to the system.

This is one step closer to applications that are dynamically loaded at runtime.

It's true, but I wonder how much of the compile-time magic must be changed to runtime for that. But let's focus on that later.

Now I have to find some time to polish the calculator and test this PR, because if optional apps get merged, it will be a huge step! Thank you for spending the time doing it 🥳

kieranc commented 8 months ago

This seems huge, can it be used/extended for watchfaces too? I would absolutely love to make a build bot (like https://nodemcu-build.com/) which allows the user to select apps/faces and provides them a current build ready to flash.

JF002 commented 8 months ago

@minacode

Nice work! Having tried around in that area a few times in the last year, I came roughly to the same design conclusions, so I obviously like this PR! This is also way better C++, I guess 😄

Thanks for your feedback! I had this idea in my head for quite some time but... templates are not easy... I honestly can't tell how many hours of CPPCON video I watched before I could write such code :D

One more what's next idea that is in my mind: dependency relations to settings and controllers of apps. In a perfectly modular system, each app would bundle its settings and background tasks/controllers that get registered to the system.

That's probably one of the next steps ! I think we could reduce the coupling between apps and settings/systemtask by having the app "register" to some system events and also by providing them an object that allows them to write their own settings in the file.

It's true, but I wonder how much of the compile-time magic must be changed to runtime for that. But let's focus on that later.

The compile time magic probably won't apply for dynamic apps, but having all the apps behave the same way (like constructing them using the same Create() method) should help also for dynamic apps.

Now I have to find some time to polish the calculator and test this PR, because if optional apps get merged, it will be a huge step! Thank you for spending the time doing it 🥳

Thank, and thank you for your contributions!

@kieranc

This seems huge, can it be used/extended for watchfaces too? I would absolutely love to make a build bot (like https://nodemcu-build.com/) which allows the user to select apps/faces and provides them a current build ready to flash.

I already have a couple of ideas to used similar implementation for the watchfaces, yes. Watchfaces are probably the apps that need the most flash space! And a tool that allows to easily build a customized firmware would definitely be awesome!

JF002 commented 7 months ago

The CI check that fails is caused by InfiniSim that builds in C++17 while we now need C++20. I created a PR to fix this.