Merge develop into main (#343)

- Refactor `AppManifest`: add new fields and rename existing ones
- Parse and validate the manifest from an app that is being installed.
- Remove deprecated `scoped()` from `Lock`
- Create `Tactility/Paths.h`
- App loading at boot now properly parses the manifest files of external apps
- Properly lock both source and destination locations during app install
- Remove LVGL path variants from `AppPaths` and `ServicePaths`
- Removed `xPath` base classes for apps and services. There's now `AppPaths` and `ServicePaths`.
- Renamed app and service paths: "data" and "system" paths are now "user data" and "assets"
This commit is contained in:
Ken Van Hoeylandt 2025-09-22 08:03:21 +02:00 committed by GitHub
parent a4d15b2a1e
commit bab3eb19bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
108 changed files with 817 additions and 757 deletions

View File

@ -17,7 +17,7 @@ class HelloWorldApp : public App {
}; };
extern const AppManifest hello_world_app = { extern const AppManifest hello_world_app = {
.id = "HelloWorld", .appId = "HelloWorld",
.name = "Hello World", .appName = "Hello World",
.createApp = create<HelloWorldApp> .createApp = create<HelloWorldApp>
}; };

View File

@ -1,4 +1,7 @@
#include "CYD4848S040C.h" #include "CYD4848S040C.h"
#include "Tactility/kernel/SystemEvents.h"
#include "Tactility/lvgl/LvglSync.h"
#include "devices/St7701Display.h" #include "devices/St7701Display.h"
#include "devices/SdCard.h" #include "devices/SdCard.h"
@ -12,7 +15,7 @@ static bool initBoot() {
static DeviceVector createDevices() { static DeviceVector createDevices() {
return { return {
std::reinterpret_pointer_cast<Device>(std::make_shared<St7701Display>()), std::make_shared<St7701Display>(),
createSdCard() createSdCard()
}; };
} }
@ -59,7 +62,7 @@ const Configuration cyd_4848s040c_config = {
} }
}, },
.spi { .spi {
//SD Card // SD Card & display init
spi::Configuration { spi::Configuration {
.device = SPI2_HOST, .device = SPI2_HOST,
.dma = SPI_DMA_CH_AUTO, .dma = SPI_DMA_CH_AUTO,
@ -68,20 +71,20 @@ const Configuration cyd_4848s040c_config = {
.miso_io_num = GPIO_NUM_41, .miso_io_num = GPIO_NUM_41,
.sclk_io_num = GPIO_NUM_48, .sclk_io_num = GPIO_NUM_48,
.quadwp_io_num = -1, .quadwp_io_num = -1,
.quadhd_io_num = -1, .quadhd_io_num = GPIO_NUM_42,
.data4_io_num = -1, .data4_io_num = -1,
.data5_io_num = -1, .data5_io_num = -1,
.data6_io_num = -1, .data6_io_num = -1,
.data7_io_num = -1, .data7_io_num = -1,
.data_io_default_level = false, .data_io_default_level = false,
.max_transfer_sz = 8192, .max_transfer_sz = 1024 * 128,
.flags = 0, .flags = 0,
.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO, .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO,
.intr_flags = 0 .intr_flags = 0
}, },
.initMode = spi::InitMode::ByTactility, .initMode = spi::InitMode::ByTactility,
.isMutable = false, .isMutable = false,
.lock = nullptr .lock = tt::lvgl::getSyncLock()
} }
} }
}; };

View File

@ -11,7 +11,9 @@ std::shared_ptr<SdCardDevice> createSdCard() {
GPIO_NUM_NC, GPIO_NUM_NC,
GPIO_NUM_NC, GPIO_NUM_NC,
GPIO_NUM_NC, GPIO_NUM_NC,
SdCardDevice::MountBehaviour::AtBoot SdCardDevice::MountBehaviour::AtBoot,
tt::lvgl::getSyncLock(),
std::vector { GPIO_NUM_39 }
); );
auto sdcard = std::make_shared<SpiSdCardDevice>( auto sdcard = std::make_shared<SpiSdCardDevice>(

View File

@ -1,39 +1,32 @@
# Contributing # Contributing
## Accepted changes ## New features and boards
Before releasing version 1.0.0, the APIs are changing rapidly. Feel free to open an [issue](https://github.com/ByteWelder/Tactility/issues/new)
I want to minimize the amount of changes that I have to maintain, because it limits the amount of breaking changes that I have to deal with when the APIs change. to discuss ideas you have regarding the implementation of new boards or features.
### New features Keep in mind that the internal APIs are changing rapidly. They might change considerably in a short timespan.
This means it's likely that you get merge conflicts while developing new features or boards.
These are currently not accepted. ## Fixing things
### Visual / interaction design changes The general take here is that minor changes are not accepted at this stage.
I don't want to spend my time reviewing and discussing these during the current stage of the project.
These are currently not accepted. I'm aware that there is a lot of room for improvement, but I want to mainly focusing on strong technical foundations before
I start building further on top of that.
Feel free to open an [issue](https://github.com/ByteWelder/Tactility/issues/new) to discuss ideas you have, if you feel like they will have a considerable impact.
### Fixing things
The general take here is that minor changes are not accepted at this stage. I don't want to spend my time reviewing and discussing these during the current stage of the project.
Only fixes for serious issues are accepted, like: Only fixes for serious issues are accepted, like:
- Bugs - Bugs
- Crashes - Crashes
- Security issues - Security issues
Some examples of non-serious issues include: Some examples of non-serious issues include:
- Documentation changes
- Code documentation changes
- Renaming code - Renaming code
- Fixes for compiler warnings that have low impact on the actual application logic (e.g. only happens on simulator when calling a logging function) - Typographical errors
- Fixes for compiler warnings that have a low impact on the actual application logic (e.g. only happens on simulator when calling a logging function)
### New board implementations ## Anything that doesn't fall in the above categories?
Please open an [issue](https://github.com/ByteWelder/Tactility/issues/new) on GitHub to discuss new boards.
### Anything that doesn't fall in the above categories?
Please [contact me](https://tactility.one/#/support) first! Please [contact me](https://tactility.one/#/support) first!
@ -42,6 +35,13 @@ Please [contact me](https://tactility.one/#/support) first!
Pull requests should only contain a single set of changes that are related to each other. Pull requests should only contain a single set of changes that are related to each other.
That way, an approved set of changes will not be blocked by an unapproved set of changes. That way, an approved set of changes will not be blocked by an unapproved set of changes.
## Licensing
All contributions to a Tactility (sub)project will be licensed under the license(s) of that project.
When third party code is used, its license must be included.
It's important that these third-party licenses are compatible with the relevant Tactility (sub)projects.
## Code Style ## Code Style
See [this document](CODING_STYLE.md) and [.clang-format](.clang-format). See [this document](CODING_STYLE.md) and [.clang-format](.clang-format).

View File

Before

Width:  |  Height:  |  Size: 753 B

After

Width:  |  Height:  |  Size: 753 B

View File

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 528 B

View File

Before

Width:  |  Height:  |  Size: 142 B

After

Width:  |  Height:  |  Size: 142 B

View File

Before

Width:  |  Height:  |  Size: 144 B

After

Width:  |  Height:  |  Size: 144 B

View File

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

View File

Before

Width:  |  Height:  |  Size: 146 B

After

Width:  |  Height:  |  Size: 146 B

View File

Before

Width:  |  Height:  |  Size: 146 B

After

Width:  |  Height:  |  Size: 146 B

View File

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

View File

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

View File

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

View File

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

View File

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

View File

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

View File

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 193 B

View File

Before

Width:  |  Height:  |  Size: 196 B

After

Width:  |  Height:  |  Size: 196 B

View File

Before

Width:  |  Height:  |  Size: 394 B

After

Width:  |  Height:  |  Size: 394 B

View File

Before

Width:  |  Height:  |  Size: 407 B

After

Width:  |  Height:  |  Size: 407 B

View File

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 524 B

View File

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 517 B

View File

Before

Width:  |  Height:  |  Size: 534 B

After

Width:  |  Height:  |  Size: 534 B

View File

@ -93,6 +93,7 @@
- RSS reader - RSS reader
- Static file web server (with option to specify path and port) - Static file web server (with option to specify path and port)
- Diceware - Diceware
- Port TamaFi https://github.com/cifertech/TamaFi
# App Store # App Store

View File

@ -4,10 +4,7 @@ version=0.1
sdk=0.6.0-SNAPSHOT1 sdk=0.6.0-SNAPSHOT1
platforms=esp32,esp32s3 platforms=esp32,esp32s3
[app] [app]
id=com.bytewelder.calculator id=one.tactility.calculator
version=0.1.0 versionName=0.1.0
versionCode=1
name=Calculator name=Calculator
description=Math is cool
[author]
name=ByteWelder
website=https://bytewelder.com

View File

@ -14,7 +14,7 @@ import shutil
import configparser import configparser
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "2.1.1" ttbuild_version = "2.2.0"
ttbuild_cdn = "https://cdn.tactility.one" ttbuild_cdn = "https://cdn.tactility.one"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@ -242,19 +242,12 @@ def validate_manifest(manifest):
exit_with_error("Invalid manifest format: [app] not found") exit_with_error("Invalid manifest format: [app] not found")
if not "id" in manifest["app"]: if not "id" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] id not found") exit_with_error("Invalid manifest format: [app] id not found")
if not "version" in manifest["app"]: if not "versionName" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] version not found") exit_with_error("Invalid manifest format: [app] versionName not found")
if not "versionCode" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] versionCode not found")
if not "name" in manifest["app"]: if not "name" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] name not found") exit_with_error("Invalid manifest format: [app] name not found")
if not "description" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] description not found")
# [author]
if not "author" in manifest:
exit_with_error("Invalid manifest format: [author] not found")
if not "name" in manifest["author"]:
exit_with_error("Invalid manifest format: [author] name not found")
if not "website" in manifest["author"]:
exit_with_error("Invalid manifest format: [author] website not found")
def is_valid_manifest_platform(manifest, platform): def is_valid_manifest_platform(manifest, platform):
manifest_platforms = manifest["target"]["platforms"].split(",") manifest_platforms = manifest["target"]["platforms"].split(",")

View File

@ -4,10 +4,7 @@ version=0.1
sdk=0.6.0-SNAPSHOT1 sdk=0.6.0-SNAPSHOT1
platforms=esp32,esp32s3 platforms=esp32,esp32s3
[app] [app]
id=com.bytewelder.graphicsdemo id=one.tactility.graphicsdemo
version=0.1.0 versionName=0.1.0
versionCode=1
name=Graphics Demo name=Graphics Demo
description=A graphics and touch driver demonstration
[author]
name=ByteWelder
website=https://bytewelder.com

View File

@ -14,7 +14,7 @@ import shutil
import configparser import configparser
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "2.1.1" ttbuild_version = "2.2.0"
ttbuild_cdn = "https://cdn.tactility.one" ttbuild_cdn = "https://cdn.tactility.one"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@ -242,19 +242,12 @@ def validate_manifest(manifest):
exit_with_error("Invalid manifest format: [app] not found") exit_with_error("Invalid manifest format: [app] not found")
if not "id" in manifest["app"]: if not "id" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] id not found") exit_with_error("Invalid manifest format: [app] id not found")
if not "version" in manifest["app"]: if not "versionName" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] version not found") exit_with_error("Invalid manifest format: [app] versionName not found")
if not "versionCode" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] versionCode not found")
if not "name" in manifest["app"]: if not "name" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] name not found") exit_with_error("Invalid manifest format: [app] name not found")
if not "description" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] description not found")
# [author]
if not "author" in manifest:
exit_with_error("Invalid manifest format: [author] not found")
if not "name" in manifest["author"]:
exit_with_error("Invalid manifest format: [author] name not found")
if not "website" in manifest["author"]:
exit_with_error("Invalid manifest format: [author] website not found")
def is_valid_manifest_platform(manifest, platform): def is_valid_manifest_platform(manifest, platform):
manifest_platforms = manifest["target"]["platforms"].split(",") manifest_platforms = manifest["target"]["platforms"].split(",")

View File

@ -4,10 +4,7 @@ version=0.1
sdk=0.6.0-SNAPSHOT1 sdk=0.6.0-SNAPSHOT1
platforms=esp32,esp32s3 platforms=esp32,esp32s3
[app] [app]
id=com.bytewelder.helloworld id=one.tactility.helloworld
version=0.1.0 versionName=0.1.0
name=Hello World versionCode=1
description=A demonstration app that says hi name=Hello World
[author]
name=ByteWelder
website=https://bytewelder.com

View File

@ -14,7 +14,7 @@ import shutil
import configparser import configparser
ttbuild_path = ".tactility" ttbuild_path = ".tactility"
ttbuild_version = "2.1.1" ttbuild_version = "2.2.0"
ttbuild_cdn = "https://cdn.tactility.one" ttbuild_cdn = "https://cdn.tactility.one"
ttbuild_sdk_json_validity = 3600 # seconds ttbuild_sdk_json_validity = 3600 # seconds
ttport = 6666 ttport = 6666
@ -242,19 +242,12 @@ def validate_manifest(manifest):
exit_with_error("Invalid manifest format: [app] not found") exit_with_error("Invalid manifest format: [app] not found")
if not "id" in manifest["app"]: if not "id" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] id not found") exit_with_error("Invalid manifest format: [app] id not found")
if not "version" in manifest["app"]: if not "versionName" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] version not found") exit_with_error("Invalid manifest format: [app] versionName not found")
if not "versionCode" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] versionCode not found")
if not "name" in manifest["app"]: if not "name" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] name not found") exit_with_error("Invalid manifest format: [app] name not found")
if not "description" in manifest["app"]:
exit_with_error("Invalid manifest format: [app] description not found")
# [author]
if not "author" in manifest:
exit_with_error("Invalid manifest format: [author] not found")
if not "name" in manifest["author"]:
exit_with_error("Invalid manifest format: [author] name not found")
if not "website" in manifest["author"]:
exit_with_error("Invalid manifest format: [author] website not found")
def is_valid_manifest_platform(manifest, platform): def is_valid_manifest_platform(manifest, platform):
manifest_platforms = manifest["target"]["platforms"].split(",") manifest_platforms = manifest["target"]["platforms"].split(",")

View File

@ -0,0 +1,21 @@
#pragma once
#include <string>
namespace tt {
bool findFirstMountedSdCardPath(std::string& path);
std::string getSystemRootPath();
std::string getTempPath();
std::string getAppInstallPath();
std::string getAppInstallPath(const std::string& appId);
std::string getUserPath();
std::string getAppUserPath(const std::string& appId);
}

View File

@ -94,10 +94,6 @@ std::shared_ptr<AppContext> _Nullable getCurrentAppContext();
/** @return the currently running app (it is only ever null before the splash screen is shown) */ /** @return the currently running app (it is only ever null before the splash screen is shown) */
std::shared_ptr<App> _Nullable getCurrentApp(); std::shared_ptr<App> _Nullable getCurrentApp();
std::string getTempPath();
std::string getInstallPath();
bool install(const std::string& path); bool install(const std::string& path);
bool uninstall(const std::string& appId); bool uninstall(const std::string& appId);

View File

@ -7,7 +7,7 @@ namespace tt::app {
// Forward declarations // Forward declarations
class App; class App;
class Paths; class AppPaths;
struct AppManifest; struct AppManifest;
enum class Result; enum class Result;
@ -32,70 +32,10 @@ public:
virtual const AppManifest& getManifest() const = 0; virtual const AppManifest& getManifest() const = 0;
virtual std::shared_ptr<const Bundle> getParameters() const = 0; virtual std::shared_ptr<const Bundle> getParameters() const = 0;
virtual std::unique_ptr<Paths> getPaths() const = 0; virtual std::unique_ptr<AppPaths> getPaths() const = 0;
virtual std::shared_ptr<App> getApp() const = 0; virtual std::shared_ptr<App> getApp() const = 0;
}; };
class Paths {
public:
Paths() = default;
virtual ~Paths() = default;
/**
* Returns the directory path for the data location for an app.
* The data directory is intended to survive OS upgrades.
* The path will not end with a "/".
*/
virtual std::string getDataDirectory() const = 0;
/**
* @see getDataDirectory(), but with LVGL prefix.
*/
virtual std::string getDataDirectoryLvgl() const = 0;
/**
* Returns the full path for an entry inside the data location for an app.
* The data directory is intended to survive OS upgrades.
* Configuration data should be stored here.
* @param[in] childPath the path without a "/" prefix
*/
virtual std::string getDataPath(const std::string& childPath) const = 0;
/**
* @see getDataPath(), but with LVGL prefix.
*/
virtual std::string getDataPathLvgl(const std::string& childPath) const = 0;
/**
* Returns the directory path for the system location for an app.
* The system directory is not intended to survive OS upgrades.
* You should not store configuration data here.
* The path will not end with a "/".
* This is mainly used for core apps (system/boot/settings type).
*/
virtual std::string getSystemDirectory() const = 0;
/**
* @see getSystemDirectory(), but with LVGL prefix.
*/
virtual std::string getSystemDirectoryLvgl() const = 0;
/**
* Returns the full path for an entry inside the system location for an app.
* The data directory is not intended to survive OS upgrades.
* You should not store configuration data here.
* This is mainly used for core apps (system/boot/settings type).
* @param[in] childPath the path without a "/" prefix
*/
virtual std::string getSystemPath(const std::string& childPath) const = 0;
/**
* @see getSystemPath(), but with LVGL prefix.
*/
virtual std::string getSystemPathLvgl(const std::string& childPath) const = 0;
};
} }

View File

@ -63,30 +63,45 @@ struct AppManifest {
constexpr static uint32_t Hidden = 1 << 1; constexpr static uint32_t Hidden = 1 << 1;
}; };
/** The version of the manifest file format */
std::string manifestVersion = {};
/** The SDK version that was used to compile this app. (e.g. "0.6.0") */
std::string targetSdk = {};
/** Comma-separated list of platforms, e.g. "esp32,esp32s3" */
std::string targetPlatforms = {};
/** The identifier by which the app is launched by the system and other apps. */ /** The identifier by which the app is launched by the system and other apps. */
std::string id = {}; std::string appId = {};
/** The user-readable name of the app. Used in UI. */ /** The user-readable name of the app. Used in UI. */
std::string name = {}; std::string appName = {};
/** Optional icon. */ /** Optional icon. */
std::string icon = {}; std::string appIcon = {};
/** The version as it is displayed to the user (e.g. "1.2.0") */
std::string appVersionName = {};
/** The technical version (must be incremented with new releases of the app */
uint64_t appVersionCode = {};
/** App category helps with listing apps in Launcher, app list or settings apps. */ /** App category helps with listing apps in Launcher, app list or settings apps. */
Category category = Category::User; Category appCategory = Category::User;
/** Where the app is located */ /** Where the app is located */
Location location = Location::internal(); Location appLocation = Location::internal();
/** Controls various settings */ /** Controls various settings */
uint32_t flags = Flags::None; uint32_t appFlags = Flags::None;
/** Create the instance of the app */ /** Create the instance of the app */
CreateApp createApp = nullptr; CreateApp createApp = nullptr;
}; };
struct { struct {
bool operator()(const std::shared_ptr<AppManifest>& left, const std::shared_ptr<AppManifest>& right) const { return left->name < right->name; } bool operator()(const std::shared_ptr<AppManifest>& left, const std::shared_ptr<AppManifest>& right) const { return left->appName < right->appName; }
} SortAppManifestByName; } SortAppManifestByName;
} // namespace } // namespace

View File

@ -0,0 +1,47 @@
#pragma once
#include <string>
#include <memory>
namespace tt::app {
// Forward declarations
class AppManifest;
class AppPaths {
const AppManifest& manifest;
public:
explicit AppPaths(const AppManifest& manifest) : manifest(manifest) {}
/**
* The user data directory is intended to survive OS upgrades.
* The path will not end with a "/".
*/
std::string getUserDataPath() const;
/**
* The user data directory is intended to survive OS upgrades.
* Configuration data should be stored here.
* @param[in] childPath the path without a "/" prefix
*/
std::string getUserDataPath(const std::string& childPath) const;
/**
* You should not store configuration data here.
* The path will not end with a "/".
* This is mainly used for core apps (system/boot/settings type).
*/
std::string getAssetsDirectory() const;
/**
* You should not store configuration data here.
* This is mainly used for core apps (system/boot/settings type).
* @param[in] childPath the path without a "/" prefix
*/
std::string getAssetsPath(const std::string& childPath) const;
};
}

View File

@ -1,14 +1,11 @@
#pragma once #pragma once
#include "ServiceManifest.h"
#include <Tactility/Mutex.h>
#include <memory> #include <memory>
namespace tt::service { namespace tt::service {
class Paths; struct ServiceManifest;
class ServicePaths;
/** /**
* The public representation of a service instance. * The public representation of a service instance.
@ -26,68 +23,8 @@ public:
virtual const ServiceManifest& getManifest() const = 0; virtual const ServiceManifest& getManifest() const = 0;
/** Retrieve the paths that are relevant to this service */ /** Retrieve the paths that are relevant to this service */
virtual std::unique_ptr<Paths> getPaths() const = 0; virtual std::unique_ptr<ServicePaths> getPaths() const = 0;
}; };
class Paths {
public:
Paths() = default;
virtual ~Paths() = default;
/**
* Returns the directory path for the data location for a service.
* The data directory is intended to survive OS upgrades.
* The path will not end with a "/".
*/
virtual std::string getDataDirectory() const = 0;
/**
* @see getDataDirectory(), but with LVGL prefix.
*/
virtual std::string getDataDirectoryLvgl() const = 0;
/**
* Returns the full path for an entry inside the data location for a service.
* The data directory is intended to survive OS upgrades.
* Configuration data should be stored here.
* @param[in] childPath the path without a "/" prefix
*/
virtual std::string getDataPath(const std::string& childPath) const = 0;
/**
* @see getDataPath(), but with LVGL prefix.
*/
virtual std::string getDataPathLvgl(const std::string& childPath) const = 0;
/**
* Returns the directory path for the system location for a service.
* The system directory is not intended to survive OS upgrades.
* You should not store configuration data here.
* The path will not end with a "/".
* This is mainly used for core services.
*/
virtual std::string getSystemDirectory() const = 0;
/**
* @see getSystemDirectory(), but with LVGL prefix.
*/
virtual std::string getSystemDirectoryLvgl() const = 0;
/**
* Returns the full path for an entry inside the system location for an app.
* The data directory is not intended to survive OS upgrades.
* You should not store configuration data here.
* This is mainly used for core apps (system/boot/settings type).
* @param[in] childPath the path without a "/" prefix
*/
virtual std::string getSystemPath(const std::string& childPath) const = 0;
/**
* @see getSystemPath(), but with LVGL prefix.
*/
virtual std::string getSystemPathLvgl(const std::string& childPath) const = 0;
};
} // namespace } // namespace

View File

@ -1,6 +1,6 @@
#pragma once #pragma once
#include "Tactility/service/Service.h" #include <Tactility/service/Service.h>
#include <string> #include <string>

View File

@ -0,0 +1,45 @@
#pragma once
#include <string>
#include <memory>
namespace tt::service {
// Forward declarations
class ServiceManifest;
class ServicePaths {
std::shared_ptr<const ServiceManifest> manifest;
public:
explicit ServicePaths(std::shared_ptr<const ServiceManifest> manifest) : manifest(std::move(manifest)) {}
/**
* The user data directory is intended to survive OS upgrades.
* The path will not end with a "/".
*/
std::string getUserDataDirectory() const;
/**
* The user data directory is intended to survive OS upgrades.
* Configuration data should be stored here.
* @param[in] childPath the path without a "/" prefix
*/
std::string getUserDataPath(const std::string& childPath) const;
/**
* You should not store configuration data here.
* The path will not end with a "/".
*/
std::string getAssetsDirectory() const;
/**
* You should not store configuration data here.
* @param[in] childPath the path without a "/" prefix
*/
std::string getAssetsPath(const std::string& childPath) const;
};
}

View File

@ -24,7 +24,7 @@ class GpsService final : public Service {
Mutex stateMutex; Mutex stateMutex;
std::vector<GpsDeviceRecord> deviceRecords; std::vector<GpsDeviceRecord> deviceRecords;
std::shared_ptr<PubSub<State>> statePubSub = std::make_shared<PubSub<State>>(); std::shared_ptr<PubSub<State>> statePubSub = std::make_shared<PubSub<State>>();
std::unique_ptr<Paths> paths; std::unique_ptr<ServicePaths> paths;
State state = State::Off; State state = State::Off;
bool startGpsDevice(GpsDeviceRecord& deviceRecord); bool startGpsDevice(GpsDeviceRecord& deviceRecord);

View File

@ -41,10 +41,10 @@ class AppInstance : public AppContext {
static std::shared_ptr<App> createApp( static std::shared_ptr<App> createApp(
const std::shared_ptr<AppManifest>& manifest const std::shared_ptr<AppManifest>& manifest
) { ) {
if (manifest->location.isInternal()) { if (manifest->appLocation.isInternal()) {
assert(manifest->createApp != nullptr); assert(manifest->createApp != nullptr);
return manifest->createApp(); return manifest->createApp();
} else if (manifest->location.isExternal()) { } else if (manifest->appLocation.isExternal()) {
if (manifest->createApp != nullptr) { if (manifest->createApp != nullptr) {
TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps"); TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps");
} }
@ -88,7 +88,7 @@ public:
std::shared_ptr<const Bundle> getParameters() const override; std::shared_ptr<const Bundle> getParameters() const override;
std::unique_ptr<Paths> getPaths() const override; std::unique_ptr<AppPaths> getPaths() const override;
std::shared_ptr<App> getApp() const override { return app; } std::shared_ptr<App> getApp() const override { return app; }
}; };

View File

@ -1,26 +0,0 @@
#pragma once
#include "Tactility/app/AppInstance.h"
namespace tt::app {
class AppInstancePaths final : public Paths {
const AppManifest& manifest;
public:
explicit AppInstancePaths(const AppManifest& manifest) : manifest(manifest) {}
~AppInstancePaths() override = default;
std::string getDataDirectory() const override;
std::string getDataDirectoryLvgl() const override;
std::string getDataPath(const std::string& childPath) const override;
std::string getDataPathLvgl(const std::string& childPath) const override;
std::string getSystemDirectory() const override;
std::string getSystemDirectoryLvgl() const override;
std::string getSystemPath(const std::string& childPath) const override;
std::string getSystemPathLvgl(const std::string& childPath) const override;
};
}

View File

@ -0,0 +1,14 @@
#pragma once
#include <Tactility/app/AppManifest.h>
#include <map>
#include <string>
namespace tt::app {
bool isValidId(const std::string& id);
bool parseManifest(const std::map<std::string, std::string>& map, AppManifest& manifest);
}

View File

@ -4,6 +4,7 @@
#include "./State.h" #include "./State.h"
#include <Tactility/app/AppContext.h> #include <Tactility/app/AppContext.h>
#include <Tactility/app/AppPaths.h>
#include <lvgl.h> #include <lvgl.h>
@ -13,7 +14,7 @@ class View final {
Bindings* bindings; Bindings* bindings;
State* state; State* state;
std::unique_ptr<Paths> paths; std::unique_ptr<AppPaths> paths;
lv_obj_t* root = nullptr; lv_obj_t* root = nullptr;
lv_obj_t* enable_switch = nullptr; lv_obj_t* enable_switch = nullptr;
lv_obj_t* enable_on_boot_switch = nullptr; lv_obj_t* enable_on_boot_switch = nullptr;

View File

@ -1,7 +1,10 @@
#pragma once #pragma once
#include "Tactility/service/ServiceContext.h" #include <Tactility/service/ServiceContext.h>
#include "Tactility/service/Service.h" #include <Tactility/service/Service.h>
#include <Tactility/Mutex.h>
#include <memory>
namespace tt::service { namespace tt::service {
@ -21,7 +24,7 @@ public:
const ServiceManifest& getManifest() const override; const ServiceManifest& getManifest() const override;
/** Retrieve the paths that are relevant to this service */ /** Retrieve the paths that are relevant to this service */
std::unique_ptr<Paths> getPaths() const override; std::unique_ptr<ServicePaths> getPaths() const override;
std::shared_ptr<Service> getService() const { return service; } std::shared_ptr<Service> getService() const { return service; }

View File

@ -1,28 +0,0 @@
#pragma once
#include "Tactility/service/ServiceInstance.h"
namespace tt::service {
class ServiceInstancePaths final : public Paths {
private:
std::shared_ptr<const ServiceManifest> manifest;
public:
explicit ServiceInstancePaths(std::shared_ptr<const ServiceManifest> manifest) : manifest(std::move(manifest)) {}
~ServiceInstancePaths() final = default;
std::string getDataDirectory() const final;
std::string getDataDirectoryLvgl() const final;
std::string getDataPath(const std::string& childPath) const final;
std::string getDataPathLvgl(const std::string& childPath) const final;
std::string getSystemDirectory() const final;
std::string getSystemDirectoryLvgl() const final;
std::string getSystemPath(const std::string& childPath) const final;
std::string getSystemPathLvgl(const std::string& childPath) const final;
};
}

View File

@ -0,0 +1,56 @@
#include <Tactility/Paths.h>
#include <Tactility/app/AppManifestParsing.h>
#include <Tactility/MountPoints.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <format>
namespace tt {
bool findFirstMountedSdCardPath(std::string& path) {
// const auto sdcards = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
bool is_set = false;
hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard, [&is_set, &path](const auto& device) {
if (device->isMounted()) {
path = device->getMountPath();
is_set = true;
return false; // stop iterating
} else {
return true;
}
});
return is_set;
}
std::string getSystemRootPath() {
std::string root_path;
if (!findFirstMountedSdCardPath(root_path)) {
root_path = file::MOUNT_POINT_DATA;
}
return root_path;
}
std::string getTempPath() {
return getSystemRootPath() + "/tmp";
}
std::string getAppInstallPath() {
return getSystemRootPath() + "/app";
}
std::string getUserPath() {
return getSystemRootPath() + "/user";
}
std::string getAppInstallPath(const std::string& appId) {
assert(app::isValidId(appId));
return std::format("{}/{}", getAppInstallPath(), appId);
}
std::string getAppUserPath(const std::string& appId) {
assert(app::isValidId(appId));
return std::format("{}/app/{}", getUserPath(), appId);
}
}

View File

@ -1,3 +1,5 @@
#include "Tactility/app/AppManifestParsing.h"
#include <Tactility/Tactility.h> #include <Tactility/Tactility.h>
#include <Tactility/TactilityConfig.h> #include <Tactility/TactilityConfig.h>
#include <Tactility/app/AppRegistration.h> #include <Tactility/app/AppRegistration.h>
@ -158,29 +160,22 @@ static void registerInstalledApp(std::string path) {
return; return;
} }
std::map<std::string, std::string> manifest; std::map<std::string, std::string> properties;
if (!file::loadPropertiesFile(manifest_path, manifest)) { if (!file::loadPropertiesFile(manifest_path, properties)) {
TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str()); TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str());
}
auto app_id_entry = manifest.find("[app]id");
if (app_id_entry == manifest.end()) {
TT_LOG_E(TAG, "Failed to find app id in manifest");
return; return;
} }
auto app_name_entry = manifest.find("[app]name"); app::AppManifest manifest;
if (app_name_entry == manifest.end()) { if (!app::parseManifest(properties, manifest)) {
TT_LOG_E(TAG, "Failed to find app name in manifest"); TT_LOG_E(TAG, "Failed to parse manifest at %s", manifest_path.c_str());
return; return;
} }
app::addApp({ manifest.appCategory = app::Category::User;
.id = app_id_entry->second, manifest.appLocation = app::Location::external(path);
.name = app_name_entry->second,
.category = app::Category::User, app::addApp(manifest);
.location = app::Location::external(path)
});
} }
static void registerInstalledApps(const std::string& path) { static void registerInstalledApps(const std::string& path) {
@ -194,7 +189,7 @@ static void registerInstalledApps(const std::string& path) {
static void registerInstalledAppsFromSdCard(const std::shared_ptr<hal::sdcard::SdCardDevice>& sdcard) { static void registerInstalledAppsFromSdCard(const std::shared_ptr<hal::sdcard::SdCardDevice>& sdcard) {
auto sdcard_root_path = sdcard->getMountPath(); auto sdcard_root_path = sdcard->getMountPath();
auto app_path = std::format("{}/apps", sdcard_root_path); auto app_path = std::format("{}/app", sdcard_root_path);
sdcard->getLock()->lock(); sdcard->getLock()->lock();
if (file::isDirectory(app_path)) { if (file::isDirectory(app_path)) {
registerInstalledApps(app_path); registerInstalledApps(app_path);
@ -289,7 +284,7 @@ void run(const Configuration& config) {
TT_LOG_I(TAG, "Starting boot app"); TT_LOG_I(TAG, "Starting boot app");
// The boot app takes care of registering system apps, user services and user apps // The boot app takes care of registering system apps, user services and user apps
addApp(app::boot::manifest); addApp(app::boot::manifest);
service::loader::startApp(app::boot::manifest.id); service::loader::startApp(app::boot::manifest.appId);
TT_LOG_I(TAG, "Main dispatcher ready"); TT_LOG_I(TAG, "Main dispatcher ready");
while (true) { while (true) {

View File

@ -1,6 +1,8 @@
#include <Tactility/app/App.h> #include "Tactility/Paths.h"
#include <Tactility/app/App.h>
#include <Tactility/app/AppManifestParsing.h>
#include <Tactility/MountPoints.h>
#include <Tactility/app/AppManifest.h> #include <Tactility/app/AppManifest.h>
#include <Tactility/app/AppRegistration.h> #include <Tactility/app/AppRegistration.h>
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
@ -103,130 +105,105 @@ static bool untar(const std::string& tarPath, const std::string& destinationPath
return success; return success;
} }
bool findFirstMountedSdCardPath(std::string& path) { void cleanupInstallDirectory(const std::string& path) {
// const auto sdcards = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard); const auto lock = file::getLock(path);
bool is_set = false; lock->lock();
hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard, [&is_set, &path](const auto& device) { if (!file::deleteRecursively(path)) {
if (device->isMounted()) { TT_LOG_W(TAG, "Failed to delete existing installation at %s", path.c_str());
path = device->getMountPath();
is_set = true;
return false; // stop iterating
} else {
return true;
}
});
return is_set;
}
std::string getTempPath() {
std::string root_path;
if (!findFirstMountedSdCardPath(root_path)) {
root_path = file::MOUNT_POINT_DATA;
} }
return root_path + "/tmp"; lock->unlock();
}
std::string getInstallPath() {
std::string root_path;
if (!findFirstMountedSdCardPath(root_path)) {
root_path = file::MOUNT_POINT_DATA;
}
return root_path + "/apps";
} }
bool install(const std::string& path) { bool install(const std::string& path) {
// TODO: Make better: lock for each path type properly (source vs target)
// We lock and unlock frequently because SPI SD card devices share // We lock and unlock frequently because SPI SD card devices share
// the lock with the display. We don't want to lock the display for very long. // the lock with the display. We don't want to lock the display for very long.
auto app_parent_path = getInstallPath(); auto app_parent_path = getAppInstallPath();
TT_LOG_I(TAG, "Installing app %s to %s", path.c_str(), app_parent_path.c_str()); TT_LOG_I(TAG, "Installing app %s to %s", path.c_str(), app_parent_path.c_str());
auto lock = file::getLock(app_parent_path)->asScopedLock(); auto target_path_lock = file::getLock(app_parent_path)->asScopedLock();
lock.lock(); target_path_lock.lock();
auto filename = file::getLastPathSegment(path); auto filename = file::getLastPathSegment(path);
const std::string app_target_path = std::format("{}/{}", app_parent_path, filename); const std::string app_target_path = std::format("{}/{}", app_parent_path, filename);
if (file::isDirectory(app_target_path) && !file::deleteRecursively(app_target_path)) { if (file::isDirectory(app_target_path) && !file::deleteRecursively(app_target_path)) {
TT_LOG_W(TAG, "Failed to delete %s", app_target_path.c_str()); TT_LOG_W(TAG, "Failed to delete %s", app_target_path.c_str());
} }
lock.unlock(); target_path_lock.unlock();
lock.lock(); target_path_lock.lock();
if (!file::findOrCreateDirectory(app_target_path, 0777)) { if (!file::findOrCreateDirectory(app_target_path, 0777)) {
TT_LOG_I(TAG, "Failed to create directory %s", app_target_path.c_str()); TT_LOG_I(TAG, "Failed to create directory %s", app_target_path.c_str());
return false; return false;
} }
lock.unlock(); target_path_lock.unlock();
lock.lock(); auto source_path_lock = file::getLock(path)->asScopedLock();
target_path_lock.lock();
source_path_lock.lock();
TT_LOG_I(TAG, "Extracting app from %s to %s", path.c_str(), app_target_path.c_str()); TT_LOG_I(TAG, "Extracting app from %s to %s", path.c_str(), app_target_path.c_str());
if (!untar(path, app_target_path)) { if (!untar(path, app_target_path)) {
TT_LOG_E(TAG, "Failed to extract"); TT_LOG_E(TAG, "Failed to extract");
return false; return false;
} }
lock.unlock(); source_path_lock.unlock();
target_path_lock.unlock();
lock.lock(); target_path_lock.lock();
auto manifest_path = app_target_path + "/manifest.properties"; auto manifest_path = app_target_path + "/manifest.properties";
if (!file::isFile(manifest_path)) { if (!file::isFile(manifest_path)) {
TT_LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str()); TT_LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str());
cleanupInstallDirectory(app_target_path);
return false; return false;
} }
lock.unlock(); target_path_lock.unlock();
lock.lock(); target_path_lock.lock();
std::map<std::string, std::string> properties; std::map<std::string, std::string> properties;
if (!file::loadPropertiesFile(manifest_path, properties)) { if (!file::loadPropertiesFile(manifest_path, properties)) {
TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str()); TT_LOG_E(TAG, "Failed to load manifest at %s", manifest_path.c_str());
cleanupInstallDirectory(app_target_path);
return false; return false;
} }
lock.unlock(); target_path_lock.unlock();
auto app_id_iterator = properties.find("[app]id"); AppManifest manifest;
if (app_id_iterator == properties.end()) { if (!parseManifest(properties, manifest)) {
TT_LOG_E(TAG, "Failed to find app id in manifest"); TT_LOG_W(TAG, "Invalid manifest");
cleanupInstallDirectory(app_target_path);
return false; return false;
} }
auto app_name_entry = properties.find("[app]name"); target_path_lock.lock();
if (app_name_entry == properties.end()) { const std::string renamed_target_path = std::format("{}/{}", app_parent_path, manifest.appId);
TT_LOG_E(TAG, "Failed to find app name in manifest");
return false;
}
lock.lock();
const std::string renamed_target_path = std::format("{}/{}", app_parent_path, app_id_iterator->second);
if (file::isDirectory(renamed_target_path)) { if (file::isDirectory(renamed_target_path)) {
if (!file::deleteRecursively(renamed_target_path)) { if (!file::deleteRecursively(renamed_target_path)) {
TT_LOG_W(TAG, "Failed to delete existing installation at %s", renamed_target_path.c_str()); TT_LOG_W(TAG, "Failed to delete existing installation at %s", renamed_target_path.c_str());
cleanupInstallDirectory(app_target_path);
return false; return false;
} }
} }
lock.unlock(); target_path_lock.unlock();
lock.lock(); target_path_lock.lock();
if (rename(app_target_path.c_str(), renamed_target_path.c_str()) != 0) { if (rename(app_target_path.c_str(), renamed_target_path.c_str()) != 0) {
TT_LOG_E(TAG, "Failed to rename %s to %s", app_target_path.c_str(), app_id_iterator->second.c_str()); TT_LOG_E(TAG, "Failed to rename \"%s\" to \"%s\"", app_target_path.c_str(), manifest.appId.c_str());
cleanupInstallDirectory(app_target_path);
return false; return false;
} }
lock.unlock(); target_path_lock.unlock();
addApp({ manifest.appLocation = Location::external(renamed_target_path);
.id = app_id_iterator->second,
.name = app_name_entry->second, addApp(manifest);
.category = Category::User,
.location = Location::external(renamed_target_path)
});
return true; return true;
} }
bool uninstall(const std::string& appId) { bool uninstall(const std::string& appId) {
TT_LOG_I(TAG, "Uninstalling app %s", appId.c_str()); TT_LOG_I(TAG, "Uninstalling app %s", appId.c_str());
auto app_path = getInstallPath() + "/" + appId; auto app_path = getAppInstallPath(appId);
return file::withLock<bool>(app_path, [&app_path, &appId]() { return file::withLock<bool>(app_path, [&app_path, &appId] {
if (!file::isDirectory(app_path)) { if (!file::isDirectory(app_path)) {
TT_LOG_E(TAG, "App %s not found at ", app_path.c_str()); TT_LOG_E(TAG, "App %s not found at ", app_path.c_str());
return false; return false;

View File

@ -1,5 +1,5 @@
#include "Tactility/app/AppInstance.h" #include <Tactility/app/AppInstance.h>
#include "Tactility/app/AppInstancePaths.h" #include <Tactility/app/AppPaths.h>
namespace tt::app { namespace tt::app {
@ -49,9 +49,9 @@ std::shared_ptr<const Bundle> AppInstance::getParameters() const {
return result; return result;
} }
std::unique_ptr<Paths> AppInstance::getPaths() const { std::unique_ptr<AppPaths> AppInstance::getPaths() const {
assert(manifest != nullptr); assert(manifest != nullptr);
return std::make_unique<AppInstancePaths>(*manifest); return std::make_unique<AppPaths>(*manifest);
} }
} // namespace } // namespace

View File

@ -1,49 +0,0 @@
#include "Tactility/app/AppInstancePaths.h"
#include <Tactility/MountPoints.h>
#define LVGL_PATH_PREFIX std::string("A:/")
#ifdef ESP_PLATFORM
#define PARTITION_PREFIX std::string("/")
#else
#define PARTITION_PREFIX std::string("")
#endif
namespace tt::app {
std::string AppInstancePaths::getDataDirectory() const {
return PARTITION_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id;
}
std::string AppInstancePaths::getDataDirectoryLvgl() const {
return LVGL_PATH_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id;
}
std::string AppInstancePaths::getDataPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return PARTITION_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
}
std::string AppInstancePaths::getDataPathLvgl(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return LVGL_PATH_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
}
std::string AppInstancePaths::getSystemDirectory() const {
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id;
}
std::string AppInstancePaths::getSystemDirectoryLvgl() const {
return LVGL_PATH_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id;
}
std::string AppInstancePaths::getSystemPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
}
std::string AppInstancePaths::getSystemPathLvgl(const std::string& childPath) const {
return LVGL_PATH_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
}
}

View File

@ -0,0 +1,131 @@
#include <Tactility/app/AppManifestParsing.h>
#include <regex>
namespace tt::app {
constexpr auto* TAG = "App";
static bool validateString(const std::string& value, const std::function<bool(const char)>& isValidChar) {
for (const auto& c : value) {
if (!isValidChar(c)) {
return false;
}
}
return true;
}
static bool getValueFromManifest(const std::map<std::string, std::string>& map, const std::string& key, std::string& output) {
const auto iterator = map.find(key);
if (iterator == map.end()) {
TT_LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
return false;
}
output = iterator->second;
return true;
}
bool isValidId(const std::string& id) {
return id.size() >= 5 && validateString(id, [](const char c) {
return std::isalnum(c) != 0 || c == '.';
});
}
static bool isValidManifestVersion(const std::string& version) {
return version.size() > 0 && validateString(version, [](const char c) {
return std::isalnum(c) != 0 || c == '.';
});
}
static bool isValidAppVersionName(const std::string& version) {
return version.size() > 0 && validateString(version, [](const char c) {
return std::isalnum(c) != 0 || c == '.' || c == '-' || c == '_';
});
}
static bool isValidAppVersionCode(const std::string& version) {
return version.size() > 0 && validateString(version, [](const char c) {
return std::isdigit(c) != 0;
});
}
static bool isValidName(const std::string& name) {
return name.size() >= 2 && validateString(name, [](const char c) {
return std::isalnum(c) != 0 || c == ' ' || c == '-';
});
}
bool parseManifest(const std::map<std::string, std::string>& map, AppManifest& manifest) {
TT_LOG_I(TAG, "Parsing manifest");
// [manifest]
if (!getValueFromManifest(map, "[manifest]version", manifest.manifestVersion)) {
return false;
}
if (!isValidManifestVersion(manifest.manifestVersion)) {
TT_LOG_E(TAG, "Invalid version");
return false;
}
// [app]
if (!getValueFromManifest(map, "[app]id", manifest.appId)) {
return false;
}
if (!isValidId(manifest.appId)) {
TT_LOG_E(TAG, "Invalid app id");
return false;
}
if (!getValueFromManifest(map, "[app]name", manifest.appName)) {
return false;
}
if (!isValidName(manifest.appName)) {
TT_LOG_I(TAG, "Invalid app name");
return false;
}
if (!getValueFromManifest(map, "[app]versionName", manifest.appVersionName)) {
return false;
}
if (!isValidAppVersionName(manifest.appVersionName)) {
TT_LOG_E(TAG, "Invalid app version name");
return false;
}
std::string version_code_string;
if (!getValueFromManifest(map, "[app]versionCode", version_code_string)) {
return false;
}
if (!isValidAppVersionCode(version_code_string)) {
TT_LOG_E(TAG, "Invalid app version code");
return false;
}
manifest.appVersionCode = std::stoull(version_code_string);
// [target]
if (!getValueFromManifest(map, "[target]sdk", manifest.targetSdk)) {
return false;
}
if (!getValueFromManifest(map, "[target]platforms", manifest.targetPlatforms)) {
return false;
}
// Defaults
manifest.appCategory = Category::User;
manifest.appLocation = Location::external("");
return true;
}
}

View File

@ -0,0 +1,44 @@
#include <Tactility/app/AppPaths.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/MountPoints.h>
#include <Tactility/file/File.h>
#include <format>
#ifdef ESP_PLATFORM
constexpr auto PARTITION_PREFIX = std::string("/");
#else
constexpr auto PARTITION_PREFIX = std::string("");
#endif
namespace tt::app {
std::string AppPaths::getUserDataPath() const {
if (manifest.appLocation.isInternal()) {
return std::format("{}{}/user/app/{}", PARTITION_PREFIX, file::DATA_PARTITION_NAME, manifest.appId);
} else {
return std::format("{}/user/app/{}", file::getFirstPathSegment(manifest.appLocation.getPath()), manifest.appId);
}
}
std::string AppPaths::getUserDataPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return std::format("{}/{}", getUserDataPath(), childPath);
}
std::string AppPaths::getAssetsDirectory() const {
if (manifest.appLocation.isInternal()) {
return std::format("{}{}/app/{}/assets", PARTITION_PREFIX, file::SYSTEM_PARTITION_NAME, manifest.appId);
} else {
return std::format("{}/assets", manifest.appLocation.getPath());
}
}
std::string AppPaths::getAssetsPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return std::format("{}/{}", getAssetsDirectory(), childPath);
}
}

View File

@ -16,15 +16,15 @@ static AppManifestMap app_manifest_map;
static Mutex hash_mutex(Mutex::Type::Normal); static Mutex hash_mutex(Mutex::Type::Normal);
void addApp(const AppManifest& manifest) { void addApp(const AppManifest& manifest) {
TT_LOG_I(TAG, "Registering manifest %s", manifest.id.c_str()); TT_LOG_I(TAG, "Registering manifest %s", manifest.appId.c_str());
hash_mutex.lock(); hash_mutex.lock();
if (app_manifest_map.contains(manifest.id)) { if (app_manifest_map.contains(manifest.appId)) {
TT_LOG_W(TAG, "Overwriting existing manifest for %s", manifest.id.c_str()); TT_LOG_W(TAG, "Overwriting existing manifest for %s", manifest.appId.c_str());
} }
app_manifest_map[manifest.id] = std::make_shared<AppManifest>(manifest); app_manifest_map[manifest.appId] = std::make_shared<AppManifest>(manifest);
hash_mutex.unlock(); hash_mutex.unlock();
} }

View File

@ -223,8 +223,8 @@ void setElfAppParameters(
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest) { std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest) {
TT_LOG_I(TAG, "createElfApp"); TT_LOG_I(TAG, "createElfApp");
assert(manifest != nullptr); assert(manifest != nullptr);
assert(manifest->location.isExternal()); assert(manifest->appLocation.isExternal());
return std::make_shared<ElfApp>(manifest->location.getPath()); return std::make_shared<ElfApp>(manifest->appLocation.getPath());
} }
} // namespace } // namespace

View File

@ -172,16 +172,16 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "AddGps", .appId = "AddGps",
.name = "Add GPS", .appName = "Add GPS",
.icon = LV_SYMBOL_GPS, .appIcon = LV_SYMBOL_GPS,
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<AddGpsApp> .createApp = create<AddGpsApp>
}; };
void start() { void start() {
app::start(manifest.id); app::start(manifest.appId);
} }
} // namespace } // namespace

View File

@ -28,7 +28,7 @@ LaunchId start(const std::string& title, const std::string& message, const std::
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title); bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message); bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_joined); bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, items_joined);
return service::loader::startApp(manifest.id, bundle); return service::loader::startApp(manifest.appId, bundle);
} }
LaunchId start(const std::string& title, const std::string& message) { LaunchId start(const std::string& title, const std::string& message) {
@ -36,7 +36,7 @@ LaunchId start(const std::string& title, const std::string& message) {
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title); bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message); bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, "OK"); bundle->putString(PARAMETER_BUNDLE_KEY_BUTTON_LABELS, "OK");
return service::loader::startApp(manifest.id, bundle); return service::loader::startApp(manifest.appId, bundle);
} }
int32_t getResultIndex(const Bundle& bundle) { int32_t getResultIndex(const Bundle& bundle) {
@ -126,10 +126,10 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "AlertDialog", .appId = "AlertDialog",
.name = "Alert Dialog", .appName = "Alert Dialog",
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<AlertDialogApp> .createApp = create<AlertDialogApp>
}; };

View File

@ -13,12 +13,12 @@ class AppListApp : public App {
static void onAppPressed(lv_event_t* e) { static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e)); const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::startApp(manifest->id); service::loader::startApp(manifest->appId);
} }
static void createAppWidget(const std::shared_ptr<AppManifest>& manifest, lv_obj_t* list) { static void createAppWidget(const std::shared_ptr<AppManifest>& manifest, lv_obj_t* list) {
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; const void* icon = !manifest->appIcon.empty() ? manifest->appIcon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str()); lv_obj_t* btn = lv_list_add_button(list, icon, manifest->appName.c_str());
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, manifest.get()); lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, manifest.get());
} }
@ -40,8 +40,8 @@ public:
std::ranges::sort(manifests, SortAppManifestByName); std::ranges::sort(manifests, SortAppManifestByName);
for (const auto& manifest: manifests) { for (const auto& manifest: manifests) {
bool is_valid_category = (manifest->category == Category::User) || (manifest->category == Category::System); bool is_valid_category = (manifest->appCategory == Category::User) || (manifest->appCategory == Category::System);
bool is_visible = (manifest->flags & AppManifest::Flags::Hidden) == 0u; bool is_visible = (manifest->appFlags & AppManifest::Flags::Hidden) == 0u;
if (is_valid_category && is_visible) { if (is_valid_category && is_visible) {
createAppWidget(manifest, list); createAppWidget(manifest, list);
} }
@ -50,10 +50,10 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "AppList", .appId = "AppList",
.name = "Apps", .appName = "Apps",
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<AppListApp>, .createApp = create<AppListApp>,
}; };

View File

@ -1,6 +1,7 @@
#include <Tactility/TactilityCore.h> #include <Tactility/TactilityCore.h>
#include <Tactility/TactilityPrivate.h> #include <Tactility/TactilityPrivate.h>
#include <Tactility/app/AppContext.h> #include <Tactility/app/AppContext.h>
#include <Tactility/app/AppPaths.h>
#include <Tactility/CpuAffinity.h> #include <Tactility/CpuAffinity.h>
#include <Tactility/hal/display/DisplayDevice.h> #include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/usb/Usb.h> #include <Tactility/hal/usb/Usb.h>
@ -163,21 +164,21 @@ public:
const char* logo; const char* logo;
// TODO: Replace with automatic asset buckets like on Android // TODO: Replace with automatic asset buckets like on Android
if (getSmallestDimension() < 150) { // e.g. Cardputer if (getSmallestDimension() < 150) { // e.g. Cardputer
logo = hal::usb::isUsbBootMode() ? "assets/logo_usb.png" : "assets/logo_small.png"; logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo_small.png";
} else { } else {
logo = hal::usb::isUsbBootMode() ? "assets/logo_usb.png" : "assets/logo.png"; logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo.png";
} }
const auto logo_path = paths->getSystemPathLvgl(logo); const auto logo_path = "A:" + paths->getAssetsPath(logo);
TT_LOG_I(TAG, "%s", logo_path.c_str()); TT_LOG_I(TAG, "%s", logo_path.c_str());
lv_image_set_src(image, logo_path.c_str()); lv_image_set_src(image, logo_path.c_str());
} }
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Boot", .appId = "Boot",
.name = "Boot", .appName = "Boot",
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::HideStatusBar | AppManifest::Flags::Hidden,
.createApp = create<BootApp> .createApp = create<BootApp>
}; };

View File

@ -224,9 +224,9 @@ class CalculatorApp : public App {
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Calculator", .appId = "Calculator",
.name = "Calculator", .appName = "Calculator",
.icon = TT_ASSETS_APP_ICON_CALCULATOR, .appIcon = TT_ASSETS_APP_ICON_CALCULATOR,
.createApp = create<CalculatorApp> .createApp = create<CalculatorApp>
}; };

View File

@ -136,9 +136,9 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Chat", .appId = "Chat",
.name = "Chat", .appName = "Chat",
.icon = TT_ASSETS_APP_ICON_CHAT, .appIcon = TT_ASSETS_APP_ICON_CHAT,
.createApp = create<ChatApp> .createApp = create<ChatApp>
}; };

View File

@ -122,15 +122,15 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "CrashDiagnostics", .appId = "CrashDiagnostics",
.name = "Crash Diagnostics", .appName = "Crash Diagnostics",
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<CrashDiagnosticsApp> .createApp = create<CrashDiagnosticsApp>
}; };
void start() { void start() {
service::loader::startApp(manifest.id); service::loader::startApp(manifest.appId);
} }
} // namespace } // namespace

View File

@ -160,14 +160,14 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Development", .appId = "Development",
.name = "Development", .appName = "Development",
.category = Category::Settings, .appCategory = Category::Settings,
.createApp = create<DevelopmentApp> .createApp = create<DevelopmentApp>
}; };
void start() { void start() {
app::start(manifest.id); app::start(manifest.appId);
} }
} // namespace } // namespace

View File

@ -164,10 +164,10 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Display", .appId = "Display",
.name = "Display", .appName = "Display",
.icon = TT_ASSETS_APP_ICON_DISPLAY_SETTINGS, .appIcon = TT_ASSETS_APP_ICON_DISPLAY_SETTINGS,
.category = Category::Settings, .appCategory = Category::Settings,
.createApp = create<DisplayApp> .createApp = create<DisplayApp>
}; };

View File

@ -33,16 +33,16 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Files", .appId = "Files",
.name = "Files", .appName = "Files",
.icon = TT_ASSETS_APP_ICON_FILES, .appIcon = TT_ASSETS_APP_ICON_FILES,
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<FilesApp> .createApp = create<FilesApp>
}; };
void start() { void start() {
service::loader::startApp(manifest.id); service::loader::startApp(manifest.appId);
} }
} // namespace } // namespace

View File

@ -56,24 +56,24 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "FileSelection", .appId = "FileSelection",
.name = "File Selection", .appName = "File Selection",
.icon = TT_ASSETS_APP_ICON_FILES, .appIcon = TT_ASSETS_APP_ICON_FILES,
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<FileSelection> .createApp = create<FileSelection>
}; };
LaunchId startForExistingFile() { LaunchId startForExistingFile() {
auto bundle = std::make_shared<Bundle>(); auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::Existing); setMode(*bundle, Mode::Existing);
return service::loader::startApp(manifest.id, bundle); return service::loader::startApp(manifest.appId, bundle);
} }
LaunchId startForExistingOrNewFile() { LaunchId startForExistingOrNewFile() {
auto bundle = std::make_shared<Bundle>(); auto bundle = std::make_shared<Bundle>();
setMode(*bundle, Mode::ExistingOrNew); setMode(*bundle, Mode::ExistingOrNew);
return service::loader::startApp(manifest.id, bundle); return service::loader::startApp(manifest.appId, bundle);
} }
} // namespace } // namespace

View File

@ -188,10 +188,10 @@ void GpioApp::onHide(AppContext& app) {
} }
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Gpio", .appId = "Gpio",
.name = "GPIO", .appName = "GPIO",
.icon = TT_ASSETS_APP_ICON_GPIO, .appIcon = TT_ASSETS_APP_ICON_GPIO,
.category = Category::System, .appCategory = Category::System,
.createApp = create<GpioApp> .createApp = create<GpioApp>
}; };

View File

@ -58,7 +58,7 @@ class GpsSettingsApp final : public App {
} }
void onAddGps() { void onAddGps() {
app::start(addgps::manifest.id); app::start(addgps::manifest.appId);
} }
void startReceivingUpdates() { void startReceivingUpdates() {
@ -344,15 +344,15 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "GpsSettings", .appId = "GpsSettings",
.name = "GPS", .appName = "GPS",
.icon = LV_SYMBOL_GPS, .appIcon = LV_SYMBOL_GPS,
.category = Category::Settings, .appCategory = Category::Settings,
.createApp = create<GpsSettingsApp> .createApp = create<GpsSettingsApp>
}; };
void start() { void start() {
app::start(manifest.id); app::start(manifest.appId);
} }
} // namespace } // namespace

View File

@ -69,7 +69,7 @@ public:
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
std::shared_ptr<I2cScannerApp> _Nullable optApp() { std::shared_ptr<I2cScannerApp> _Nullable optApp() {
auto appContext = getCurrentAppContext(); auto appContext = getCurrentAppContext();
if (appContext != nullptr && appContext->getManifest().id == manifest.id) { if (appContext != nullptr && appContext->getManifest().appId == manifest.appId) {
return std::static_pointer_cast<I2cScannerApp>(appContext->getApp()); return std::static_pointer_cast<I2cScannerApp>(appContext->getApp());
} else { } else {
return nullptr; return nullptr;
@ -403,15 +403,15 @@ void I2cScannerApp::onScanTimerFinished() {
} }
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "I2cScanner", .appId = "I2cScanner",
.name = "I2C Scanner", .appName = "I2C Scanner",
.icon = TT_ASSETS_APP_ICON_I2C_SETTINGS, .appIcon = TT_ASSETS_APP_ICON_I2C_SETTINGS,
.category = Category::System, .appCategory = Category::System,
.createApp = create<I2cScannerApp> .createApp = create<I2cScannerApp>
}; };
void start() { void start() {
service::loader::startApp(manifest.id); service::loader::startApp(manifest.appId);
} }
} // namespace } // namespace

View File

@ -93,10 +93,10 @@ class I2cSettingsApp : public App {
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "I2cSettings", .appId = "I2cSettings",
.name = "I2C", .appName = "I2C",
.icon = TT_ASSETS_APP_ICON_I2C_SETTINGS, .appIcon = TT_ASSETS_APP_ICON_I2C_SETTINGS,
.category = Category::Settings, .appCategory = Category::Settings,
.createApp = create<I2cSettingsApp> .createApp = create<I2cSettingsApp>
}; };

View File

@ -60,17 +60,17 @@ class ImageViewerApp : public App {
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "ImageViewer", .appId = "ImageViewer",
.name = "Image Viewer", .appName = "Image Viewer",
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<ImageViewerApp> .createApp = create<ImageViewerApp>
}; };
void start(const std::string& file) { void start(const std::string& file) {
auto parameters = std::make_shared<Bundle>(); auto parameters = std::make_shared<Bundle>();
parameters->putString(IMAGE_VIEWER_FILE_ARGUMENT, file); parameters->putString(IMAGE_VIEWER_FILE_ARGUMENT, file);
service::loader::startApp(manifest.id, parameters); service::loader::startApp(manifest.appId, parameters);
} }
} // namespace } // namespace

View File

@ -26,7 +26,7 @@ void start(const std::string& title, const std::string& message, const std::stri
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title); bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message); bundle->putString(PARAMETER_BUNDLE_KEY_MESSAGE, message);
bundle->putString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled); bundle->putString(PARAMETER_BUNDLE_KEY_PREFILLED, prefilled);
service::loader::startApp(manifest.id, bundle); service::loader::startApp(manifest.appId, bundle);
} }
std::string getResult(const Bundle& bundle) { std::string getResult(const Bundle& bundle) {
@ -118,10 +118,10 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "InputDialog", .appId = "InputDialog",
.name = "Input Dialog", .appName = "Input Dialog",
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<InputDialogApp> .createApp = create<InputDialogApp>
}; };

View File

@ -1,6 +1,7 @@
#include <Tactility/Tactility.h> #include <Tactility/Tactility.h>
#include <Tactility/app/AppContext.h> #include <Tactility/app/AppContext.h>
#include <Tactility/app/AppPaths.h>
#include <Tactility/app/AppRegistration.h> #include <Tactility/app/AppRegistration.h>
#include <Tactility/hal/power/PowerDevice.h> #include <Tactility/hal/power/PowerDevice.h>
#include <Tactility/service/loader/Loader.h> #include <Tactility/service/loader/Loader.h>
@ -112,9 +113,9 @@ public:
const int32_t margin = is_landscape_display ? std::min<int32_t>(available_width / 16, button_size) : 0; const int32_t margin = is_landscape_display ? std::min<int32_t>(available_width / 16, button_size) : 0;
const auto paths = app.getPaths(); const auto paths = app.getPaths();
const auto apps_icon_path = paths->getSystemPathLvgl("assets/icon_apps.png"); const auto apps_icon_path = "A:" + paths->getAssetsPath("icon_apps.png");
const auto files_icon_path = paths->getSystemPathLvgl("assets/icon_files.png"); const auto files_icon_path = "A:" + paths->getAssetsPath("icon_files.png");
const auto settings_icon_path = paths->getSystemPathLvgl("assets/icon_settings.png"); const auto settings_icon_path = "A:" + paths->getAssetsPath("icon_settings.png");
createAppButton(buttons_wrapper, ui_scale, apps_icon_path.c_str(), "AppList", margin); createAppButton(buttons_wrapper, ui_scale, apps_icon_path.c_str(), "AppList", margin);
createAppButton(buttons_wrapper, ui_scale, files_icon_path.c_str(), "Files", margin); createAppButton(buttons_wrapper, ui_scale, files_icon_path.c_str(), "Files", margin);
@ -136,14 +137,15 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Launcher", .appId = "Launcher",
.name = "Launcher", .appName = "Launcher",
.category = Category::System, .appCategory = Category::System,
.appFlags = AppManifest::Flags::Hidden,
.createApp = create<LauncherApp> .createApp = create<LauncherApp>
}; };
void start() { void start() {
service::loader::startApp(manifest.id); service::loader::startApp(manifest.appId);
} }
} // namespace } // namespace

View File

@ -162,15 +162,15 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "LocaleSettings", .appId = "LocaleSettings",
.name = "Region & Language", .appName = "Region & Language",
.icon = TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS, .appIcon = TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS,
.category = Category::Settings, .appCategory = Category::Settings,
.createApp = create<LocaleSettingsApp> .createApp = create<LocaleSettingsApp>
}; };
void start() { void start() {
service::loader::startApp(manifest.id); service::loader::startApp(manifest.appId);
} }
} // namespace } // namespace

View File

@ -120,10 +120,10 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Log", .appId = "Log",
.name = "Log", .appName = "Log",
.icon = LV_SYMBOL_LIST, .appIcon = LV_SYMBOL_LIST,
.category = Category::System, .appCategory = Category::System,
.createApp = create<LogApp> .createApp = create<LogApp>
}; };

View File

@ -207,15 +207,15 @@ class NotesApp : public App {
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Notes", .appId = "Notes",
.name = "Notes", .appName = "Notes",
.icon = TT_ASSETS_APP_ICON_NOTES, .appIcon = TT_ASSETS_APP_ICON_NOTES,
.createApp = create<NotesApp> .createApp = create<NotesApp>
}; };
void start(const std::string& filePath) { void start(const std::string& filePath) {
auto parameters = std::make_shared<Bundle>(); auto parameters = std::make_shared<Bundle>();
parameters->putString(NOTES_FILE_ARGUMENT, filePath); parameters->putString(NOTES_FILE_ARGUMENT, filePath);
service::loader::startApp(manifest.id, parameters); service::loader::startApp(manifest.appId, parameters);
} }
} // namespace tt::app::notes } // namespace tt::app::notes

View File

@ -22,7 +22,7 @@ class PowerApp;
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
std::shared_ptr<PowerApp> _Nullable optApp() { std::shared_ptr<PowerApp> _Nullable optApp() {
auto appContext = getCurrentAppContext(); auto appContext = getCurrentAppContext();
if (appContext != nullptr && appContext->getManifest().id == manifest.id) { if (appContext != nullptr && appContext->getManifest().appId == manifest.appId) {
return std::static_pointer_cast<PowerApp>(appContext->getApp()); return std::static_pointer_cast<PowerApp>(appContext->getApp());
} else { } else {
return nullptr; return nullptr;
@ -189,10 +189,10 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Power", .appId = "Power",
.name = "Power", .appName = "Power",
.icon = TT_ASSETS_APP_ICON_POWER_SETTINGS, .appIcon = TT_ASSETS_APP_ICON_POWER_SETTINGS,
.category = Category::Settings, .appCategory = Category::Settings,
.createApp = create<PowerApp> .createApp = create<PowerApp>
}; };

View File

@ -48,7 +48,7 @@ public:
/** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */ /** Returns the app data if the app is active. Note that this could clash if the same app is started twice and a background thread is slow. */
std::shared_ptr<ScreenshotApp> _Nullable optApp() { std::shared_ptr<ScreenshotApp> _Nullable optApp() {
auto appContext = getCurrentAppContext(); auto appContext = getCurrentAppContext();
if (appContext != nullptr && appContext->getManifest().id == manifest.id) { if (appContext != nullptr && appContext->getManifest().appId == manifest.appId) {
return std::static_pointer_cast<ScreenshotApp>(appContext->getApp()); return std::static_pointer_cast<ScreenshotApp>(appContext->getApp());
} else { } else {
return nullptr; return nullptr;
@ -278,10 +278,10 @@ void ScreenshotApp::onShow(AppContext& appContext, lv_obj_t* parent) {
} }
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Screenshot", .appId = "Screenshot",
.name = "Screenshot", .appName = "Screenshot",
.icon = LV_SYMBOL_IMAGE, .appIcon = LV_SYMBOL_IMAGE,
.category = Category::System, .appCategory = Category::System,
.createApp = create<ScreenshotApp> .createApp = create<ScreenshotApp>
}; };

View File

@ -26,7 +26,7 @@ void start(const std::string& title, const std::vector<std::string>& items) {
auto bundle = std::make_shared<Bundle>(); auto bundle = std::make_shared<Bundle>();
bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title); bundle->putString(PARAMETER_BUNDLE_KEY_TITLE, title);
bundle->putString(PARAMETER_BUNDLE_KEY_ITEMS, items_joined); bundle->putString(PARAMETER_BUNDLE_KEY_ITEMS, items_joined);
service::loader::startApp(manifest.id, bundle); service::loader::startApp(manifest.appId, bundle);
} }
int32_t getResultIndex(const Bundle& bundle) { int32_t getResultIndex(const Bundle& bundle) {
@ -112,10 +112,10 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "SelectionDialog", .appId = "SelectionDialog",
.name = "Selection Dialog", .appName = "Selection Dialog",
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<SelectionDialogApp> .createApp = create<SelectionDialogApp>
}; };

View File

@ -85,10 +85,10 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "SerialConsole", .appId = "SerialConsole",
.name = "Serial Console", .appName = "Serial Console",
.icon = LV_SYMBOL_LIST, .appIcon = LV_SYMBOL_LIST,
.category = Category::System, .appCategory = Category::System,
.createApp = create<SerialConsoleApp> .createApp = create<SerialConsoleApp>
}; };

View File

@ -12,14 +12,14 @@ namespace tt::app::settings {
static void onAppPressed(lv_event_t* e) { static void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e)); const auto* manifest = static_cast<const AppManifest*>(lv_event_get_user_data(e));
service::loader::startApp(manifest->id); service::loader::startApp(manifest->appId);
} }
static void createWidget(const std::shared_ptr<AppManifest>& manifest, void* parent) { static void createWidget(const std::shared_ptr<AppManifest>& manifest, void* parent) {
tt_check(parent); tt_check(parent);
auto* list = (lv_obj_t*)parent; auto* list = (lv_obj_t*)parent;
const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; const void* icon = !manifest->appIcon.empty() ? manifest->appIcon.c_str() : TT_ASSETS_APP_ICON_FALLBACK;
auto* btn = lv_list_add_button(list, icon, manifest->name.c_str()); auto* btn = lv_list_add_button(list, icon, manifest->appName.c_str());
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest.get()); lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)manifest.get());
} }
@ -38,7 +38,7 @@ class SettingsApp : public App {
auto manifests = getApps(); auto manifests = getApps();
std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); std::sort(manifests.begin(), manifests.end(), SortAppManifestByName);
for (const auto& manifest: manifests) { for (const auto& manifest: manifests) {
if (manifest->category == Category::Settings) { if (manifest->appCategory == Category::Settings) {
createWidget(manifest, list); createWidget(manifest, list);
} }
} }
@ -46,11 +46,11 @@ class SettingsApp : public App {
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "Settings", .appId = "Settings",
.name = "Settings", .appName = "Settings",
.icon = TT_ASSETS_APP_ICON_SETTINGS, .appIcon = TT_ASSETS_APP_ICON_SETTINGS,
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<SettingsApp> .createApp = create<SettingsApp>
}; };

View File

@ -292,10 +292,10 @@ class SystemInfoApp final : public App {
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "SystemInfo", .appId = "SystemInfo",
.name = "System Info", .appName = "System Info",
.icon = TT_ASSETS_APP_ICON_SYSTEM_INFO, .appIcon = TT_ASSETS_APP_ICON_SYSTEM_INFO,
.category = Category::System, .appCategory = Category::System,
.createApp = create<SystemInfoApp> .createApp = create<SystemInfoApp>
}; };

View File

@ -57,15 +57,15 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "TimeDateSettings", .appId = "TimeDateSettings",
.name = "Time & Date", .appName = "Time & Date",
.icon = TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS, .appIcon = TT_ASSETS_APP_ICON_TIME_DATE_SETTINGS,
.category = Category::Settings, .appCategory = Category::Settings,
.createApp = create<TimeDateSettingsApp> .createApp = create<TimeDateSettingsApp>
}; };
void start() { void start() {
service::loader::startApp(manifest.id); service::loader::startApp(manifest.appId);
} }
} // namespace } // namespace

View File

@ -1,5 +1,6 @@
#include <Tactility/app/AppContext.h> #include <Tactility/app/AppContext.h>
#include <Tactility/app/AppManifest.h> #include <Tactility/app/AppManifest.h>
#include <Tactility/app/AppPaths.h>
#include <Tactility/app/timezone/TimeZone.h> #include <Tactility/app/timezone/TimeZone.h>
#include <Tactility/lvgl/Toolbar.h> #include <Tactility/lvgl/Toolbar.h>
#include <Tactility/lvgl/LvglSync.h> #include <Tactility/lvgl/LvglSync.h>
@ -200,7 +201,7 @@ public:
lv_obj_set_style_image_recolor_opa(icon, 255, 0); lv_obj_set_style_image_recolor_opa(icon, 255, 0);
lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0); lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0);
std::string icon_path = app.getPaths()->getSystemPathLvgl("search.png"); std::string icon_path = "A:" + app.getPaths()->getAssetsPath("search.png");
lv_image_set_src(icon, icon_path.c_str()); lv_image_set_src(icon, icon_path.c_str());
lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0); lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0);
@ -226,15 +227,15 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "TimeZone", .appId = "TimeZone",
.name = "Select timezone", .appName = "Select timezone",
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<TimeZoneApp> .createApp = create<TimeZoneApp>
}; };
void start() { void start() {
service::loader::startApp(manifest.id); service::loader::startApp(manifest.appId);
} }
} }

View File

@ -42,10 +42,10 @@ class UsbSettingsApp : public App {
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "UsbSettings", .appId = "UsbSettings",
.name = "USB", .appName = "USB",
.icon = LV_SYMBOL_USB, .appIcon = LV_SYMBOL_USB,
.category = Category::Settings, .appCategory = Category::Settings,
.createApp = create<UsbSettingsApp> .createApp = create<UsbSettingsApp>
}; };

View File

@ -21,7 +21,7 @@ extern const AppManifest manifest;
void start(const std::string& ssid) { void start(const std::string& ssid) {
auto bundle = std::make_shared<Bundle>(); auto bundle = std::make_shared<Bundle>();
bundle->putString("ssid", ssid); bundle->putString("ssid", ssid);
app::start(manifest.id, bundle); app::start(manifest.appId, bundle);
} }
class WifiApSettings : public App { class WifiApSettings : public App {
@ -241,11 +241,11 @@ public:
}; };
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "WifiApSettings", .appId = "WifiApSettings",
.name = "Wi-Fi AP Settings", .appName = "Wi-Fi AP Settings",
.icon = LV_SYMBOL_WIFI, .appIcon = LV_SYMBOL_WIFI,
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<WifiApSettings> .createApp = create<WifiApSettings>
}; };

View File

@ -94,11 +94,11 @@ void WifiConnect::onHide(TT_UNUSED AppContext& app) {
} }
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "WifiConnect", .appId = "WifiConnect",
.name = "Wi-Fi Connect", .appName = "Wi-Fi Connect",
.icon = LV_SYMBOL_WIFI, .appIcon = LV_SYMBOL_WIFI,
.category = Category::System, .appCategory = Category::System,
.flags = AppManifest::Flags::Hidden, .appFlags = AppManifest::Flags::Hidden,
.createApp = create<WifiConnect> .createApp = create<WifiConnect>
}; };
@ -106,7 +106,7 @@ void start(const std::string& ssid, const std::string& password) {
auto parameters = std::make_shared<Bundle>(); auto parameters = std::make_shared<Bundle>();
parameters->putString(WIFI_CONNECT_PARAM_SSID, ssid); parameters->putString(WIFI_CONNECT_PARAM_SSID, ssid);
parameters->putString(WIFI_CONNECT_PARAM_PASSWORD, password); parameters->putString(WIFI_CONNECT_PARAM_PASSWORD, password);
service::loader::startApp(manifest.id, parameters); service::loader::startApp(manifest.appId, parameters);
} }
bool optSsidParameter(const std::shared_ptr<const Bundle>& bundle, std::string& ssid) { bool optSsidParameter(const std::shared_ptr<const Bundle>& bundle, std::string& ssid) {

View File

@ -133,15 +133,15 @@ void WifiManage::onHide(TT_UNUSED AppContext& app) {
} }
extern const AppManifest manifest = { extern const AppManifest manifest = {
.id = "WifiManage", .appId = "WifiManage",
.name = "Wi-Fi", .appName = "Wi-Fi",
.icon = LV_SYMBOL_WIFI, .appIcon = LV_SYMBOL_WIFI,
.category = Category::Settings, .appCategory = Category::Settings,
.createApp = create<WifiManage> .createApp = create<WifiManage>
}; };
void start() { void start() {
service::loader::startApp(manifest.id); service::loader::startApp(manifest.appId);
} }
} // namespace } // namespace

View File

@ -1,6 +1,8 @@
#include <Tactility/hal/sdcard/SdCardMounting.h> #include <Tactility/hal/sdcard/SdCardMounting.h>
#include <Tactility/hal/sdcard/SdCardDevice.h> #include <Tactility/hal/sdcard/SdCardDevice.h>
#include <format>
namespace tt::hal::sdcard { namespace tt::hal::sdcard {
constexpr auto* TAG = "SdCardMounting"; constexpr auto* TAG = "SdCardMounting";
@ -15,24 +17,18 @@ static void mount(const std::shared_ptr<SdCardDevice>& sdcard, const std::string
}); });
} }
static std::string getMountPath(int index, int count) {
return (count == 1) ? TT_SDCARD_MOUNT_POINT : std::format("{}{}", TT_SDCARD_MOUNT_POINT, index);
}
void mountAll() { void mountAll() {
auto sdcards = hal::findDevices<SdCardDevice>(Device::Type::SdCard); const auto sdcards = hal::findDevices<SdCardDevice>(Device::Type::SdCard);
if (!sdcards.empty()) { // Numbered mount path name
if (sdcards.size() == 1) { for (int i = 0; i < sdcards.size(); i++) {
// Fixed mount path name auto sdcard = sdcards[i];
auto sdcard = sdcards[0]; if (!sdcard->isMounted() && sdcard->getMountBehaviour() == SdCardDevice::MountBehaviour::AtBoot) {
if (!sdcard->isMounted()) { std::string mount_path = getMountPath(i, sdcards.size());
mount(sdcard, TT_SDCARD_MOUNT_POINT); mount(sdcard, mount_path);
}
} else {
// Numbered mount path name
for (int i = 0; i < sdcards.size(); i++) {
auto sdcard = sdcards[i];
if (!sdcard->isMounted()) {
std::string mount_path = TT_SDCARD_MOUNT_POINT + std::to_string(i);
mount(sdcard, mount_path);
}
}
} }
} }
} }

View File

@ -144,7 +144,7 @@ lv_obj_t* toolbar_create(lv_obj_t* parent, const std::string& title) {
} }
lv_obj_t* toolbar_create(lv_obj_t* parent, const app::AppContext& app) { lv_obj_t* toolbar_create(lv_obj_t* parent, const app::AppContext& app) {
return toolbar_create(parent, app.getManifest().name); return toolbar_create(parent, app.getManifest().appName);
} }
void toolbar_set_title(lv_obj_t* obj, const std::string& title) { void toolbar_set_title(lv_obj_t* obj, const std::string& title) {

View File

@ -1,17 +1,19 @@
#include "Tactility/service/ServiceInstance.h" #include <Tactility/service/ServiceInstance.h>
#include "Tactility/service/ServiceInstancePaths.h"
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServicePaths.h>
namespace tt::service { namespace tt::service {
ServiceInstance::ServiceInstance(std::shared_ptr<const service::ServiceManifest> manifest) : ServiceInstance::ServiceInstance(std::shared_ptr<const ServiceManifest> manifest) :
manifest(manifest), manifest(manifest),
service(manifest->createService()) service(manifest->createService())
{} {}
const service::ServiceManifest& ServiceInstance::getManifest() const { return *manifest; } const ServiceManifest& ServiceInstance::getManifest() const { return *manifest; }
std::unique_ptr<Paths> ServiceInstance::getPaths() const { std::unique_ptr<ServicePaths> ServiceInstance::getPaths() const {
return std::make_unique<ServiceInstancePaths>(manifest); return std::make_unique<ServicePaths>(manifest);
} }
} }

View File

@ -1,50 +0,0 @@
#include "Tactility/service/ServiceInstancePaths.h"
#include "Tactility/MountPoints.h"
#define LVGL_PATH_PREFIX std::string("A:/")
#ifdef ESP_PLATFORM
#define PARTITION_PREFIX std::string("/")
#else
#define PARTITION_PREFIX std::string("")
#endif
namespace tt::service {
std::string ServiceInstancePaths::getDataDirectory() const {
return PARTITION_PREFIX + file::DATA_PARTITION_NAME + "/service/" + manifest->id;
}
std::string ServiceInstancePaths::getDataDirectoryLvgl() const {
return LVGL_PATH_PREFIX + file::DATA_PARTITION_NAME + "/service/" + manifest->id;
}
std::string ServiceInstancePaths::getDataPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return PARTITION_PREFIX + file::DATA_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
}
std::string ServiceInstancePaths::getDataPathLvgl(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return LVGL_PATH_PREFIX + file::DATA_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
}
std::string ServiceInstancePaths::getSystemDirectory() const {
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/service/" + manifest->id;
}
std::string ServiceInstancePaths::getSystemDirectoryLvgl() const {
return LVGL_PATH_PREFIX + file::SYSTEM_PARTITION_NAME + "/service/" + manifest->id;
}
std::string ServiceInstancePaths::getSystemPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
}
std::string ServiceInstancePaths::getSystemPathLvgl(const std::string& childPath) const {
return LVGL_PATH_PREFIX + file::SYSTEM_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
}
}

View File

@ -0,0 +1,35 @@
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/MountPoints.h>
#include <cassert>
#include <format>
#ifdef ESP_PLATFORM
constexpr auto PARTITION_PREFIX = std::string("/");
#else
constexpr auto PARTITION_PREFIX = std::string("");
#endif
namespace tt::service {
std::string ServicePaths::getUserDataDirectory() const {
return std::format("{}{}/service/{}", PARTITION_PREFIX, file::DATA_PARTITION_NAME, manifest->id);
}
std::string ServicePaths::getUserDataPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return std::format("{}/{}", getUserDataDirectory(), childPath);
}
std::string ServicePaths::getAssetsDirectory() const {
return std::format("{}{}/service/{}/assets", PARTITION_PREFIX, file::SYSTEM_PARTITION_NAME, manifest->id);
}
std::string ServicePaths::getAssetsPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return std::format("{}/{}", getAssetsDirectory(), childPath);
}
}

View File

@ -4,10 +4,10 @@
#include <Tactility/app/App.h> #include <Tactility/app/App.h>
#include <Tactility/app/AppRegistration.h> #include <Tactility/app/AppRegistration.h>
#include <Tactility/app/ElfApp.h>
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
#include <Tactility/network/HttpdReq.h> #include <Tactility/network/HttpdReq.h>
#include <Tactility/network/Url.h> #include <Tactility/network/Url.h>
#include <Tactility/Paths.h>
#include <Tactility/service/development/DevelopmentSettings.h> #include <Tactility/service/development/DevelopmentSettings.h>
#include <Tactility/service/ServiceManifest.h> #include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h> #include <Tactility/service/ServiceRegistration.h>
@ -247,7 +247,7 @@ esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
} }
content_left -= content_read; content_left -= content_read;
const std::string tmp_path = app::getTempPath(); const std::string tmp_path = getTempPath();
auto lock = file::getLock(tmp_path)->asScopedLock(); auto lock = file::getLock(tmp_path)->asScopedLock();
lock.lock(); lock.lock();

View File

@ -1,6 +1,6 @@
#include "Tactility/service/gps/GpsService.h" #include <Tactility/file/ObjectFile.h>
#include <Tactility/service/gps/GpsService.h>
#include "Tactility/file/ObjectFile.h" #include <Tactility/service/ServicePaths.h>
#include <cstring> #include <cstring>
#include <unistd.h> #include <unistd.h>
@ -17,12 +17,12 @@ bool GpsService::getConfigurationFilePath(std::string& output) const {
return false; return false;
} }
if (!file::findOrCreateDirectory(paths->getDataDirectory(), 0777)) { if (!file::findOrCreateDirectory(paths->getUserDataDirectory(), 0777)) {
TT_LOG_E(TAG, "Failed to find or create path %s", paths->getDataDirectory().c_str()); TT_LOG_E(TAG, "Failed to find or create path %s", paths->getUserDataDirectory().c_str());
return false; return false;
} }
output = paths->getDataPath("config.bin"); output = paths->getUserDataPath("config.bin");
return true; return true;
} }

View File

@ -1,9 +1,10 @@
#include "Tactility/service/gps/GpsService.h" #include <Tactility/service/gps/GpsService.h>
#include "Tactility/service/ServiceManifest.h"
#include "Tactility/service/ServiceRegistration.h"
#include <Tactility/Log.h>
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
#include <Tactility/Log.h>
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
using tt::hal::gps::GpsDevice; using tt::hal::gps::GpsDevice;

View File

@ -193,7 +193,7 @@ void GuiService::requestDraw() {
void GuiService::showApp(std::shared_ptr<app::AppContext> app) { void GuiService::showApp(std::shared_ptr<app::AppContext> app) {
lock(); lock();
if (!isStarted) { if (!isStarted) {
TT_LOG_W(TAG, "Failed to show app %s: GUI not started", app->getManifest().id.c_str()); TT_LOG_W(TAG, "Failed to show app %s: GUI not started", app->getManifest().appId.c_str());
} else { } else {
// Ensure previous app triggers onHide() logic // Ensure previous app triggers onHide() logic
if (appToRender != nullptr) { if (appToRender != nullptr) {

View File

@ -103,7 +103,7 @@ void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launc
auto previous_app = !appStack.empty() ? appStack.top() : nullptr; auto previous_app = !appStack.empty() ? appStack.top() : nullptr;
auto new_app = std::make_shared<app::AppInstance>(app_manifest, launchId, parameters); auto new_app = std::make_shared<app::AppInstance>(app_manifest, launchId, parameters);
new_app->mutableFlags().hideStatusbar = (app_manifest->flags & app::AppManifest::Flags::HideStatusBar); new_app->mutableFlags().hideStatusbar = (app_manifest->appFlags & app::AppManifest::Flags::HideStatusBar);
appStack.push(new_app); appStack.push(new_app);
transitionAppToState(new_app, app::State::Initial); transitionAppToState(new_app, app::State::Initial);
@ -136,12 +136,12 @@ void LoaderService::onStopAppMessage(const std::string& id) {
// Stop current app // Stop current app
auto app_to_stop = appStack.top(); auto app_to_stop = appStack.top();
if (app_to_stop->getManifest().id != id) { if (app_to_stop->getManifest().appId != id) {
TT_LOG_E(TAG, "Stop app: id mismatch (wanted %s but found %s on top of stack)", id.c_str(), app_to_stop->getManifest().id.c_str()); TT_LOG_E(TAG, "Stop app: id mismatch (wanted %s but found %s on top of stack)", id.c_str(), app_to_stop->getManifest().appId.c_str());
return; return;
} }
if (original_stack_size == 1 && app_to_stop->getManifest().name != "Boot") { if (original_stack_size == 1 && app_to_stop->getManifest().appName != "Boot") {
TT_LOG_E(TAG, "Stop app: can't stop root app"); TT_LOG_E(TAG, "Stop app: can't stop root app");
return; return;
} }
@ -162,12 +162,12 @@ void LoaderService::onStopAppMessage(const std::string& id) {
// We only expect the app to be referenced within the current scope // We only expect the app to be referenced within the current scope
if (app_to_stop.use_count() > 1) { if (app_to_stop.use_count() > 1) {
TT_LOG_W(TAG, "Memory leak: Stopped %s, but use count is %ld", app_to_stop->getManifest().id.c_str(), app_to_stop.use_count() - 1); TT_LOG_W(TAG, "Memory leak: Stopped %s, but use count is %ld", app_to_stop->getManifest().appId.c_str(), app_to_stop.use_count() - 1);
} }
// Refcount is expected to be 2: 1 within app_to_stop and 1 within the current scope // Refcount is expected to be 2: 1 within app_to_stop and 1 within the current scope
if (app_to_stop->getApp().use_count() > 2) { if (app_to_stop->getApp().use_count() > 2) {
TT_LOG_W(TAG, "Memory leak: Stopped %s, but use count is %ld", app_to_stop->getManifest().id.c_str(), app_to_stop->getApp().use_count() - 2); TT_LOG_W(TAG, "Memory leak: Stopped %s, but use count is %ld", app_to_stop->getManifest().appId.c_str(), app_to_stop->getApp().use_count() - 2);
} }
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
@ -224,7 +224,7 @@ void LoaderService::transitionAppToState(const std::shared_ptr<app::AppInstance>
TT_LOG_I( TT_LOG_I(
TAG, TAG,
"App \"%s\" state: %s -> %s", "App \"%s\" state: %s -> %s",
app_manifest.id.c_str(), app_manifest.appId.c_str(),
appStateToString(old_state), appStateToString(old_state),
appStateToString(state) appStateToString(state)
); );
@ -263,7 +263,7 @@ app::LaunchId LoaderService::startApp(const std::string& id, std::shared_ptr<con
void LoaderService::stopApp() { void LoaderService::stopApp() {
TT_LOG_I(TAG, "stopApp()"); TT_LOG_I(TAG, "stopApp()");
auto id = getCurrentAppContext()->getManifest().id; auto id = getCurrentAppContext()->getManifest().appId;
dispatcherThread->dispatch([this, id]() { dispatcherThread->dispatch([this, id]() {
onStopAppMessage(id); onStopAppMessage(id);
}); });

View File

@ -84,10 +84,10 @@ void ScreenshotTask::taskMain() {
auto appContext = app::getCurrentAppContext(); auto appContext = app::getCurrentAppContext();
if (appContext != nullptr) { if (appContext != nullptr) {
const app::AppManifest& manifest = appContext->getManifest(); const app::AppManifest& manifest = appContext->getManifest();
if (manifest.id != last_app_id) { if (manifest.appId != last_app_id) {
kernel::delayMillis(100); kernel::delayMillis(100);
last_app_id = manifest.id; last_app_id = manifest.appId;
auto filename = std::format("{}/screenshot-{}.png", work.path, manifest.id); auto filename = std::format("{}/screenshot-{}.png", work.path, manifest.appId);
makeScreenshot(filename); makeScreenshot(filename);
} }
} }

View File

@ -1,16 +1,16 @@
#include <Tactility/lvgl/Statusbar.h> #include <Tactility/lvgl/Statusbar.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/hal/power/PowerDevice.h> #include <Tactility/hal/power/PowerDevice.h>
#include <Tactility/hal/sdcard/SdCardDevice.h> #include <Tactility/hal/sdcard/SdCardDevice.h>
#include <Tactility/lvgl/Lvgl.h> #include <Tactility/lvgl/Lvgl.h>
#include <Tactility/service/gps/GpsService.h> #include <Tactility/lvgl/LvglSync.h>
#include <Tactility/Mutex.h> #include <Tactility/Mutex.h>
#include <Tactility/Tactility.h> #include <Tactility/service/gps/GpsService.h>
#include <Tactility/Timer.h>
#include <Tactility/service/ServiceContext.h> #include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/ServiceRegistration.h> #include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/wifi/Wifi.h> #include <Tactility/service/wifi/Wifi.h>
#include <Tactility/Timer.h>
namespace tt::service::statusbar { namespace tt::service::statusbar {
@ -148,7 +148,7 @@ class StatusbarService final : public Service {
int8_t power_icon_id; int8_t power_icon_id;
const char* power_last_icon = nullptr; const char* power_last_icon = nullptr;
std::unique_ptr<Paths> paths; std::unique_ptr<ServicePaths> paths;
void lock() const { void lock() const {
mutex.lock(); mutex.lock();
@ -163,7 +163,7 @@ class StatusbarService final : public Service {
bool show_icon = (gps_state == gps::State::OnPending) || (gps_state == gps::State::On); bool show_icon = (gps_state == gps::State::OnPending) || (gps_state == gps::State::On);
if (gps_last_state != show_icon) { if (gps_last_state != show_icon) {
if (show_icon) { if (show_icon) {
auto icon_path = paths->getSystemPathLvgl(STATUSBAR_ICON_GPS); auto icon_path = "A:" + paths->getAssetsPath(STATUSBAR_ICON_GPS);
lvgl::statusbar_icon_set_image(gps_icon_id, icon_path); lvgl::statusbar_icon_set_image(gps_icon_id, icon_path);
lvgl::statusbar_icon_set_visibility(gps_icon_id, true); lvgl::statusbar_icon_set_visibility(gps_icon_id, true);
} else { } else {
@ -179,7 +179,7 @@ class StatusbarService final : public Service {
const char* desired_icon = getWifiStatusIcon(radio_state, is_secure); const char* desired_icon = getWifiStatusIcon(radio_state, is_secure);
if (wifi_last_icon != desired_icon) { if (wifi_last_icon != desired_icon) {
if (desired_icon != nullptr) { if (desired_icon != nullptr) {
auto icon_path = paths->getSystemPathLvgl(desired_icon); auto icon_path = "A:" + paths->getAssetsPath(desired_icon);
lvgl::statusbar_icon_set_image(wifi_icon_id, icon_path); lvgl::statusbar_icon_set_image(wifi_icon_id, icon_path);
lvgl::statusbar_icon_set_visibility(wifi_icon_id, true); lvgl::statusbar_icon_set_visibility(wifi_icon_id, true);
} else { } else {
@ -193,7 +193,7 @@ class StatusbarService final : public Service {
const char* desired_icon = getPowerStatusIcon(); const char* desired_icon = getPowerStatusIcon();
if (power_last_icon != desired_icon) { if (power_last_icon != desired_icon) {
if (desired_icon != nullptr) { if (desired_icon != nullptr) {
auto icon_path = paths->getSystemPathLvgl(desired_icon); auto icon_path = "A:" + paths->getAssetsPath(desired_icon);
lvgl::statusbar_icon_set_image(power_icon_id, icon_path); lvgl::statusbar_icon_set_image(power_icon_id, icon_path);
lvgl::statusbar_icon_set_visibility(power_icon_id, true); lvgl::statusbar_icon_set_visibility(power_icon_id, true);
} else { } else {
@ -212,7 +212,7 @@ class StatusbarService final : public Service {
if (state != hal::sdcard::SdCardDevice::State::Timeout) { if (state != hal::sdcard::SdCardDevice::State::Timeout) {
auto* desired_icon = getSdCardStatusIcon(state); auto* desired_icon = getSdCardStatusIcon(state);
if (sdcard_last_icon != desired_icon) { if (sdcard_last_icon != desired_icon) {
auto icon_path = paths->getSystemPathLvgl(desired_icon); auto icon_path = "A:" + paths->getAssetsPath(desired_icon);
lvgl::statusbar_icon_set_image(sdcard_icon_id, icon_path); lvgl::statusbar_icon_set_image(sdcard_icon_id, icon_path);
lvgl::statusbar_icon_set_visibility(sdcard_icon_id, true); lvgl::statusbar_icon_set_visibility(sdcard_icon_id, true);
sdcard_last_icon = desired_icon; sdcard_last_icon = desired_icon;

Some files were not shown because too many files have changed in this diff Show More