Merge develop into main (#304)

## New

- Read property files with `PropertiesFile`
- Support `boot.properties` so the user can specify the launcher app and an optional app to start after the launcher finishes. (see `BootProperties.cpp`)
- Create registry for CPU affinity and update code to make use of it
- `AppRegistration` and `ServiceRegistration` now also ensure that the `/data` directories always exist for all apps
- `Notes` is now the default app for opening text files. `TextViewer` is removed entirely. Created `tt::app:🎶:start(path)` function.
- WiFi settings moved from NVS to properties file.
- Specify `*.ap.properties` file on the SD card for automatic WiFi settings import on start-up.
- Added `file::getLock(path)` and `file::withLock(path, function)` to do safe file operations on SD cards

## Improvements

- Update TinyUSB to `1.7.6~1`
- Improved `Boot.cpp` code. General code quality fixes and some restructuring to improve readability.
- `tt::string` functionality improvements
- Rename `AppRegistry` to `AppRegistration`
- Rename `ServiceRegistry` to `ServiceRegistration`
- Cleanup in `Notes.cpp`
- `FileTest.cpp` fix for PC
- Created `TestFile` helper class for tests, which automatically deletes files after the test.
- Renamed `Partitions.h` to `MountPoints.h`
- Created `std::string getMountPoints()` function for easy re-use
- Other code quality improvements
- `SdCardDevice`'s `getState()` and `isMounted()` now have a timeout argument

## Fixes

- ELF loading now has a lock so to avoid a bug when 2 ELF apps are loaded in parallel
This commit is contained in:
Ken Van Hoeylandt 2025-08-23 17:10:18 +02:00 committed by GitHub
parent fbaff8cbac
commit ee5a5a7181
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 1396 additions and 744 deletions

View File

@ -16,7 +16,7 @@ dependencies:
version: "1.3.2"
espressif/esp_lcd_panel_io_additions: "1.0.1"
espressif/esp_tinyusb:
version: "1.5.0"
version: "1.7.6~1"
rules:
- if: "target == esp32s3"
idf: '5.4'

View File

@ -8,8 +8,6 @@ using tt::hal::sdcard::SdCardDevice;
class SimulatorSdCard final : public SdCardDevice {
private:
State state;
std::shared_ptr<tt::Lock> lock;
std::string mountPath;
@ -21,10 +19,10 @@ public:
lock(std::make_shared<tt::Mutex>())
{}
std::string getName() const final { return "Mock SD Card"; }
std::string getDescription() const final { return ""; }
std::string getName() const override { return "Mock SD Card"; }
std::string getDescription() const override { return ""; }
bool mount(const std::string& newMountPath) final {
bool mount(const std::string& newMountPath) override {
state = State::Mounted;
mountPath = newMountPath;
return true;
@ -36,9 +34,9 @@ public:
return true;
}
std::string getMountPath() const final { return mountPath; };
std::string getMountPath() const override { return mountPath; }
tt::Lock& getLock() const final { return *lock; }
std::shared_ptr<tt::Lock> getLock() const override { return lock; }
State getState() const override { return state; }
State getState(TickType_t timeout) const override { return state; }
};

View File

@ -0,0 +1,2 @@
launcherAppId=Launcher
#autoStartAppId=

View File

@ -0,0 +1 @@
enableOnBoot=false

View File

@ -1,2 +0,0 @@
This file exists to test the partition.
It can be deleted when the partition contains other files.

View File

@ -1,41 +1,47 @@
# TODOs
## Higher Priority
- Move Development settings from flash to `/data/apps/development/development.properties` (just the "start on boot")
- Move Display settings from flash to `/data/apps/display/display.properties`
- App data directory should be automatically created (and then we can remove the custom code from Notes.cpp)
- When an external app fails to load (e.g. due to mapping error) then show an error dialog.
- Revisit TinyUSB mouse idea: the bugs related to cleanup seem to be fixed in the library.
- Bug: When a Wi-Fi SSID is too long, then it fails to save the credentials
- Add a Keyboard setting app to override the behaviour of soft keyboard hiding (e.g. keyboard hardware is present, but the user wants to use a soft keyboard)
- HAL for display touch calibration
- Expose app::Paths to TactilityC
- Call tt::lvgl::isSyncSet after HAL init and show an error (and crash?) when it is not set.
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility.
- Localization of texts (load in boot app from sd?)
- App packaging
- Create more unit tests for `tactility-core`
- Make a URL handler. Use it for handling local files. Match file types with apps.
## Lower Priority
- Support hot-plugging SD card
- Create more unit tests for `tactility`
- Explore LVGL9's FreeRTOS functionality
- CrashHandler: use "corrupted" flag
- CrashHandler: process other types of crashes (WDT?)
- Add a Keyboard setting in `keyboard.properties` to override the behaviour of soft keyboard hiding (e.g. keyboard hardware is present, but the user wants to use a soft keyboard)
- Use GPS time to set/update the current time
- All drivers (e.g. display, touch, etc.) should call stop() in their destructor, or at least assert that they should not be running.
- Fix bug in T-Deck/etc: esp_lvgl_port settings has a large stack size (~9kB) to fix an issue where the T-Deck would get a stackoverflow. This sometimes happens when WiFi is auto-enabled and you open the app while it is still connecting.
- Start using non_null (either via MS GSL, or custom)
- `hal/Configuration.h` defines C function types: Use C++ std::function instead
- Fix system time to not be 1980 (use build year as a minimum). Consider keeping track of the last known time.
- Use std::span or string_view in StringUtils https://youtu.be/FRkJCvHWdwQ?t=2754
- Fix bug in T-Deck/etc: esp_lvgl_port settings has a large stack size (~9kB) to fix an issue where the T-Deck would get a stackoverflow. This sometimes happens when WiFi is auto-enabled and you open the app while it is still connecting.
- Mutex: Implement give/take from ISR support (works only for non-recursive ones)
- Extend unPhone power driver: add charging status, usb connection status, etc.
- Expose app::Paths to TactilityC
- Make a ledger for setting CPU affinity of various services and tasks
- CrashHandler: use "corrupted" flag
- CrashHandler: process other types of crashes (WDT?)
- Call tt::lvgl::isSyncSet after HAL init and show an error (and crash?) when it is not set.
- Create different partition files for different ESP flash size targets (N4, N8, N16, N32)
- T-Deck: Clear screen before turning on blacklight
- T-Deck: Use knob for UI selection?
- Crash monitoring: Keep track of which system phase the app crashed in (e.g. which app in which state)
- App::onResult should pass the app id (or launch request id!) that was started, so we can differentiate between multiple types of apps being launched
- Create more unit tests for `tactility-core` and `tactility` (PC-only for now)
- Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials.
- Show a warning screen when a user plugs in the SD card on a device that only supports mounting at boot.
- Localisation of texts (load in boot app from sd?)
- Explore LVGL9's FreeRTOS functionality
- External app loading: Check version of Tactility and check ESP target hardware to check for compatibility.
- Scanning SD card for external apps and auto-register them (in a temporary register?)
- Support hot-plugging SD card
- All drivers (e.g. display, touch, etc.) should call stop() in their destructor, or at least assert that they should not be running.
- Use GPS time to set/update the current time
- Remove flex_flow from app_container in Gui.cpp
# Nice-to-haves
- Considering the lack of callstack debugging for external apps: allow for some debugging to be exposed during a device crash. Apps could report their state (e.g. an integer value) which can be stored during app operation and retrieve after crash. The same can be done for various OS apps and states. We can keep an array of these numbers to keep track of the last X states, to get an idea of what's going on.
- Give external app a different icon. Allow an external app to update their id, icon, type and name once they are running (and persist that info?). Loader will need to be able to find app by (external) location.
- Audio player app
- Audio recording app
@ -52,6 +58,7 @@
# App Ideas
- Revisit TinyUSB mouse idea: the bugs related to cleanup seem to be fixed in the library.
- Map widget:
https://github.com/portapack-mayhem/mayhem-firmware/blob/b66d8b1aa178d8a9cd06436fea788d5d58cb4c8d/firmware/application/ui/ui_geomap.cpp
https://github.com/portapack-mayhem/mayhem-firmware/blob/b66d8b1aa178d8a9cd06436fea788d5d58cb4c8d/firmware/tools/generate_world_map.bin.py
@ -68,3 +75,4 @@
- Compile unix tools to ELF apps?
- Todo list
- Calendar
- Display touch calibration

View File

@ -0,0 +1,24 @@
#pragma once
#include <string>
namespace tt {
struct BootProperties {
/** App to start automatically after the splash screen. */
std::string launcherAppId;
/** App to start automatically from the launcher screen. */
std::string autoStartAppId;
};
/**
* Load the boot properties from the relevant file location(s).
* It will first attempt to load them from the SD card and if no file was found,
* then it will try to load the one from the data mount point.
*
* @param[out] properties the resulting properties
* @return true when the properties were successfully loaded and the result was set
*/
bool loadBootProperties(BootProperties& properties);
}

View File

@ -0,0 +1,26 @@
#pragma once
#include <dirent.h>
#include <vector>
namespace tt::file {
constexpr auto* SYSTEM_PARTITION_NAME = "system";
#ifdef ESP_PLATFORM
constexpr auto* MOUNT_POINT_SYSTEM = "/system";
#else
constexpr auto* MOUNT_POINT_SYSTEM = "system";
#endif
constexpr auto* DATA_PARTITION_NAME = "data";
#ifdef ESP_PLATFORM
constexpr auto* MOUNT_POINT_DATA = "/data";
#else
constexpr auto* MOUNT_POINT_DATA = "data";
#endif
std::vector<dirent> getMountPoints();
} // namespace

View File

@ -1,21 +0,0 @@
#pragma once
namespace tt {
#define SYSTEM_PARTITION_NAME "system"
#ifdef ESP_PLATFORM
#define MOUNT_POINT_SYSTEM "/system"
#else
#define MOUNT_POINT_SYSTEM "system"
#endif
#define DATA_PARTITION_NAME "data"
#ifdef ESP_PLATFORM
#define MOUNT_POINT_DATA "/data"
#else
#define MOUNT_POINT_DATA "data"
#endif
} // namespace

View File

@ -4,7 +4,6 @@
#include <Tactility/hal/Configuration.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/TactilityConfig.h>
namespace tt {
@ -20,10 +19,6 @@ struct Configuration {
const std::vector<const app::AppManifest*> apps = {};
/** List of user services */
const std::vector<const service::ServiceManifest*> services = {};
/** Optional app to start automatically after the splash screen. */
const std::string launcherAppId = app::launcher::manifest.id;
/** Optional app to start automatically after the splash screen. */
const std::string autoStartAppId = {};
};
/**

View File

@ -1,15 +1,9 @@
#pragma once
#include "Tactility/app/ManifestRegistry.h"
#include <Tactility/CoreDefines.h>
#include <Tactility/Bundle.h>
#include "Tactility/app/AppRegistration.h"
#include <string>
// Forward declarations
typedef struct _lv_obj_t lv_obj_t;
namespace tt::app {
class App;
@ -40,8 +34,6 @@ enum class Result {
class Location {
private:
std::string path;
Location() = default;
explicit Location(const std::string& path) : path(path) {}
@ -54,7 +46,13 @@ public:
return Location(path);
}
/** Internal apps are all apps that are part of the firmware release. */
bool isInternal() const { return path.empty(); }
/**
* External apps are all apps that are not part of the firmware release.
* e.g. an application on the sd card or one that is installed in /data
*/
bool isExternal() const { return !path.empty(); }
const std::string& getPath() const { return path; }
};

View File

@ -0,0 +1,11 @@
#pragma once
namespace tt::app::notes {
/**
* Start the notes app with the specified text file.
* @param[in] filePath the path to the text file to open
*/
void start(const std::string& filePath);
}

View File

@ -1,7 +0,0 @@
#pragma once
namespace tt::app::textviewer {
void start(const std::string& file);
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <Tactility/Lock.h>
#include <memory>
#include <string>
/**
* Some file systems belong to devices on a shared bus (e.g. SPI SD card).
* Because of the shared bus, a lock is required for its operation.
*/
namespace tt::file {
/**
* @param[in] path the path to find a lock for
* @return a non-null lock for the specified path.
*/
std::shared_ptr<Lock> getLock(const std::string& path);
/**
* Acquires a lock, calls the function, then releases the lock.
* @param[in] path the path to find a lock for
* @param[in] fn the code to execute while the lock is acquired
*/
template<typename ReturnType>
ReturnType withLock(const std::string& path, std::function<ReturnType()> fn) {
const auto lock = getLock(path)->asScopedLock();
lock.lock();
return fn();
}
}

View File

@ -1,18 +1,18 @@
#pragma once
#include "File.h"
#include <Tactility/file/File.h>
#include <cstdint>
#include <functional>
#include <string>
#include <utility>
#include "FileLock.h"
/**
* @warning The functionality below does NOT safely acquire file locks. Use file::getLock() or file::withLock() when using the functionality below.
*/
namespace tt::file {
class ObjectFileReader {
private:
const std::string filePath;
const uint32_t recordSize = 0;
@ -41,12 +41,11 @@ public:
class ObjectFileWriter {
private:
const std::string filePath;
const uint32_t recordSize;
const uint32_t recordVersion;
const bool append;
const std::shared_ptr<Lock> lock;
std::unique_ptr<FILE, FileCloser> file;
uint32_t recordsWritten = 0;
@ -57,7 +56,8 @@ public:
filePath(std::move(filePath)),
recordSize(recordSize),
recordVersion(recordVersion),
append(append)
append(append),
lock(getLock(filePath))
{}

View File

@ -0,0 +1,36 @@
#pragma once
#include <functional>
#include <map>
#include <string>
/**
* @note The functionality below safely acquires and releases any SD card device locks. Manual locking isn't needed.
*/
namespace tt::file {
/**
* Load a properties file into a map
* @param[in] filePath the file to load
* @param[out] properties the resulting properties
* @return true when the properties file was opened successfully
*/
bool loadPropertiesFile(const std::string& filePath, std::map<std::string, std::string>& properties);
/**
* Load a properties file and report key-values to a function
* @param[in] filePath the file to load
* @param[in] callback the callback function that receives the key-values
* @return true when the properties file was opened successfully
*/
bool loadPropertiesFile(const std::string& filePath, std::function<void(const std::string& key, const std::string& value)> callback);
/**
* Save properties to a file
* @param[in] filePath the file to save to
* @param[in] properties the properties to save
* @return true when the data was written to the file succesfully
*/
bool savePropertiesFile(const std::string& filePath, const std::map<std::string, std::string>& properties);
}

View File

@ -14,7 +14,7 @@ public:
Mounted,
Unmounted,
Error,
Unknown
Timeout // Failed to retrieve state due to timeout
};
enum class MountBehaviour {
@ -35,32 +35,28 @@ public:
virtual bool mount(const std::string& mountPath) = 0;
virtual bool unmount() = 0;
virtual State getState() const = 0;
virtual State getState(TickType_t timeout = portMAX_DELAY) const = 0;
/** Return empty string when not mounted or the mount path if mounted */
virtual std::string getMountPath() const = 0;
virtual Lock& getLock() const = 0;
/** Non-null lock */
virtual std::shared_ptr<Lock> getLock() const = 0;
virtual MountBehaviour getMountBehaviour() const { return mountBehaviour; }
bool isMounted() const { return getState() == State::Mounted; }
/** @return true if the SD card was mounted, returns false when it was not or when a timeout happened. */
bool isMounted(TickType_t timeout = portMAX_DELAY) const { return getState(timeout) == State::Mounted; }
};
/** Return the SdCard device if the path is within the SdCard mounted path (path std::string::starts_with() check)*/
std::shared_ptr<SdCardDevice> _Nullable find(const std::string& path);
/**
* Acquires an SD card lock if the path is an SD card path.
* Always calls the function, but doesn't lock if the path is not an SD card path.
* Attempt to find an SD card that the specified belongs to,
* and returns its lock if the SD card is mounted. Otherwise it returns nullptr.
* @param[in] a path on a file system (e.g. file, directory, etc.)
* @return the lock of a mounted SD card or otherwise null
*/
template<typename ReturnType>
ReturnType withSdCardLock(const std::string& path, std::function<ReturnType()> fn) {
auto sdcard = find(path);
if (sdcard != nullptr) {
auto scoped_lockable = sdcard->getLock().asScopedLock();
scoped_lockable.lock(portMAX_DELAY);
}
return fn();
}
std::shared_ptr<Lock> findSdCardLock(const std::string& path);
} // namespace tt::hal

View File

@ -28,7 +28,7 @@ public:
gpio_num_t spiPinWp,
gpio_num_t spiPinInt,
MountBehaviour mountBehaviourAtBoot,
/** When custom lock is nullptr, use the SPI default one */
/** When customLock is a nullptr, use the SPI default one */
std::shared_ptr<Lock> _Nullable customLock = nullptr,
std::vector<gpio_num_t> csPinWorkAround = std::vector<gpio_num_t>(),
spi_host_device_t spiHost = SPI2_HOST,
@ -49,7 +49,7 @@ public:
gpio_num_t spiPinCd; // Card detect
gpio_num_t spiPinWp; // Write-protect
gpio_num_t spiPinInt; // Interrupt
SdCardDevice::MountBehaviour mountBehaviourAtBoot;
MountBehaviour mountBehaviourAtBoot;
std::shared_ptr<Lock> _Nullable customLock;
std::vector<gpio_num_t> csPinWorkAround;
spi_host_device_t spiHost;
@ -74,22 +74,22 @@ public:
config(std::move(config))
{}
std::string getName() const final { return "SD Card"; }
std::string getDescription() const final { return "SD card via SPI interface"; }
std::string getName() const override { return "SD Card"; }
std::string getDescription() const override { return "SD card via SPI interface"; }
bool mount(const std::string& mountPath) final;
bool unmount() final;
std::string getMountPath() const final { return mountPath; }
bool mount(const std::string& mountPath) override;
bool unmount() override;
std::string getMountPath() const override { return mountPath; }
Lock& getLock() const final {
if (config->customLock) {
return *config->customLock;
std::shared_ptr<Lock> getLock() const override {
if (config->customLock != nullptr) {
return config->customLock;
} else {
return *spi::getLock(config->spiHost);
return spi::getLock(config->spiHost);
}
}
State getState() const override;
State getState(TickType_t timeout) const override;
sdmmc_card_t* _Nullable getCard() { return card; }
};

View File

@ -28,6 +28,7 @@ enum class SystemEvent {
/** Value 0 mean "no subscription" */
typedef uint32_t SystemEventSubscription;
constexpr SystemEventSubscription NoSystemEventSubscription = 0U;
typedef std::function<void(SystemEvent)> OnSystemEvent;

View File

@ -1,17 +1,14 @@
#pragma once
#include "./WifiGlobals.h"
#include "./WifiSettings.h"
#include <Tactility/PubSub.h>
#include <cstdio>
#include <string>
#include <vector>
#include "WifiApSettings.h"
#ifdef ESP_PLATFORM
#include "esp_wifi.h"
#include "WifiSettings.h"
#else
#include <cstdint>
// From esp_wifi_types.h in ESP-IDF 5.2
@ -124,7 +121,7 @@ std::string getIp();
* @param[in] ap
* @param[in] remember whether to save the ap data to the settings upon successful connection
*/
void connect(const settings::WifiApSettings* ap, bool remember);
void connect(const settings::WifiApSettings& ap, bool remember);
/** @brief Disconnect from the access point. Doesn't have any effect when not connected. */
void disconnect();

View File

@ -0,0 +1,58 @@
#pragma once
#include <string>
namespace tt::service::wifi::settings {
/**
* This struct is stored as-is into NVS flash.
*
* The SSID and secret are increased by 1 byte to facilitate string null termination.
* This makes it easier to use the char array as a string in various places.
*/
struct WifiApSettings {
std::string ssid;
std::string password;
bool autoConnect;
int32_t channel;
WifiApSettings(
std::string ssid,
std::string password,
bool autoConnect = true,
int32_t channel = 0
) : ssid(ssid), password(password), autoConnect(autoConnect), channel(channel) {}
WifiApSettings() : ssid(""), password(""), autoConnect(true), channel(0) {}
};
/**
* Check if settings exist for the provided SSID
* @param[in] ssid the access point to look for
* @return true if the settings exist
*/
bool contains(const std::string& ssid);
/**
* Load the settings for the provided SSID
* @param[in] ssid the access point to look for
* @param[out] settings the output settings
* @return true if the settings were loaded successfully
*/
bool load(const std::string& ssid, WifiApSettings& settings);
/**
* Save settings
* @param settings the settings to save
* @return true when the settings were saved successfully
*/
bool save(const WifiApSettings& settings);
/**
* Remove settings that were saved previously.
* @param ssid the name of the SSID for the settings to remove
* @return true when settings were found and removed
*/
bool remove(const std::string& ssid);
} // namespace

View File

@ -1,31 +1,7 @@
#pragma once
#include "WifiGlobals.h"
#include <cstdint>
namespace tt::service::wifi::settings {
/**
* This struct is stored as-is into NVS flash.
*
* The SSID and secret are increased by 1 byte to facilitate string null termination.
* This makes it easier to use the char array as a string in various places.
*/
struct WifiApSettings {
char ssid[TT_WIFI_SSID_LIMIT + 1] = { 0 };
char password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT + 1] = { 0 };
int32_t channel = 0;
bool auto_connect = true;
};
bool contains(const char* ssid);
bool load(const char* ssid, WifiApSettings* settings);
bool save(const WifiApSettings* settings);
bool remove(const char* ssid);
void setEnableOnBoot(bool enable);
bool shouldEnableOnBoot();

View File

@ -6,23 +6,21 @@ namespace tt::app {
class AppInstancePaths final : public Paths {
private:
const AppManifest& manifest;
public:
explicit AppInstancePaths(const AppManifest& manifest) : manifest(manifest) {}
~AppInstancePaths() final = default;
~AppInstancePaths() override = 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;
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

@ -20,7 +20,7 @@ class View {
void showActionsForFile();
void viewFile(const std::string&path, const std::string&filename);
void createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry);
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
void onNavigate();
public:

View File

@ -1,10 +1,10 @@
#pragma once
#include "Tactility/service/wifi/WifiSettings.h"
#include <Tactility/service/wifi/WifiApSettings.h>
namespace tt::app::wificonnect {
typedef void (*OnConnectSsid)(const service::wifi::settings::WifiApSettings* settings, bool store, void* context);
typedef void (*OnConnectSsid)(const service::wifi::settings::WifiApSettings& settings, bool store, void* context);
typedef struct {
OnConnectSsid onConnectSsid;

View File

@ -1,18 +1,13 @@
#pragma once
#include <Tactility/Mutex.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/service/wifi/WifiSettings.h>
#include <Tactility/service/wifi/WifiApSettings.h>
namespace tt::app::wificonnect {
class State {
Mutex lock;
service::wifi::settings::WifiApSettings apSettings = {
.ssid = { 0 },
.password = { 0 },
.auto_connect = false
};
service::wifi::settings::WifiApSettings apSettings;
bool connectionError = false;
bool connecting = false;
public:
@ -20,7 +15,7 @@ public:
void setConnectionError(bool error);
bool hasConnectionError() const;
void setApSettings(const service::wifi::settings::WifiApSettings* newSettings);
void setApSettings(const service::wifi::settings::WifiApSettings& newSettings);
void setConnecting(bool isConnecting);
bool isConnecting() const;

View File

@ -13,8 +13,6 @@ class WifiConnect;
class View {
private:
Bindings* bindings;
State* state;

View File

@ -12,8 +12,6 @@ namespace tt::app::wificonnect {
class WifiConnect : public App {
private:
Mutex mutex;
State state;
Bindings bindings = {

View File

@ -0,0 +1,10 @@
#pragma once
namespace tt::service::wifi {
/**
* Called during boot, this function loads WiFi settings from SD card (when available).
*/
void bootSplashInit();
}

View File

@ -0,0 +1,48 @@
#include "Tactility/BootProperties.h"
#include "Tactility/MountPoints.h"
#include "Tactility/file/PropertiesFile.h"
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include <Tactility/Log.h>
#include <Tactility/file/File.h>
#include <cassert>
#include <format>
#include <string>
#include <vector>
namespace tt {
constexpr auto* TAG = "BootProperties";
constexpr auto* PROPERTIES_FILE_FORMAT = "{}/boot.properties";
constexpr auto* PROPERTIES_KEY_LAUNCHER_APP_ID = "launcherAppId";
constexpr auto* PROPERTIES_KEY_AUTO_START_APP_ID = "autoStartAppId";
static std::string getPropertiesFilePath() {
const auto sdcards = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
for (auto& sdcard : sdcards) {
std::string path = std::format(PROPERTIES_FILE_FORMAT, sdcard->getMountPath());
if (file::isFile(path)) {
return path;
}
}
return std::format(PROPERTIES_FILE_FORMAT, file::MOUNT_POINT_DATA);
}
bool loadBootProperties(BootProperties& properties) {
const std::string path = getPropertiesFilePath();
if (!file::loadPropertiesFile(path, [&properties](auto& key, auto& value) {
if (key == PROPERTIES_KEY_AUTO_START_APP_ID) {
properties.autoStartAppId = value;
} else if (key == PROPERTIES_KEY_LAUNCHER_APP_ID) {
properties.launcherAppId = value;
}
})) {
TT_LOG_E(TAG, "Failed to load %s", path.c_str());
return false;
}
return !properties.launcherAppId.empty();
}
}

View File

@ -0,0 +1,55 @@
#include "Tactility/MountPoints.h"
#include "Tactility/hal/Device.h"
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include <Tactility/file/File.h>
#include <cstring>
#include <vector>
#include <dirent.h>
namespace tt::file {
std::vector<dirent> getMountPoints() {
std::vector<dirent> dir_entries;
dir_entries.clear();
// System partition
auto system_dirent = dirent{
.d_ino = 0,
.d_type = TT_DT_DIR,
.d_name = { 0 }
};
strcpy(system_dirent.d_name, SYSTEM_PARTITION_NAME);
dir_entries.push_back(system_dirent);
// Data partition
auto data_dirent = dirent{
.d_ino = 1,
.d_type = TT_DT_DIR,
.d_name = { 0 }
};
strcpy(data_dirent.d_name, DATA_PARTITION_NAME);
dir_entries.push_back(data_dirent);
// SD card partitions
auto sdcards = tt::hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
for (auto& sdcard : sdcards) {
auto state = sdcard->getState();
if (state == hal::sdcard::SdCardDevice::State::Mounted) {
auto mount_name = sdcard->getMountPath().substr(1);
auto dir_entry = dirent {
.d_ino = 2,
.d_type = TT_DT_DIR,
.d_name = { 0 }
};
assert(mount_name.length() < sizeof(dirent::d_name));
strcpy(dir_entry.d_name, mount_name.c_str());
dir_entries.push_back(dir_entry);
}
}
return dir_entries;
}
}

View File

@ -1,11 +1,11 @@
#include "Tactility/Tactility.h"
#include "Tactility/app/ManifestRegistry.h"
#include "Tactility/app/AppRegistration.h"
#include "Tactility/lvgl/LvglPrivate.h"
#include "Tactility/service/ServiceManifest.h"
#include <Tactility/TactilityHeadless.h>
#include <Tactility/service/ServiceRegistry.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/loader/Loader.h>
namespace tt {
@ -54,7 +54,6 @@ namespace app {
namespace serialconsole { extern const AppManifest manifest; }
namespace settings { extern const AppManifest manifest; }
namespace systeminfo { extern const AppManifest manifest; }
namespace textviewer { extern const AppManifest manifest; }
namespace timedatesettings { extern const AppManifest manifest; }
namespace timezone { extern const AppManifest manifest; }
namespace usbsettings { extern const AppManifest manifest; }
@ -96,7 +95,6 @@ static void registerSystemApps() {
addApp(app::settings::manifest);
addApp(app::selectiondialog::manifest);
addApp(app::systeminfo::manifest);
addApp(app::textviewer::manifest);
addApp(app::timedatesettings::manifest);
addApp(app::timezone::manifest);
addApp(app::usbsettings::manifest);

View File

@ -3,7 +3,7 @@
#include "Tactility/hal/Hal_i.h"
#include "Tactility/network/NtpPrivate.h"
#include "Tactility/service/ServiceManifest.h"
#include "Tactility/service/ServiceRegistry.h"
#include "Tactility/service/ServiceRegistration.h"
#include <Tactility/Dispatcher.h>
#include <Tactility/time/TimePrivate.h>

View File

@ -1,6 +1,6 @@
#include "Tactility/app/AppInstancePaths.h"
#include <Tactility/Partitions.h>
#include <Tactility/MountPoints.h>
#define LVGL_PATH_PREFIX std::string("A:/")
#ifdef ESP_PLATFORM
@ -12,38 +12,38 @@
namespace tt::app {
std::string AppInstancePaths::getDataDirectory() const {
return PARTITION_PREFIX + DATA_PARTITION_NAME + "/app/" + manifest.id;
return PARTITION_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id;
}
std::string AppInstancePaths::getDataDirectoryLvgl() const {
return LVGL_PATH_PREFIX + DATA_PARTITION_NAME + "/app/" + manifest.id;
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 + DATA_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
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 + DATA_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
return LVGL_PATH_PREFIX + file::DATA_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
}
std::string AppInstancePaths::getSystemDirectory() const {
return PARTITION_PREFIX + SYSTEM_PARTITION_NAME + "/app/" + manifest.id;
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id;
}
std::string AppInstancePaths::getSystemDirectoryLvgl() const {
return LVGL_PATH_PREFIX + SYSTEM_PARTITION_NAME + "/app/" + manifest.id;
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 + SYSTEM_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
}
std::string AppInstancePaths::getSystemPathLvgl(const std::string& childPath) const {
return LVGL_PATH_PREFIX + SYSTEM_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
return LVGL_PATH_PREFIX + file::SYSTEM_PARTITION_NAME + "/app/" + manifest.id + '/' + childPath;
}
}

View File

@ -1,9 +1,10 @@
#include "Tactility/app/ManifestRegistry.h"
#include "Tactility/app/AppRegistration.h"
#include "Tactility/app/AppManifest.h"
#include <Tactility/Mutex.h>
#include <unordered_map>
#include <Tactility/file/File.h>
#define TAG "app"

View File

@ -2,9 +2,9 @@
#include "Tactility/app/ElfApp.h"
#include "Tactility/file/File.h"
#include "Tactility/file/FileLock.h"
#include "Tactility/service/loader/Loader.h"
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include <Tactility/Log.h>
#include <Tactility/StringUtils.h>
@ -15,7 +15,7 @@
namespace tt::app {
#define TAG "elf_app"
constexpr auto* TAG = "ElfApp";
struct ElfManifest {
/** The user-readable name of the app. Used in UI. */
@ -33,6 +33,7 @@ struct ElfManifest {
static size_t elfManifestSetCount = 0;
static ElfManifest elfManifest;
static std::shared_ptr<Lock> elfManifestLock = std::make_shared<Mutex>();
class ElfApp : public App {
@ -48,7 +49,7 @@ class ElfApp : public App {
assert(elfFileData == nullptr);
size_t size = 0;
hal::sdcard::withSdCardLock<void>(filePath, [this, &size](){
file::withLock<void>(filePath, [this, &size]{
elfFileData = file::readBinary(filePath, size);
});
@ -56,25 +57,30 @@ class ElfApp : public App {
return false;
}
if (esp_elf_init(&elf) < 0) {
if (esp_elf_init(&elf) != ESP_OK) {
TT_LOG_E(TAG, "Failed to initialize");
shouldCleanupElf = true;
elfFileData = nullptr;
return false;
}
if (esp_elf_relocate(&elf, elfFileData.get()) < 0) {
if (esp_elf_relocate(&elf, elfFileData.get()) != ESP_OK) {
TT_LOG_E(TAG, "Failed to load executable");
esp_elf_deinit(&elf);
elfFileData = nullptr;
return false;
}
int argc = 0;
char* argv[] = {};
if (esp_elf_request(&elf, 0, argc, argv) < 0) {
if (esp_elf_request(&elf, 0, argc, argv) != ESP_OK) {
TT_LOG_W(TAG, "Executable returned error code");
esp_elf_deinit(&elf);
elfFileData = nullptr;
return false;
}
shouldCleanupElf = true;
return true;
}
@ -95,10 +101,16 @@ public:
explicit ElfApp(std::string filePath) : filePath(std::move(filePath)) {}
void onCreate(AppContext& appContext) override {
// Because we use global variables, we have to ensure that we are not starting 2 apps in parallel
// We use a ScopedLock so we don't have to safeguard all branches
auto lock = elfManifestLock->asScopedLock();
lock.lock();
auto initial_count = elfManifestSetCount;
if (startElf()) {
if (elfManifestSetCount > initial_count) {
manifest = std::make_unique<ElfManifest>(elfManifest);
lock.unlock();
if (manifest->createData != nullptr) {
data = manifest->createData();
@ -181,7 +193,7 @@ void registerElfApp(const std::string& filePath) {
if (findAppById(filePath) == nullptr) {
auto manifest = AppManifest {
.id = getElfAppId(filePath),
.name = tt::string::removeFileExtension(tt::string::getLastPathSegment(filePath)),
.name = string::removeFileExtension(string::getLastPathSegment(filePath)),
.type = Type::User,
.location = Location::external(filePath)
};

View File

@ -1,4 +1,4 @@
#include "Tactility/app/ManifestRegistry.h"
#include "Tactility/app/AppRegistration.h"
#include "Tactility/service/loader/Loader.h"
#include "Tactility/lvgl/Toolbar.h"

View File

@ -11,6 +11,8 @@
#include <Tactility/kernel/SystemEvents.h>
#include <lvgl.h>
#include <Tactility/BootProperties.h>
#include <Tactility/CpuAffinity.h>
#ifdef ESP_PLATFORM
#include "Tactility/app/crashdiagnostics/CrashDiagnostics.h"
@ -24,26 +26,25 @@
namespace tt::app::boot {
static std::shared_ptr<tt::hal::display::DisplayDevice> getHalDisplay() {
static std::shared_ptr<hal::display::DisplayDevice> getHalDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
}
class BootApp : public App {
private:
Thread thread = Thread(
"boot",
4096,
[] { return bootThreadCallback(); },
getCpuAffinityConfiguration().system
);
Thread thread = Thread("boot", 4096, [this]() { return bootThreadCallback(); });
int32_t bootThreadCallback() {
TickType_t start_time = kernel::getTicks();
kernel::publishSystemEvent(kernel::SystemEvent::BootSplash);
auto hal_display = getHalDisplay();
static void setupDisplay() {
const auto hal_display = getHalDisplay();
assert(hal_display != nullptr);
if (hal_display->supportsBacklightDuty()) {
uint8_t backlight_duty = 200;
app::display::getBacklightDuty(backlight_duty);
display::getBacklightDuty(backlight_duty);
TT_LOG_I(TAG, "backlight %du", backlight_duty);
hal_display->setBacklightDuty(backlight_duty);
} else {
@ -52,27 +53,45 @@ private:
if (hal_display->getGammaCurveCount() > 0) {
uint8_t gamma_curve;
if (app::display::getGammaCurve(gamma_curve)) {
if (display::getGammaCurve(gamma_curve)) {
hal_display->setGammaCurve(gamma_curve);
TT_LOG_I(TAG, "gamma %du", gamma_curve);
}
}
}
if (hal::usb::isUsbBootMode()) {
TT_LOG_I(TAG, "Rebooting into mass storage device mode");
hal::usb::resetUsbBootMode();
hal::usb::startMassStorageWithSdmmc();
} else {
static bool setupUsbBootMode() {
if (!hal::usb::isUsbBootMode()) {
return false;
}
TT_LOG_I(TAG, "Rebooting into mass storage device mode");
hal::usb::resetUsbBootMode();
hal::usb::startMassStorageWithSdmmc();
return true;
}
static void waitForMinimalSplashDuration(TickType_t startTime) {
const auto end_time = kernel::getTicks();
const auto ticks_passed = end_time - startTime;
constexpr auto minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS);
if (minimum_ticks > ticks_passed) {
kernel::delayTicks(minimum_ticks - ticks_passed);
}
}
static int32_t bootThreadCallback() {
const auto start_time = kernel::getTicks();
kernel::publishSystemEvent(kernel::SystemEvent::BootSplash);
setupDisplay();
if (!setupUsbBootMode()) {
initFromBootApp();
TickType_t end_time = tt::kernel::getTicks();
TickType_t ticks_passed = end_time - start_time;
TickType_t minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS);
if (minimum_ticks > ticks_passed) {
kernel::delayTicks(minimum_ticks - ticks_passed);
}
tt::service::loader::stopApp();
waitForMinimalSplashDuration(start_time);
service::loader::stopApp();
startNextApp();
}
@ -81,16 +100,20 @@ private:
static void startNextApp() {
#ifdef ESP_PLATFORM
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_PANIC) {
app::crashdiagnostics::start();
if (esp_reset_reason() == ESP_RST_PANIC) {
crashdiagnostics::start();
return;
}
#endif
auto* config = tt::getConfiguration();
assert(!config->launcherAppId.empty());
tt::service::loader::startApp(config->launcherAppId);
BootProperties boot_properties;
if (!loadBootProperties(boot_properties) || boot_properties.launcherAppId.empty()) {
TT_LOG_E(TAG, "Launcher not configured");
stop();
return;
}
service::loader::startApp(boot_properties.launcherAppId);
}
public:
@ -99,9 +122,9 @@ public:
auto* image = lv_image_create(parent);
lv_obj_set_size(image, LV_PCT(100), LV_PCT(100));
auto paths = app.getPaths();
const auto paths = app.getPaths();
const char* logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo.png";
auto logo_path = paths->getSystemPathLvgl(logo);
const auto logo_path = paths->getSystemPathLvgl(logo);
TT_LOG_I(TAG, "%s", logo_path.c_str());
lv_image_set_src(image, logo_path.c_str());

View File

@ -9,7 +9,7 @@
namespace tt::app::filebrowser {
#define TAG "filebrowser_app"
constexpr auto* TAG = "FileBrowser";
extern const AppManifest manifest;

View File

@ -3,7 +3,7 @@
#include <Tactility/file/File.h>
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include <Tactility/Log.h>
#include <Tactility/Partitions.h>
#include <Tactility/MountPoints.h>
#include <Tactility/kernel/Kernel.h>
#include <cstring>
@ -11,10 +11,10 @@
#include <vector>
#include <dirent.h>
#define TAG "filebrowser_app"
namespace tt::app::filebrowser {
constexpr auto* TAG = "FileBrowser";
State::State() {
if (kernel::getPlatform() == kernel::PlatformSimulator) {
char cwd[PATH_MAX];
@ -43,46 +43,20 @@ bool State::setEntriesForPath(const std::string& path) {
TT_LOG_I(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str());
/**
* ESP32 does not have a root directory, so we have to create it manually.
* We'll add the NVS Flash partitions and the binding for the sdcard.
* On PC, the root entry point ("/") is a folder.
* On ESP32, the root entry point contains the various mount points.
*/
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/");
if (show_custom_root) {
bool get_mount_points = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/");
if (get_mount_points) {
TT_LOG_I(TAG, "Setting custom root");
dir_entries.clear();
dir_entries.push_back(dirent{
.d_ino = 0,
.d_type = file::TT_DT_DIR,
.d_name = SYSTEM_PARTITION_NAME
});
dir_entries.push_back(dirent{
.d_ino = 1,
.d_type = file::TT_DT_DIR,
.d_name = DATA_PARTITION_NAME
});
auto sdcards = tt::hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
for (auto& sdcard : sdcards) {
auto state = sdcard->getState();
if (state == hal::sdcard::SdCardDevice::State::Mounted) {
auto mount_name = sdcard->getMountPath().substr(1);
auto dir_entry = dirent {
.d_ino = 2,
.d_type = file::TT_DT_DIR,
.d_name = { 0 }
};
assert(mount_name.length() < sizeof(dirent::d_name));
strcpy(dir_entry.d_name, mount_name.c_str());
dir_entries.push_back(dir_entry);
}
}
dir_entries = file::getMountPoints();
current_path = path;
selected_child_entry = "";
action = ActionNone;
return true;
} else {
dir_entries.clear();
// TODO: file Lock
int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType);
if (count >= 0) {
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);

View File

@ -3,7 +3,7 @@
namespace tt::app::filebrowser {
#define TAG "filebrowser_app"
constexpr auto* TAG = "FileBrowser";
bool isSupportedExecutableFile(const std::string& filename) {
#ifdef ESP_PLATFORM

View File

@ -4,13 +4,14 @@
#include "Tactility/app/alertdialog/AlertDialog.h"
#include "Tactility/app/imageviewer/ImageViewer.h"
#include "Tactility/app/inputdialog/InputDialog.h"
#include "Tactility/app/textviewer/TextViewer.h"
#include "Tactility/app/notes/Notes.h"
#include "Tactility/app/ElfApp.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/lvgl/LvglSync.h"
#include <Tactility/Tactility.h>
#include "Tactility/file/File.h"
#include <Tactility/file/File.h>
#include <Tactility/Log.h>
#include <Tactility/StringUtils.h>
#include <cstring>
@ -20,10 +21,10 @@
#include "Tactility/service/loader/Loader.h"
#endif
#define TAG "filebrowser_app"
namespace tt::app::filebrowser {
constexpr auto* TAG = "FileBrowser";
// region Callbacks
static void dirEntryListScrollBeginCallback(lv_event_t* event) {
@ -95,10 +96,10 @@ void View::viewFile(const std::string& path, const std::string& filename) {
imageviewer::start(processed_filepath);
} else if (isSupportedTextFile(filename)) {
if (kernel::getPlatform() == kernel::PlatformEsp) {
textviewer::start(processed_filepath);
notes::start(processed_filepath);
} else {
// Remove forward slash, because we need a relative path
textviewer::start(processed_filepath.substr(1));
notes::start(processed_filepath.substr(1));
}
} else {
TT_LOG_W(TAG, "opening files of this type is not supported");
@ -163,7 +164,6 @@ void View::onDirEntryLongPressed(int32_t index) {
}
}
void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
tt_check(list);
const char* symbol;

View File

@ -10,7 +10,7 @@
namespace tt::app::fileselection {
#define TAG "fileselection_app"
constexpr auto* TAG = "FileSelection";
extern const AppManifest manifest;

View File

@ -3,7 +3,7 @@
#include <Tactility/file/File.h>
#include "Tactility/hal/sdcard/SdCardDevice.h"
#include <Tactility/Log.h>
#include <Tactility/Partitions.h>
#include <Tactility/MountPoints.h>
#include <Tactility/kernel/Kernel.h>
#include <cstring>
@ -11,10 +11,10 @@
#include <vector>
#include <dirent.h>
#define TAG "fileselection_app"
namespace tt::app::fileselection {
constexpr auto* TAG = "FileSelection";
State::State() {
if (kernel::getPlatform() == kernel::PlatformSimulator) {
char cwd[PATH_MAX];
@ -49,34 +49,7 @@ bool State::setEntriesForPath(const std::string& path) {
bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/");
if (show_custom_root) {
TT_LOG_I(TAG, "Setting custom root");
dir_entries.clear();
dir_entries.push_back(dirent{
.d_ino = 0,
.d_type = file::TT_DT_DIR,
.d_name = SYSTEM_PARTITION_NAME
});
dir_entries.push_back(dirent{
.d_ino = 1,
.d_type = file::TT_DT_DIR,
.d_name = DATA_PARTITION_NAME
});
auto sdcards = tt::hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
for (auto& sdcard : sdcards) {
auto state = sdcard->getState();
if (state == hal::sdcard::SdCardDevice::State::Mounted) {
auto mount_name = sdcard->getMountPath().substr(1);
auto dir_entry = dirent {
.d_ino = 2,
.d_type = file::TT_DT_DIR,
.d_name = { 0 }
};
assert(mount_name.length() < sizeof(dirent::d_name));
strcpy(dir_entry.d_name, mount_name.c_str());
dir_entries.push_back(dir_entry);
}
}
dir_entries = file::getMountPoints();
current_path = path;
selected_child_entry = "";
return true;

View File

@ -15,10 +15,10 @@
#include "Tactility/service/loader/Loader.h"
#endif
#define TAG "fileselection_app"
namespace tt::app::fileselection {
constexpr auto* TAG = "FileSelection";
// region Callbacks
static void onDirEntryPressedCallback(lv_event_t* event) {

View File

@ -148,7 +148,7 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) {
// Add the GPIO number after the last item on a row
auto* postfix = lv_label_create(row_wrapper);
lv_label_set_text_fmt(postfix, "%02d", i);
lv_obj_set_pos(postfix, (int32_t)((column+1) * x_spacing + offset_from_left_label), 0);
lv_obj_set_pos(postfix, (column + 1) * x_spacing + offset_from_left_label, 0);
// Add a new row wrapper underneath the last one
auto* new_row_wrapper = createGpioRowWrapper(wrapper);

View File

@ -24,8 +24,6 @@ extern const AppManifest manifest;
class I2cScannerApp : public App {
private:
// Core
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::unique_ptr<Timer> scanTimer = nullptr;
@ -286,7 +284,7 @@ void I2cScannerApp::startScanning() {
lv_obj_clean(scanListWidget);
scanState = ScanStateScanning;
scanTimer = std::make_unique<Timer>(Timer::Type::Once, [](){
scanTimer = std::make_unique<Timer>(Timer::Type::Once, []{
onScanTimerCallback();
});
scanTimer->start(10);

View File

@ -1,11 +1,11 @@
#include "Tactility/app/AppContext.h"
#include "Tactility/app/ManifestRegistry.h"
#include "Tactility/app/AppRegistration.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/Check.h>
#include <Tactility/Tactility.h>
#include <lvgl.h>
#include <Tactility/BootProperties.h>
#define TAG "launcher"
@ -54,10 +54,10 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char
class LauncherApp : public App {
void onCreate(TT_UNUSED AppContext& app) override {
auto* config = getConfiguration();
if (!config->autoStartAppId.empty()) {
TT_LOG_I(TAG, "auto-starting %s", config->autoStartAppId.c_str());
service::loader::startApp(config->autoStartAppId);
BootProperties boot_properties;
if (loadBootProperties(boot_properties) && !boot_properties.autoStartAppId.empty()) {
TT_LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
service::loader::startApp(boot_properties.autoStartAppId);
}
}

View File

@ -1,17 +1,19 @@
#include <Tactility/app/AppManifest.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/Keyboard.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/Assets.h>
#include <lvgl.h>
#include "Tactility/app/AppManifest.h"
#include "Tactility/app/fileselection/FileSelection.h"
#include "Tactility/file/FileLock.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/service/loader/Loader.h"
#include "Tactility/Assets.h"
#include <Tactility/app/fileselection/FileSelection.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/file/File.h>
#include <lvgl.h>
namespace tt::app::notes {
constexpr const char* TAG = "Notes";
constexpr auto* TAG = "Notes";
constexpr auto* NOTES_FILE_ARGUMENT = "file";
class NotesApp : public App {
@ -81,7 +83,7 @@ class NotesApp : public App {
void openFile(const std::string& path) {
// We might be reading from the SD card, which could share a SPI bus with other devices (display)
hal::sdcard::withSdCardLock<void>(path, [this, path]() {
file::withLock<void>(path, [this, path] {
auto data = file::readString(path);
if (data != nullptr) {
auto lock = lvgl::getSyncLock()->asScopedLock();
@ -96,7 +98,7 @@ class NotesApp : public App {
bool saveFile(const std::string& path) {
// We might be writing to SD card, which could share a SPI bus with other devices (display)
return hal::sdcard::withSdCardLock<bool>(path, [this, path]() {
return file::withLock<bool>(path, [this, path] {
if (file::writeString(path, saveBuffer.c_str())) {
TT_LOG_I(TAG, "Saved to %s", path.c_str());
filePath = path;
@ -109,11 +111,20 @@ class NotesApp : public App {
#pragma endregion Open_Events_Functions
void onCreate(AppContext& appContext) override {
auto parameters = appContext.getParameters();
std::string file_path;
if (parameters != nullptr && parameters->optString(NOTES_FILE_ARGUMENT, file_path)) {
if (!file_path.empty()) {
filePath = file_path;
}
}
}
void onShow(AppContext& context, lv_obj_t* parent) override {
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_t* toolbar = tt::lvgl::toolbar_create(parent, context);
lv_obj_t* toolbar = lvgl::toolbar_create(parent, context);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
uiDropDownMenu = lv_dropdown_create(toolbar);
@ -168,9 +179,8 @@ class NotesApp : public App {
lv_label_set_text(uiCurrentFileName, "Untitled");
lv_obj_align(uiCurrentFileName, LV_ALIGN_CENTER, 0, 0);
//TODO: Move this to SD Card?
if (!file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) {
TT_LOG_E(TAG, "Failed to find or create path %s", context.getPaths()->getDataDirectory().c_str());
if (!filePath.empty()) {
openFile(filePath);
}
}
@ -202,4 +212,9 @@ extern const AppManifest manifest = {
.createApp = create<NotesApp>
};
void start(const std::string& filePath) {
auto parameters = std::make_shared<Bundle>();
parameters->putString(NOTES_FILE_ARGUMENT, filePath);
service::loader::startApp(manifest.id, parameters);
}
} // namespace tt::app::notes

View File

@ -1,4 +1,4 @@
#include "Tactility/app/ManifestRegistry.h"
#include "Tactility/app/AppRegistration.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"

View File

@ -1,61 +0,0 @@
#include "Tactility/lvgl/LabelUtils.h"
#include "Tactility/lvgl/Style.h"
#include "Tactility/lvgl/Toolbar.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/TactilityCore.h>
#include <lvgl.h>
#define TAG "text_viewer"
#define TEXT_VIEWER_FILE_ARGUMENT "file"
namespace tt::app::textviewer {
class TextViewerApp : public App {
void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lvgl::toolbar_create(parent, app);
auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));
lv_obj_set_flex_grow(wrapper, 1);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(wrapper, 0, 0);
lvgl::obj_set_style_bg_invisible(wrapper);
auto* label = lv_label_create(wrapper);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
auto parameters = app.getParameters();
tt_check(parameters != nullptr, "Parameters missing");
bool success = false;
std::string file_argument;
if (parameters->optString(TEXT_VIEWER_FILE_ARGUMENT, file_argument)) {
TT_LOG_I(TAG, "Opening %s", file_argument.c_str());
if (lvgl::label_set_text_file(label, file_argument.c_str())) {
success = true;
}
}
if (!success) {
lv_label_set_text_fmt(label, "Failed to load %s", file_argument.c_str());
}
}
};
extern const AppManifest manifest = {
.id = "TextViewer",
.name = "Text Viewer",
.type = Type::Hidden,
.createApp = create<TextViewerApp>
};
void start(const std::string& file) {
auto parameters = std::make_shared<Bundle>();
parameters->putString(TEXT_VIEWER_FILE_ARGUMENT, file);
service::loader::startApp(manifest.id, parameters);
}
} // namespace

View File

@ -5,7 +5,7 @@
#include "Tactility/lvgl/LvglSync.h"
#include "Tactility/service/loader/Loader.h"
#include <Tactility/Partitions.h>
#include <Tactility/MountPoints.h>
#include <Tactility/StringUtils.h>
#include <Tactility/Timer.h>
@ -14,7 +14,7 @@
namespace tt::app::timezone {
#define TAG "timezone_select"
constexpr auto* TAG = "TimeZone";
#define RESULT_BUNDLE_CODE_INDEX "code"
#define RESULT_BUNDLE_NAME_INDEX "name"
@ -62,8 +62,7 @@ void setResultCode(Bundle& bundle, const std::string& code) {
// endregion
class TimeZoneApp : public App {
class TimeZoneApp final : public App {
Mutex mutex;
std::vector<TimeZoneEntry> entries;
@ -123,7 +122,7 @@ class TimeZoneApp : public App {
}
void readTimeZones(std::string filter) {
auto path = std::string(MOUNT_POINT_SYSTEM) + "/timezones.csv";
auto path = std::string(file::MOUNT_POINT_SYSTEM) + "/timezones.csv";
auto* file = fopen(path.c_str(), "rb");
if (file == nullptr) {
TT_LOG_E(TAG, "Failed to open %s", path.c_str());
@ -136,7 +135,7 @@ class TimeZoneApp : public App {
std::vector<TimeZoneEntry> new_entries;
while (fgets(line, 96, file)) {
if (parseEntry(line, name, code)) {
if (tt::string::lowercase(name).find(filter) != std::string::npos) {
if (string::lowercase(name).find(filter) != std::string::npos) {
count++;
new_entries.push_back({.name = name, .code = code});
@ -165,7 +164,7 @@ class TimeZoneApp : public App {
void updateList() {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
std::string filter = tt::string::lowercase(std::string(lv_textarea_get_text(filterTextareaWidget)));
std::string filter = string::lowercase(std::string(lv_textarea_get_text(filterTextareaWidget)));
readTimeZones(filter);
lvgl::unlock();
} else {
@ -227,7 +226,7 @@ public:
}
void onCreate(AppContext& app) override {
updateTimer = std::make_unique<Timer>(Timer::Type::Once, []() { updateTimerCallback(); });
updateTimer = std::make_unique<Timer>(Timer::Type::Once, [] { updateTimerCallback(); });
}
};

View File

@ -44,10 +44,10 @@ static void onToggleAutoConnect(lv_event_t* event) {
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
std::string ssid = parameters->getString("ssid");
service::wifi::settings::WifiApSettings settings {};
if (service::wifi::settings::load(ssid.c_str(), &settings)) {
settings.auto_connect = is_on;
if (!service::wifi::settings::save(&settings)) {
service::wifi::settings::WifiApSettings settings;
if (service::wifi::settings::load(ssid.c_str(), settings)) {
settings.autoConnect = is_on;
if (!service::wifi::settings::save(settings)) {
TT_LOG_E(TAG, "Failed to save settings");
}
} else {
@ -98,9 +98,9 @@ class WifiApSettings : public App {
lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0);
lv_label_set_text(forget_button_label, "Forget");
service::wifi::settings::WifiApSettings settings {};
if (service::wifi::settings::load(ssid.c_str(), &settings)) {
if (settings.auto_connect) {
service::wifi::settings::WifiApSettings settings;
if (service::wifi::settings::load(ssid.c_str(), settings)) {
if (settings.autoConnect) {
lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED);
} else {
lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED);

View File

@ -17,9 +17,9 @@ bool State::hasConnectionError() const {
return result;
}
void State::setApSettings(const service::wifi::settings::WifiApSettings* newSettings) {
void State::setApSettings(const service::wifi::settings::WifiApSettings& newSettings) {
lock.lock();
memcpy(&this->apSettings, newSettings, sizeof(service::wifi::settings::WifiApSettings));
this->apSettings = newSettings;
lock.unlock();
}

View File

@ -5,10 +5,11 @@
#include "Tactility/lvgl/Spinner.h"
#include <Tactility/TactilityCore.h>
#include <Tactility/service/wifi/WifiSettings.h>
#include <lvgl.h>
#include <cstring>
#include <Tactility/service/wifi/WifiApSettings.h>
#include <Tactility/service/wifi/WifiGlobals.h>
namespace tt::app::wificonnect {
@ -50,14 +51,14 @@ static void onConnect(TT_UNUSED lv_event_t* event) {
view.setLoading(true);
service::wifi::settings::WifiApSettings settings;
strcpy((char*)settings.password, password);
strcpy((char*)settings.ssid, ssid);
settings.password = password;
settings.ssid = ssid;
settings.channel = 0;
settings.auto_connect = TT_WIFI_AUTO_CONNECT; // No UI yet, so use global setting:w
settings.autoConnect = TT_WIFI_AUTO_CONNECT; // No UI yet, so use global setting:w
auto* bindings = &wifi->getBindings();
bindings->onConnectSsid(
&settings,
settings,
store,
bindings->onConnectSsidContext
);

View File

@ -6,7 +6,6 @@
#include "Tactility/lvgl/LvglSync.h"
namespace tt::app::wificonnect {
#define TAG "wifi_connect"
#define WIFI_CONNECT_PARAM_SSID "ssid" // String
#define WIFI_CONNECT_PARAM_PASSWORD "password" // String
@ -37,7 +36,7 @@ static void eventCallback(const void* message, void* context) {
wifi->requestViewUpdate();
}
static void onConnect(const service::wifi::settings::WifiApSettings* ap_settings, bool remember, TT_UNUSED void* parameter) {
static void onConnect(const service::wifi::settings::WifiApSettings& ap_settings, bool remember, TT_UNUSED void* parameter) {
auto* wifi = static_cast<WifiConnect*>(parameter);
wifi->getState().setApSettings(ap_settings);
wifi->getState().setConnecting(true);

View File

@ -11,6 +11,7 @@
#include <format>
#include <string>
#include <set>
#include <Tactility/service/wifi/WifiSettings.h>
namespace tt::app::wifimanage {

View File

@ -16,9 +16,9 @@ extern const AppManifest manifest;
static void onConnect(const char* ssid) {
service::wifi::settings::WifiApSettings settings;
if (service::wifi::settings::load(ssid, &settings)) {
if (service::wifi::settings::load(ssid, settings)) {
TT_LOG_I(TAG, "Connecting with known credentials");
service::wifi::connect(&settings, false);
service::wifi::connect(settings, false);
} else {
TT_LOG_I(TAG, "Starting connection dialog");
wificonnect::start(ssid);

View File

@ -0,0 +1,24 @@
#include "Tactility/file/FileLock.h"
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <Tactility/Mutex.h>
namespace tt::file {
class NoLock : public Lock {
bool lock(TickType_t timeout) const override { return true; }
bool unlock() const override { return true; }
};
static std::shared_ptr<Lock> noLock = std::make_shared<NoLock>();
std::shared_ptr<Lock> getLock(const std::string& path) {
auto sdcard_lock = hal::sdcard::findSdCardLock(path);
if (sdcard_lock != nullptr) {
return sdcard_lock;
} else {
return noLock;
}
}
}

View File

@ -6,7 +6,7 @@
namespace tt::file {
constexpr const char* TAG = "ObjectFileReader";
constexpr auto* TAG = "ObjectFileReader";
bool ObjectFileReader::open() {
auto opening_file = std::unique_ptr<FILE, FileCloser>(fopen(filePath.c_str(), "r"));

View File

@ -1,4 +1,4 @@
#include "Tactility/file/ObjectFile.h"
#include "../../Include/Tactility/file/ObjectFile.h"
#include "Tactility/file/ObjectFilePrivate.h"
#include <cstring>
@ -7,7 +7,7 @@
namespace tt::file {
constexpr const char* TAG = "ObjectFileWriter";
constexpr auto* TAG = "ObjectFileWriter";
bool ObjectFileWriter::open() {
bool edit_existing = append && access(filePath.c_str(), F_OK) == 0;

View File

@ -0,0 +1,74 @@
#include "Tactility/file/PropertiesFile.h"
#include <Tactility/StringUtils.h>
#include <Tactility/file/File.h>
#include <Tactility/file/FileLock.h>
namespace tt::file {
static auto TAG = "PropertiesFile";
bool getKeyValuePair(const std::string& input, std::string& key, std::string& value) {
auto index = input.find('=');
if (index == std::string::npos) {
return false;
}
key = input.substr(0, index);
value = input.substr(index + 1);
return true;
}
bool loadPropertiesFile(const std::string& filePath, std::function<void(const std::string& key, const std::string& value)> callback) {
return file::withLock<bool>(filePath, [&filePath, &callback] {
TT_LOG_I(TAG, "Reading properties file %s", filePath.c_str());
const auto input = readString(filePath);
if (input == nullptr) {
TT_LOG_E(TAG, "Failed to read file contents of %s", filePath.c_str());
return false;
}
const auto* input_start = reinterpret_cast<const char*>(input.get());
const std::string input_string = input_start;
uint16_t line_count = 0;
string::split(input_string, "\n", [&line_count, &filePath, &callback](auto token) {
line_count++;
std::string key, value;
auto trimmed_token = string::trim(token, " \t");
if (!trimmed_token.starts_with("#")) {
if (getKeyValuePair(token, key, value)) {
std::string trimmed_key = string::trim(key, " \t");
std::string trimmed_value = string::trim(value, " \t");
callback(trimmed_key, trimmed_value);
} else { TT_LOG_E(TAG, "Failed to parse line %d of %s", line_count, filePath.c_str()); }
}
});
return true;
});
}
bool loadPropertiesFile(const std::string& filePath, std::map<std::string, std::string>& outProperties) {
return loadPropertiesFile(filePath, [&outProperties](const std::string& key, const std::string& value) {
outProperties[key] = value;
});
}
bool savePropertiesFile(const std::string& filePath, const std::map<std::string, std::string>& properties) {
return file::withLock<bool>(filePath, [filePath, &properties] {
TT_LOG_I(TAG, "Saving properties file %s", filePath.c_str());
FILE* file = fopen(filePath.c_str(), "w");
if (file == nullptr) {
TT_LOG_E(TAG, "Failed to open %s", filePath.c_str());
return false;
}
for (const auto& [key, value]: properties) { fprintf(file, "%s=%s\n", key.c_str(), value.c_str()); }
fclose(file);
return true;
});
}
}

View File

@ -1,8 +1,8 @@
#include "Tactility/hal/gps/GpsConfiguration.h"
#include "Tactility/service/gps/GpsService.h"
#include "Tactility/file/ObjectFile.h"
#include <Tactility/TactilityCore.h>
#include <Tactility/file/ObjectFile.h>
namespace tt::hal::gps {

View File

@ -14,4 +14,13 @@ std::shared_ptr<SdCardDevice> _Nullable find(const std::string& path) {
return nullptr;
}
std::shared_ptr<Lock> findSdCardLock(const std::string& path) {
auto sdcard = find(path);
if (sdcard != nullptr) {
return sdcard->getLock();
}
return nullptr;
}
}

View File

@ -8,10 +8,10 @@
#include <esp_vfs_fat.h>
#include <sdmmc_cmd.h>
#define TAG "spi_sdcard"
namespace tt::hal::sdcard {
constexpr auto* TAG = "SpiSdCardDevice";
/**
* Before we can initialize the sdcard's SPI communications, we have to set all
* other SPI pins on the board high.
@ -78,7 +78,7 @@ bool SpiSdCardDevice::mountInternal(const std::string& newMountPath) {
esp_err_t result = esp_vfs_fat_sdspi_mount(newMountPath.c_str(), &host, &slot_config, &mount_config, &card);
if (result != ESP_OK) {
if (result != ESP_OK || card == nullptr) {
if (result == ESP_FAIL) {
TT_LOG_E(TAG, "Mounting failed. Ensure the card is formatted with FAT.");
} else {
@ -99,6 +99,7 @@ bool SpiSdCardDevice::mount(const std::string& newMountPath) {
}
if (mountInternal(newMountPath)) {
TT_LOG_I(TAG, "Mounted at %s", newMountPath.c_str());
sdmmc_card_print_info(stdout, card);
return true;
} else {
@ -113,18 +114,19 @@ bool SpiSdCardDevice::unmount() {
return false;
}
if (esp_vfs_fat_sdcard_unmount(mountPath.c_str(), card) == ESP_OK) {
mountPath = "";
card = nullptr;
return true;
} else {
if (esp_vfs_fat_sdcard_unmount(mountPath.c_str(), card) != ESP_OK) {
TT_LOG_E(TAG, "Unmount failed for %s", mountPath.c_str());
return false;
}
TT_LOG_I(TAG, "Unmounted %s", mountPath.c_str());
mountPath = "";
card = nullptr;
return true;
}
// TODO: Refactor to "bool getStatus(Status* status)" method so that it can fail when the lvgl lock fails
SdCardDevice::State SpiSdCardDevice::getState() const {
SdCardDevice::State SpiSdCardDevice::getState(TickType_t timeout) const {
if (card == nullptr) {
return State::Unmounted;
}
@ -134,20 +136,17 @@ SdCardDevice::State SpiSdCardDevice::getState() const {
* Writing and reading to the bus from 2 devices at the same time causes crashes.
* This work-around ensures that this check is only happening when LVGL isn't rendering.
*/
auto lock = getLock().asScopedLock();
bool locked = lock.lock(50); // TODO: Refactor to a more reliable locking mechanism
auto lock = getLock()->asScopedLock();
bool locked = lock.lock(timeout);
if (!locked) {
TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "LVGL");
return State::Unknown;
return State::Timeout;
}
bool result = sdmmc_get_status(card) == ESP_OK;
if (result) {
return State::Mounted;
} else {
if (sdmmc_get_status(card) != ESP_OK) {
return State::Error;
}
return State::Mounted;
}
}

View File

@ -2,23 +2,23 @@
#include <Tactility/lvgl/LvglSync.h>
#include <esp_lvgl_port.h>
#include <Tactility/CpuAffinity.h>
#include <Tactility/Mutex.h>
// LVGL
// The minimum task stack seems to be about 3500, but that crashes the wifi app in some scenarios
// At 8192, it sometimes crashes when wifi-auto enables and is busy connecting and then you open WifiManage
#define TDECK_LVGL_TASK_STACK_DEPTH 9216
#define TAG "lvgl"
auto constexpr TAG = "lvgl";
namespace tt::lvgl {
bool initEspLvglPort() {
TT_LOG_D(TAG, "Port init");
static lv_disp_t* display = nullptr;
const lvgl_port_cfg_t lvgl_cfg = {
.task_priority = static_cast<UBaseType_t>(Thread::Priority::Critical),
.task_stack = TDECK_LVGL_TASK_STACK_DEPTH,
.task_affinity = 1, // -1 = disabled, 0 = core 1, 1 = core 2
.task_affinity = getCpuAffinityConfiguration().graphics,
.task_max_sleep_ms = 500,
.timer_period_ms = 5
};
@ -28,7 +28,7 @@ bool initEspLvglPort() {
return false;
}
tt::lvgl::syncSet(&lvgl_port_lock, &lvgl_port_unlock);
syncSet(&lvgl_port_lock, &lvgl_port_unlock);
return true;
}

View File

@ -1,13 +1,13 @@
#include <Tactility/lvgl/LabelUtils.h>
#include <Tactility/file/File.h>
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include "Tactility/lvgl/LabelUtils.h"
#include "Tactility/file/File.h"
#include "Tactility/file/FileLock.h"
namespace tt::lvgl {
#define TAG "tt_lv_label"
constexpr auto* TAG = "LabelUtils";
bool label_set_text_file(lv_obj_t* label, const char* filepath) {
auto text = hal::sdcard::withSdCardLock<std::unique_ptr<uint8_t[]>>(std::string(filepath), [filepath]() {
auto text = file::withLock<std::unique_ptr<uint8_t[]>>(std::string(filepath), [filepath] {
return file::readString(filepath);
});

View File

@ -13,10 +13,9 @@
#endif
#include <lvgl.h>
#include <Tactility/Tactility.h>
#include <Tactility/TactilityHeadless.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/ServiceRegistry.h>
#include <Tactility/service/ServiceRegistration.h>
namespace tt::lvgl {

View File

@ -1,6 +1,6 @@
#include "Tactility/service/ServiceInstancePaths.h"
#include "Tactility/Partitions.h"
#include "Tactility/MountPoints.h"
#define LVGL_PATH_PREFIX std::string("A:/")
@ -13,38 +13,38 @@
namespace tt::service {
std::string ServiceInstancePaths::getDataDirectory() const {
return PARTITION_PREFIX + DATA_PARTITION_NAME + "/service/" + manifest->id;
return PARTITION_PREFIX + file::DATA_PARTITION_NAME + "/service/" + manifest->id;
}
std::string ServiceInstancePaths::getDataDirectoryLvgl() const {
return LVGL_PATH_PREFIX + DATA_PARTITION_NAME + "/service/" + manifest->id;
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 + DATA_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
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 + DATA_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
return LVGL_PATH_PREFIX + file::DATA_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
}
std::string ServiceInstancePaths::getSystemDirectory() const {
return PARTITION_PREFIX + SYSTEM_PARTITION_NAME + "/service/" + manifest->id;
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/service/" + manifest->id;
}
std::string ServiceInstancePaths::getSystemDirectoryLvgl() const {
return LVGL_PATH_PREFIX + SYSTEM_PARTITION_NAME + "/service/" + manifest->id;
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 + SYSTEM_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
return PARTITION_PREFIX + file::SYSTEM_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
}
std::string ServiceInstancePaths::getSystemPathLvgl(const std::string& childPath) const {
return LVGL_PATH_PREFIX + SYSTEM_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
return LVGL_PATH_PREFIX + file::SYSTEM_PARTITION_NAME + "/service/" + manifest->id + '/' + childPath;
}
}

View File

@ -1,4 +1,4 @@
#include "Tactility/service/ServiceRegistry.h"
#include "Tactility/service/ServiceRegistration.h"
#include "Tactility/service/ServiceInstance.h"
#include "Tactility/service/ServiceManifest.h"
@ -6,8 +6,6 @@
#include <Tactility/Mutex.h>
#include <string>
#include <unordered_map>
#include <Tactility/app/AppInstance.h>
namespace tt::service {
@ -23,8 +21,9 @@ static Mutex manifest_mutex(Mutex::Type::Normal);
static Mutex instance_mutex(Mutex::Type::Normal);
void addService(std::shared_ptr<const ServiceManifest> manifest, bool autoStart) {
assert(manifest != nullptr);
// We'll move the manifest pointer, but we'll need to id later
std::string id = manifest->id;
const auto& id = manifest->id;
TT_LOG_I(TAG, "Adding %s", id.c_str());

View File

@ -6,7 +6,7 @@
#include "Tactility/network/Url.h"
#include "Tactility/TactilityHeadless.h"
#include "Tactility/service/ServiceManifest.h"
#include "Tactility/service/ServiceRegistry.h"
#include "Tactility/service/ServiceRegistration.h"
#include "Tactility/service/wifi/Wifi.h"
#include <cstring>
@ -17,7 +17,7 @@
#include <Tactility/StringUtils.h>
#include <Tactility/app/App.h>
#include <Tactility/app/ElfApp.h>
#include <Tactility/app/ManifestRegistry.h>
#include <Tactility/app/AppRegistration.h>
namespace tt::service::development {

View File

@ -3,7 +3,7 @@
#include "Tactility/service/espnow/EspNowService.h"
#include "Tactility/TactilityHeadless.h"
#include "Tactility/service/ServiceManifest.h"
#include "Tactility/service/ServiceRegistry.h"
#include "Tactility/service/ServiceRegistration.h"
#include "Tactility/service/espnow/EspNowWifi.h"
#include <cstring>
#include <esp_now.h>

View File

@ -1,6 +1,7 @@
#include "Tactility/service/gps/GpsService.h"
#include <Tactility/file/ObjectFile.h>
#include "Tactility/file/ObjectFile.h"
#include <cstring>
#include <unistd.h>

View File

@ -1,6 +1,6 @@
#include "Tactility/service/gps/GpsService.h"
#include "Tactility/service/ServiceManifest.h"
#include "Tactility/service/ServiceRegistry.h"
#include "Tactility/service/ServiceRegistration.h"
#include <Tactility/Log.h>
#include <Tactility/file/File.h>

View File

@ -6,7 +6,7 @@
#include <Tactility/Tactility.h>
#include <Tactility/app/AppInstance.h>
#include <Tactility/service/ServiceRegistry.h>
#include <Tactility/service/ServiceRegistration.h>
namespace tt::service::gui {

View File

@ -1,12 +1,12 @@
#include "Tactility/service/loader/Loader.h"
#include "Tactility/app/AppInstance.h"
#include "Tactility/app/AppManifest.h"
#include "Tactility/app/ManifestRegistry.h"
#include "Tactility/app/AppRegistration.h"
#include <Tactility/RtosCompat.h>
#include <Tactility/DispatcherThread.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistry.h>
#include <Tactility/service/ServiceRegistration.h>
#include <stack>

View File

@ -5,7 +5,7 @@
#include "Tactility/service/screenshot/Screenshot.h"
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServiceRegistry.h>
#include <Tactility/service/ServiceRegistration.h>
#include <memory>

View File

@ -11,6 +11,7 @@
#include <Tactility/TactilityCore.h>
#include <format>
#include <Tactility/CpuAffinity.h>
namespace tt::service::screenshot {
@ -109,10 +110,11 @@ void ScreenshotTask::taskStart() {
thread = new Thread(
"screenshot",
8192,
[this]() {
[this] {
this->taskMain();
return 0;
}
},
getCpuAffinityConfiguration().graphics
);
thread->start();
}

View File

@ -1,6 +1,6 @@
#include "Tactility/service/ServiceContext.h"
#include "Tactility/TactilityHeadless.h"
#include "Tactility/service/ServiceRegistry.h"
#include "Tactility/service/ServiceRegistration.h"
#include <Tactility/Mutex.h>
#include <Tactility/Timer.h>
@ -13,8 +13,6 @@ extern const ServiceManifest manifest;
class SdCardService final : public Service {
private:
Mutex mutex;
std::unique_ptr<Timer> updateTimer;
hal::sdcard::SdCardDevice::State lastState = hal::sdcard::SdCardDevice::State::Unmounted;
@ -28,14 +26,14 @@ private:
}
void update() {
auto sdcard = tt::hal::getConfiguration()->sdcard;
auto sdcard = hal::getConfiguration()->sdcard;
assert(sdcard);
if (lock(50)) {
auto new_state = sdcard->getState();
if (new_state == hal::sdcard::SdCardDevice::State::Error) {
TT_LOG_W(TAG, "Sdcard error - unmounting. Did you eject the card in an unsafe manner?");
TT_LOG_E(TAG, "Sdcard error - unmounting. Did you eject the card in an unsafe manner?");
sdcard->unmount();
}
@ -51,7 +49,7 @@ private:
public:
void onStart(ServiceContext& serviceContext) final {
void onStart(ServiceContext& serviceContext) override {
if (hal::getConfiguration()->sdcard != nullptr) {
auto service = findServiceById<SdCardService>(manifest.id);
updateTimer = std::make_unique<Timer>(Timer::Type::Periodic, [service]() {
@ -64,7 +62,7 @@ public:
}
}
void onStop(ServiceContext& serviceContext) final {
void onStop(ServiceContext& serviceContext) override {
if (updateTimer != nullptr) {
// Stop thread
updateTimer->stop();

View File

@ -9,7 +9,7 @@
#include <Tactility/TactilityHeadless.h>
#include <Tactility/Timer.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServiceRegistry.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/wifi/Wifi.h>
namespace tt::service::statusbar {
@ -81,7 +81,7 @@ static const char* getSdCardStatusIcon(hal::sdcard::SdCardDevice::State state) {
return STATUSBAR_ICON_SDCARD;
case Error:
case Unmounted:
case Unknown:
case Timeout:
return STATUSBAR_ICON_SDCARD_ALERT;
default:
tt_crash("Unhandled SdCard state");
@ -199,8 +199,8 @@ class StatusbarService final : public Service {
void updateSdCardIcon() {
auto sdcard = hal::getConfiguration()->sdcard;
if (sdcard != nullptr) {
auto state = sdcard->getState();
if (state != hal::sdcard::SdCardDevice::State::Unknown) {
auto state = sdcard->getState(50 / portTICK_PERIOD_MS);
if (state != hal::sdcard::SdCardDevice::State::Timeout) {
auto* desired_icon = getSdCardStatusIcon(state);
if (sdcard_last_icon != desired_icon) {
auto icon_path = paths->getSystemPathLvgl(desired_icon);

View File

@ -0,0 +1,190 @@
#include "Tactility/service/wifi/WifiApSettings.h"
#include "Tactility/file/PropertiesFile.h"
#include <Tactility/crypt/Crypt.h>
#include <Tactility/file/File.h>
#include <cstring>
#include <format>
#include <iomanip>
#include <ranges>
#include <sstream>
#include <string>
namespace tt::service::wifi::settings {
constexpr auto* TAG = "WifiApSettings";
constexpr auto* AP_SETTINGS_FORMAT = "/data/service/Wifi/{}.ap.properties";
constexpr auto* AP_PROPERTIES_KEY_SSID = "ssid";
constexpr auto* AP_PROPERTIES_KEY_PASSWORD = "password";
constexpr auto* AP_PROPERTIES_KEY_AUTO_CONNECT = "autoConnect";
constexpr auto* AP_PROPERTIES_KEY_CHANNEL = "channel";
std::string toHexString(const uint8_t *data, int length) {
std::stringstream stream;
stream << std::hex;
for( int i(0) ; i < length; ++i )
stream << std::setw(2) << std::setfill('0') << static_cast<int>(data[i]);
return stream.str();
}
bool readHex(const std::string& input, uint8_t* buffer, int length) {
if (input.size() / 2 != length) {
TT_LOG_E(TAG, "readHex() length mismatch");
return false;
}
char hex[3] = { 0 };
for (int i = 0; i < length; i++) {
hex[0] = input[i * 2];
hex[1] = input[i * 2 + 1];
char* endptr;
buffer[i] = static_cast<uint8_t>(strtoul(hex, &endptr, 16));
}
return true;
}
static std::string getApPropertiesFilePath(const std::string& ssid) {
return std::format(AP_SETTINGS_FORMAT, ssid);
}
static bool encrypt(const std::string& ssidInput, std::string& ssidOutput) {
uint8_t iv[16];
const auto length = ssidInput.size();
constexpr size_t chunk_size = 16;
const auto encrypted_length = ((length / chunk_size) + (length % chunk_size ? 1 : 0)) * chunk_size;
auto* buffer = static_cast<uint8_t*>(malloc(encrypted_length));
crypt::getIv(ssidInput.c_str(), ssidInput.size(), iv);
if (crypt::encrypt(iv, reinterpret_cast<const uint8_t*>(ssidInput.c_str()), buffer, encrypted_length) != 0) {
TT_LOG_E(TAG, "Failed to encrypt");
free(buffer);
return false;
}
ssidOutput = toHexString(buffer, encrypted_length);
free(buffer);
return true;
}
static bool decrypt(const std::string& ssidInput, std::string& ssidOutput) {
assert(!ssidInput.empty());
assert(ssidInput.size() % 2 == 0);
auto* data = static_cast<uint8_t*>(malloc(ssidInput.size() / 2));
if (!readHex(ssidInput, data, ssidInput.size() / 2)) {
TT_LOG_E(TAG, "Failed to read hex");
return false;
}
uint8_t iv[16];
crypt::getIv(ssidInput.c_str(), ssidInput.size(), iv);
auto result_length = ssidInput.size() / 2;
// Allocate correct length plus space for string null terminator
auto* result = static_cast<uint8_t*>(malloc(result_length + 1));
result[result_length] = 0;
int decrypt_result = crypt::decrypt(
iv,
data,
result,
ssidInput.size() / 2
);
free(data);
if (decrypt_result != 0) {
TT_LOG_E(TAG, "Failed to decrypt credentials for \"%s\": %d", ssidInput.c_str(), decrypt_result);
free(result);
return false;
}
ssidOutput = reinterpret_cast<char*>(result);
free(result);
return true;
}
bool contains(const std::string& ssid) {
const auto file_path = getApPropertiesFilePath(ssid);
return file::isFile(file_path);
}
bool load(const std::string& ssid, WifiApSettings& apSettings) {
const auto file_path = getApPropertiesFilePath(ssid);
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(file_path, map)) {
return false;
}
// SSID is required
if (!map.contains(AP_PROPERTIES_KEY_SSID)) {
return false;
}
apSettings.ssid = map[AP_PROPERTIES_KEY_SSID];
assert(ssid == apSettings.ssid);
if (map.contains(AP_PROPERTIES_KEY_PASSWORD)) {
std::string password_decrypted;
if (decrypt(map[AP_PROPERTIES_KEY_PASSWORD], password_decrypted)) {
apSettings.password = password_decrypted;
} else {
return false;
}
} else {
apSettings.password = "";
}
if (map.contains(AP_PROPERTIES_KEY_AUTO_CONNECT)) {
apSettings.autoConnect = (map[AP_PROPERTIES_KEY_AUTO_CONNECT] == "true");
} else {
apSettings.autoConnect = true;
}
if (map.contains(AP_PROPERTIES_KEY_CHANNEL)) {
apSettings.channel = std::stoi(map[AP_PROPERTIES_KEY_CHANNEL].c_str());
} else {
apSettings.channel = 0;
}
return true;
}
bool save(const WifiApSettings& apSettings) {
if (apSettings.ssid.empty()) {
return false;
}
const auto file_path = getApPropertiesFilePath(apSettings.ssid);
std::map<std::string, std::string> map;
std::string password_encrypted;
if (!encrypt(apSettings.password, password_encrypted)) {
return false;
}
map[AP_PROPERTIES_KEY_PASSWORD] = password_encrypted;
map[AP_PROPERTIES_KEY_SSID] = apSettings.ssid;
map[AP_PROPERTIES_KEY_AUTO_CONNECT] = apSettings.autoConnect ? "true" : "false";
map[AP_PROPERTIES_KEY_CHANNEL] = std::to_string(apSettings.channel);
return file::savePropertiesFile(file_path, map);
}
bool remove(const std::string& ssid) {
const auto path = getApPropertiesFilePath(ssid);
if (!file::isFile(path)) {
return false;
}
return ::remove(path.c_str()) == 0;
}
}

View File

@ -0,0 +1,128 @@
#include "Tactility/service/wifi/WifiBootSplashInit.h"
#include "Tactility/file/PropertiesFile.h"
#include <Tactility/file/File.h>
#include <Tactility/Log.h>
#include <Tactility/service/wifi/WifiApSettings.h>
#include <dirent.h>
#include <format>
#include <map>
#include <string>
#include <vector>
#include <Tactility/hal/sdcard/SdCardDevice.h>
namespace tt::service::wifi {
constexpr auto* TAG = "WifiBootSplashInit";
constexpr auto* AP_PROPERTIES_KEY_SSID = "ssid";
constexpr auto* AP_PROPERTIES_KEY_PASSWORD = "password";
constexpr auto* AP_PROPERTIES_KEY_AUTO_CONNECT = "autoConnect";
constexpr auto* AP_PROPERTIES_KEY_CHANNEL = "channel";
constexpr auto* AP_PROPERTIES_KEY_AUTO_REMOVE = "autoRemovePropertiesFile";
struct ApProperties {
std::string ssid;
std::string password;
bool autoConnect;
int32_t channel;
bool autoRemovePropertiesFile;
};
static void importWifiAp(const std::string& filePath) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(filePath, map)) {
return;
}
const auto ssid_iterator = map.find(AP_PROPERTIES_KEY_SSID);
if (ssid_iterator == map.end()) {
TT_LOG_E(TAG, "%s is missing ssid", filePath.c_str());
return;
}
const auto ssid = ssid_iterator->second;
if (!settings::contains(ssid)) {
const auto password_iterator = map.find(AP_PROPERTIES_KEY_PASSWORD);
const auto password = password_iterator == map.end() ? "" : password_iterator->second;
const auto auto_connect_iterator = map.find(AP_PROPERTIES_KEY_AUTO_CONNECT);
const auto auto_connect = auto_connect_iterator == map.end() ? true : (auto_connect_iterator->second == "true");
const auto channel_iterator = map.find(AP_PROPERTIES_KEY_CHANNEL);
const auto channel = channel_iterator == map.end() ? 0 : std::stoi(channel_iterator->second);
settings::WifiApSettings settings(
ssid,
password,
auto_connect,
channel
);
if (!settings::save(settings)) {
TT_LOG_E(TAG, "Failed to save settings for %s", ssid.c_str());
} else {
TT_LOG_I(TAG, "Imported %s from %s", ssid.c_str(), filePath.c_str());
}
}
const auto auto_remove_iterator = map.find(AP_PROPERTIES_KEY_AUTO_REMOVE);
if (auto_remove_iterator != map.end() && auto_remove_iterator->second == "true") {
if (!remove(filePath.c_str())) {
TT_LOG_E(TAG, "Failed to auto-remove %s", filePath.c_str());
} else {
TT_LOG_I(TAG, "Auto-removed %s", filePath.c_str());
}
}
}
static void importWifiApSettings(std::shared_ptr<hal::sdcard::SdCardDevice> sdcard) {
// auto lock = sdcard->getLock()->asScopedLock();
// lock.lock();
auto path = sdcard->getMountPath();
std::vector<dirent> dirent_list;
if (file::scandir(path, dirent_list, [](const dirent* entry) {
switch (entry->d_type) {
case file::TT_DT_DIR:
case file::TT_DT_CHR:
case file::TT_DT_LNK:
return -1;
case file::TT_DT_REG:
default: {
std::string name = entry->d_name;
if (name.ends_with(".ap.properties")) {
return 0;
} else {
return -1;
}
}
}
}, nullptr) == 0) {
return;
}
if (dirent_list.empty()) {
TT_LOG_W(TAG, "No AP files found at %s", sdcard->getMountPath().c_str());
return;
}
for (auto& dirent : dirent_list) {
std::string absolute_path = std::format("{}/{}", path, dirent.d_name);
importWifiAp(absolute_path);
}
}
void bootSplashInit() {
const auto sdcards = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
for (auto& sdcard : sdcards) {
if (sdcard->isMounted()) {
importWifiApSettings(sdcard);
} else {
TT_LOG_W(TAG, "Skipping unmounted SD card %s", sdcard->getMountPath().c_str());
}
}
}
}

View File

@ -1,19 +1,20 @@
#ifdef ESP_PLATFORM
#include <lwip/esp_netif_net_stack.h>
#include "Tactility/service/wifi/Wifi.h"
#include "Tactility/TactilityHeadless.h"
#include "Tactility/service/ServiceContext.h"
#include "Tactility/service/wifi/WifiGlobals.h"
#include "Tactility/service/wifi/WifiSettings.h"
#include "Tactility/service/wifi/WifiBootSplashInit.h"
#include <Tactility/kernel/SystemEvents.h>
#include <Tactility/Timer.h>
#include <lwip/esp_netif_net_stack.h>
#include <freertos/FreeRTOS.h>
#include <atomic>
#include <cstring>
#include <Tactility/kernel/SystemEvents.h>
#include <sys/cdefs.h>
namespace tt::service::wifi {
@ -36,8 +37,6 @@ static void dispatchDisconnectButKeepActive(std::shared_ptr<Wifi> wifi);
class Wifi {
private:
std::atomic<RadioState> radio_state = RadioState::Off;
bool scan_active = false;
bool secure_connection = false;
@ -66,14 +65,11 @@ public:
esp_event_handler_instance_t event_handler_any_id = nullptr;
esp_event_handler_instance_t event_handler_got_ip = nullptr;
EventFlag connection_wait_flags;
settings::WifiApSettings connection_target = {
.ssid = { 0 },
.password = { 0 },
.auto_connect = false
};
settings::WifiApSettings connection_target;
bool pause_auto_connect = false; // Pause when manually disconnecting until manually connecting again
bool connection_target_remember = false; // Whether to store the connection_target on successful connection or not
esp_netif_ip_info_t ip_info;
kernel::SystemEventSubscription bootEventSubscription = kernel::NoSystemEventSubscription;
RadioState getRadioState() const {
auto lock = dataMutex.asScopedLock();
@ -187,8 +183,8 @@ bool isScanning() {
}
}
void connect(const settings::WifiApSettings* ap, bool remember) {
TT_LOG_I(TAG, "connect(%s, %d)", ap->ssid, remember);
void connect(const settings::WifiApSettings& ap, bool remember) {
TT_LOG_I(TAG, "connect(%s, %d)", ap.ssid.c_str(), remember);
auto wifi = wifi_singleton;
if (wifi == nullptr) {
return;
@ -201,14 +197,14 @@ void connect(const settings::WifiApSettings* ap, bool remember) {
// Manual connect (e.g. via app) should stop auto-connecting until the connection is established
wifi->pause_auto_connect = true;
memcpy(&wifi->connection_target, ap, sizeof(settings::WifiApSettings));
wifi->connection_target = ap;
wifi->connection_target_remember = remember;
if (wifi->getRadioState() == RadioState::Off) {
getMainDispatcher().dispatch([wifi]() { dispatchEnable(wifi); });
getMainDispatcher().dispatch([wifi] { dispatchEnable(wifi); });
}
getMainDispatcher().dispatch([wifi]() { dispatchConnect(wifi); });
getMainDispatcher().dispatch([wifi] { dispatchConnect(wifi); });
}
void disconnect() {
@ -223,11 +219,7 @@ void disconnect() {
return;
}
wifi->connection_target = (settings::WifiApSettings) {
.ssid = { 0 },
.password = { 0 },
.auto_connect = false
};
wifi->connection_target = settings::WifiApSettings("", "");
// Manual disconnect (e.g. via app) should stop auto-connecting until a new connection is established
wifi->pause_auto_connect = true;
getMainDispatcher().dispatch([wifi]() { dispatchDisconnectButKeepActive(wifi); });
@ -307,9 +299,9 @@ void setEnabled(bool enabled) {
}
if (enabled) {
getMainDispatcher().dispatch([wifi]() { dispatchEnable(wifi); });
getMainDispatcher().dispatch([wifi] { dispatchEnable(wifi); });
} else {
getMainDispatcher().dispatch([wifi]() { dispatchDisable(wifi); });
getMainDispatcher().dispatch([wifi] { dispatchDisable(wifi); });
}
wifi->pause_auto_connect = false;
@ -431,8 +423,8 @@ static bool find_auto_connect_ap(std::shared_ptr<Wifi> wifi, settings::WifiApSet
auto ssid = reinterpret_cast<const char*>(wifi->scan_list[i].ssid);
if (settings::contains(ssid)) {
static_assert(sizeof(wifi->scan_list[i].ssid) == (TT_WIFI_SSID_LIMIT + 1), "SSID size mismatch");
if (settings::load(ssid, &settings)) {
if (settings.auto_connect) {
if (settings::load(ssid, settings)) {
if (settings.autoConnect) {
return true;
}
} else {
@ -451,8 +443,8 @@ static void dispatchAutoConnect(std::shared_ptr<Wifi> wifi) {
settings::WifiApSettings settings;
if (find_auto_connect_ap(wifi, settings)) {
TT_LOG_I(TAG, "Auto-connecting to %s", settings.ssid);
connect(&settings, false);
TT_LOG_I(TAG, "Auto-connecting to %s", settings.ssid.c_str());
connect(settings, false);
// TODO: We currently have to manually reset it because connect() sets it.
// connect() assumes it's only being called by the user and not internally, so it disables auto-connect
wifi->pause_auto_connect = false;
@ -726,7 +718,7 @@ static void dispatchConnect(std::shared_ptr<Wifi> wifi) {
return;
}
TT_LOG_I(TAG, "Connecting to %s", wifi->connection_target.ssid);
TT_LOG_I(TAG, "Connecting to %s", wifi->connection_target.ssid.c_str());
// Stop radio first, if needed
RadioState radio_state = wifi->getRadioState();
@ -758,11 +750,10 @@ static void dispatchConnect(std::shared_ptr<Wifi> wifi) {
config.sta.threshold.rssi = -127;
config.sta.pmf_cfg.capable = true;
static_assert(sizeof(config.sta.ssid) == (sizeof(wifi_singleton->connection_target.ssid)-1), "SSID size mismatch");
memcpy(config.sta.ssid, wifi_singleton->connection_target.ssid, sizeof(config.sta.ssid));
memcpy(config.sta.ssid, wifi_singleton->connection_target.ssid.c_str(), wifi_singleton->connection_target.ssid.size());
if (wifi_singleton->connection_target.password[0] != 0x00) {
memcpy(config.sta.password, wifi_singleton->connection_target.password, sizeof(config.sta.password));
memcpy(config.sta.password, wifi_singleton->connection_target.password.c_str(), wifi_singleton->connection_target.password.size());
config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
}
@ -794,9 +785,9 @@ static void dispatchConnect(std::shared_ptr<Wifi> wifi) {
wifi->setSecureConnection(config.sta.password[0] != 0x00U);
wifi->setRadioState(RadioState::ConnectionActive);
publish_event_simple(wifi, EventType::ConnectionSuccess);
TT_LOG_I(TAG, "Connected to %s", wifi->connection_target.ssid);
TT_LOG_I(TAG, "Connected to %s", wifi->connection_target.ssid.c_str());
if (wifi->connection_target_remember) {
if (!settings::save(&wifi->connection_target)) {
if (!settings::save(wifi->connection_target)) {
TT_LOG_E(TAG, "Failed to store credentials");
} else {
TT_LOG_I(TAG, "Stored credentials");
@ -805,7 +796,7 @@ static void dispatchConnect(std::shared_ptr<Wifi> wifi) {
} else if (bits & WIFI_FAIL_BIT) {
wifi->setRadioState(RadioState::On);
publish_event_simple(wifi, EventType::ConnectionFailed);
TT_LOG_I(TAG, "Failed to connect to %s", wifi->connection_target.ssid);
TT_LOG_I(TAG, "Failed to connect to %s", wifi->connection_target.ssid.c_str());
} else {
wifi->setRadioState(RadioState::On);
publish_event_simple(wifi, EventType::ConnectionFailed);
@ -911,13 +902,17 @@ public:
assert(wifi_singleton == nullptr);
wifi_singleton = std::make_shared<Wifi>();
wifi_singleton->bootEventSubscription = kernel::subscribeSystemEvent(kernel::SystemEvent::BootSplash, [](auto) {
bootSplashInit();
});
wifi_singleton->autoConnectTimer = std::make_unique<Timer>(Timer::Type::Periodic, []() { onAutoConnectTimer(); });
// We want to try and scan more often in case of startup or scan lock failure
wifi_singleton->autoConnectTimer->start(std::min(2000, AUTO_SCAN_INTERVAL));
if (settings::shouldEnableOnBoot()) {
TT_LOG_I(TAG, "Auto-enabling due to setting");
getMainDispatcher().dispatch([]() { dispatchEnable(wifi_singleton); });
getMainDispatcher().dispatch([] { dispatchEnable(wifi_singleton); });
}
}

View File

@ -66,7 +66,7 @@ bool isScanning() {
return wifi->scan_active;
}
void connect(const settings::WifiApSettings* ap, bool remember) {
void connect(const settings::WifiApSettings& ap, bool remember) {
assert(wifi);
// TODO: implement
}

View File

@ -1,18 +1,54 @@
#include "Tactility/service/wifi/WifiSettings.h"
#include "Tactility/Preferences.h"
#include "Tactility/file/PropertiesFile.h"
#define WIFI_PREFERENCES_NAMESPACE "wifi"
#define WIFI_PREFERENCES_KEY_ENABLE_ON_BOOT "enable_on_boot"
#include <Tactility/LogEsp.h>
#include <Tactility/file/File.h>
namespace tt::service::wifi::settings {
constexpr auto* TAG = "WifiSettings";
constexpr auto* SETTINGS_FILE = "/data/service/Wifi/wifi.properties";
constexpr auto* SETTINGS_KEY_ENABLE_ON_BOOT = "enableOnBoot";
struct WifiProperties {
bool enableOnBoot;
};
static bool load(WifiProperties& properties) {
std::map<std::string, std::string> map;
if (!file::loadPropertiesFile(SETTINGS_FILE, map)) {
return false;
}
if (!map.contains(SETTINGS_KEY_ENABLE_ON_BOOT)) {
return false;
}
auto enable_on_boot_string = map[SETTINGS_KEY_ENABLE_ON_BOOT];
properties.enableOnBoot = (enable_on_boot_string == "true");
return true;
}
static bool save(const WifiProperties& properties) {
std::map<std::string, std::string> map;
map[SETTINGS_KEY_ENABLE_ON_BOOT] = properties.enableOnBoot ? "true" : "false";
return file::savePropertiesFile(SETTINGS_FILE, map);
}
void setEnableOnBoot(bool enable) {
Preferences(WIFI_PREFERENCES_NAMESPACE).putBool(WIFI_PREFERENCES_KEY_ENABLE_ON_BOOT, enable);
WifiProperties properties { .enableOnBoot = enable };
if (!save(properties)) {
TT_LOG_E(TAG, "Failed to save %s", SETTINGS_FILE);
}
}
bool shouldEnableOnBoot() {
bool enable = false;
Preferences(WIFI_PREFERENCES_NAMESPACE).optBool(WIFI_PREFERENCES_KEY_ENABLE_ON_BOOT, enable);
return enable;
WifiProperties properties;
if (!load(properties)) {
return false;
}
return properties.enableOnBoot;
}
} // namespace

View File

@ -1,148 +0,0 @@
#ifdef ESP_PLATFORM
#include "Tactility/service/wifi/WifiGlobals.h"
#include "Tactility/service/wifi/WifiSettings.h"
#include <Tactility/Log.h>
#include <Tactility/crypt/Hash.h>
#include <Tactility/crypt/Crypt.h>
#include <nvs_flash.h>
#include <cstring>
#define TAG "wifi_settings"
#define TT_NVS_NAMESPACE "wifi_settings" // limited by NVS_KEY_NAME_MAX_SIZE
// region Wi-Fi Credentials - static
static esp_err_t credentials_nvs_open(nvs_handle_t* handle, nvs_open_mode_t mode) {
return nvs_open(TT_NVS_NAMESPACE, NVS_READWRITE, handle);
}
static void credentials_nvs_close(nvs_handle_t handle) {
nvs_close(handle);
}
// endregion Wi-Fi Credentials - static
// region Wi-Fi Credentials - public
namespace tt::service::wifi::settings {
bool contains(const char* ssid) {
nvs_handle_t handle;
esp_err_t result = credentials_nvs_open(&handle, NVS_READONLY);
if (result != ESP_OK) {
TT_LOG_E(TAG, "Failed to open NVS handle: %s", esp_err_to_name(result));
return false;
}
bool key_exists = nvs_find_key(handle, ssid, nullptr) == ESP_OK;
credentials_nvs_close(handle);
return key_exists;
}
bool load(const char* ssid, WifiApSettings* settings) {
nvs_handle_t handle;
esp_err_t result = credentials_nvs_open(&handle, NVS_READONLY);
if (result != ESP_OK) {
TT_LOG_E(TAG, "Failed to open NVS handle: %s", esp_err_to_name(result));
return false;
}
WifiApSettings encrypted_settings;
size_t length = sizeof(WifiApSettings);
result = nvs_get_blob(handle, ssid, &encrypted_settings, &length);
uint8_t iv[16];
crypt::getIv(ssid, strlen(ssid), iv);
int decrypt_result = crypt::decrypt(
iv,
(uint8_t*)encrypted_settings.password,
(uint8_t*)settings->password,
TT_WIFI_CREDENTIALS_PASSWORD_LIMIT
);
// Manually ensure null termination, because encryption must be a multiple of 16 bytes
encrypted_settings.password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT] = 0;
if (decrypt_result != 0) {
result = ESP_FAIL;
TT_LOG_E(TAG, "Failed to decrypt credentials for \"%s\": %d", ssid, decrypt_result);
}
if (result != ESP_OK && result != ESP_ERR_NVS_NOT_FOUND) {
TT_LOG_E(TAG, "Failed to get credentials for \"%s\": %s", ssid, esp_err_to_name(result));
}
credentials_nvs_close(handle);
settings->auto_connect = encrypted_settings.auto_connect;
strcpy((char*)settings->ssid, encrypted_settings.ssid);
return result == ESP_OK;
}
bool save(const WifiApSettings* settings) {
nvs_handle_t handle;
esp_err_t result = credentials_nvs_open(&handle, NVS_READWRITE);
if (result != ESP_OK) {
TT_LOG_E(TAG, "Failed to open NVS handle: %s", esp_err_to_name(result));
return false;
}
WifiApSettings encrypted_settings = {
.ssid = { 0 },
.password = { 0 },
.auto_connect = settings->auto_connect,
};
strcpy((char*)encrypted_settings.ssid, settings->ssid);
// We only decrypt multiples of 16, so we have to ensure the last byte is set to 0
encrypted_settings.password[TT_WIFI_CREDENTIALS_PASSWORD_LIMIT] = 0;
uint8_t iv[16];
crypt::getIv(settings->ssid, strlen(settings->ssid), iv);
int encrypt_result = crypt::encrypt(
iv,
(uint8_t*)settings->password,
(uint8_t*)encrypted_settings.password,
TT_WIFI_CREDENTIALS_PASSWORD_LIMIT
);
if (encrypt_result != 0) {
result = ESP_FAIL;
TT_LOG_E(TAG, "Failed to encrypt credentials \"%s\": %d", settings->ssid, encrypt_result);
}
if (result == ESP_OK) {
result = nvs_set_blob(handle, settings->ssid, &encrypted_settings, sizeof(WifiApSettings));
if (result != ESP_OK) {
TT_LOG_E(TAG, "Failed to get credentials for \"%s\": %s", settings->ssid, esp_err_to_name(result));
}
}
credentials_nvs_close(handle);
return result == ESP_OK;
}
bool remove(const char* ssid) {
nvs_handle_t handle;
esp_err_t result = credentials_nvs_open(&handle, NVS_READWRITE);
if (result != ESP_OK) {
TT_LOG_E(TAG, "Failed to open NVS handle to store \"%s\": %s", ssid, esp_err_to_name(result));
return false;
}
result = nvs_erase_key(handle, ssid);
if (result != ESP_OK) {
TT_LOG_E(TAG, "Failed to erase credentials for \"%s\": %s", ssid, esp_err_to_name(result));
}
credentials_nvs_close(handle);
return result == ESP_OK;
}
// end region Wi-Fi Credentials - public
} // nemespace
#endif // ESP_PLATFORM

View File

@ -1,27 +0,0 @@
#ifndef ESP_PLATFORM
#include "Tactility/service/wifi/WifiSettings.h"
namespace tt::service::wifi::settings {
#define TAG "wifi_settings_mock"
bool contains(const char* ssid) {
return false;
}
bool load(const char* ssid, WifiApSettings* settings) {
return false;
}
bool save(const WifiApSettings* settings) {
return true;
}
bool remove(const char* ssid) {
return true;
}
} // namespace
#endif // ESP_PLATFORM

View File

@ -35,7 +35,7 @@ void tt_app_stop();
/**
* Start an app by id and bundle.
* @param[in] appId the app manifest id
* @param[in] parameters the parameters to pass onto the starting app
* @param[in] parameters the parameters to pass onto the starting app
*/
void tt_app_start_with_bundle(const char* appId, BundleHandle parameters);

View File

@ -34,10 +34,11 @@ void tt_wifi_set_enabled(bool enabled) {
void tt_wifi_connect(const char* ssid, const char* password, int32_t channel, bool autoConnect, bool remember) {
wifi::settings::WifiApSettings settings;
strcpy(settings.ssid, ssid);
strcpy(settings.password, password);
settings.ssid = ssid;
settings.password = password;
settings.channel = channel;
settings.auto_connect = autoConnect;
settings.autoConnect = autoConnect;
wifi::connect(settings, remember);
}
void tt_wifi_disconnect() {

View File

@ -9,7 +9,6 @@ if (DEFINED ENV{ESP_IDF_VERSION})
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Include/"
PRIV_INCLUDE_DIRS "Private/"
REQUIRES mbedtls nvs_flash esp_rom esp_timer
)
@ -25,10 +24,6 @@ else()
PRIVATE ${SOURCES}
)
include_directories(
PRIVATE Private/
)
target_include_directories(TactilityCore SYSTEM
PUBLIC Include/
)

View File

@ -0,0 +1,27 @@
#pragma once
#include "RtosCompat.h"
namespace tt {
typedef portBASE_TYPE CpuAffinity;
constexpr static CpuAffinity None = -1;
/**
* Determines the preferred affinity for certain (sub)systems.
*/
struct CpuAffinityConfiguration {
CpuAffinity system;
CpuAffinity graphics; // Display, LVGL
CpuAffinity wifi;
CpuAffinity mainDispatcher;
CpuAffinity apps;
CpuAffinity timer; // Tactility Timer (based on FreeRTOS)
};
void setCpuAffinityConfiguration(const CpuAffinityConfiguration& config);
const CpuAffinityConfiguration& getCpuAffinityConfiguration();
}

View File

@ -66,7 +66,7 @@ public:
explicit ScopedLock(const Lock& lockable) : lockable(lockable) {}
~ScopedLock() final {
~ScopedLock() override {
lockable.unlock(); // We don't care whether it succeeded or not
}

View File

@ -43,18 +43,18 @@ public:
using Lock::lock;
explicit Mutex(Type type = Type::Normal);
~Mutex() final = default;
~Mutex() override = default;
/** Attempt to lock the mutex. Blocks until timeout passes or lock is acquired.
* @param[in] timeout
* @return success result
*/
bool lock(TickType_t timeout) const final;
bool lock(TickType_t timeout) const override;
/** Attempt to unlock the mutex.
* @return success result
*/
bool unlock() const final;
bool unlock() const override;
/** @return the owner of the thread */
ThreadId getOwner() const;

View File

@ -4,6 +4,7 @@
#include <cstdio>
#include <string>
#include <vector>
#include <functional>
namespace tt::string {
@ -30,6 +31,16 @@ std::string getLastPathSegment(const std::string& path);
*/
std::vector<std::string> split(const std::string& input, const std::string& delimiter);
/**
* Splits the provided input into separate pieces with delimiter as separator text.
* When the input string is empty, the output list will be empty too.
*
* @param input the input to split up
* @param delimiter a non-empty string to recognize as separator
* @param callback the callback function that receives the split parts
*/
void split(const std::string& input, const std::string& delimiter, std::function<void(const std::string&)> callback);
/**
* Join a set of tokens into a single string, given a delimiter (separator).
* If the input is an empty list, the result will be an empty string.

View File

@ -18,7 +18,7 @@ public:
private:
struct TimerHandleDeleter {
void operator()(TimerHandle_t handleToDelete) {
void operator()(TimerHandle_t handleToDelete) const {
xTimerDelete(handleToDelete, portMAX_DELAY);
}
};

View File

@ -44,7 +44,7 @@ void getIv(const void* data, size_t dataLength, uint8_t iv[16]);
* @param[in] dataLength data length, a multiple of 16 (for both inData and outData)
* @return the result of esp_aes_crypt_cbc() (MBEDTLS_ERR_*)
*/
int encrypt(const uint8_t iv[16], uint8_t* inData, uint8_t* outData, size_t dataLength);
int encrypt(const uint8_t iv[16], const uint8_t* inData, uint8_t* outData, size_t dataLength);
/**
* @brief Decrypt data.
@ -58,6 +58,7 @@ int encrypt(const uint8_t iv[16], uint8_t* inData, uint8_t* outData, size_t data
* @param[in] dataLength data length, a multiple of 16 (for both inData and outData)
* @return the result of esp_aes_crypt_cbc() (MBEDTLS_ERR_*)
*/
int decrypt(const uint8_t iv[16], uint8_t* inData, uint8_t* outData, size_t dataLength);
int decrypt(const uint8_t iv[16], const uint8_t* inData, uint8_t* outData, size_t dataLength);
} // namespace

View File

@ -7,28 +7,23 @@
#include <sys/stat.h>
#include <vector>
/**
* @warning SD card access requires a locking mechanism:
* @warning When using this in the Tactility main project, use `file::getLock()` or `file::withLock()`
*/
namespace tt::file {
/** File types for `dirent`'s `d_type`. */
enum {
TT_DT_UNKNOWN = 0,
#define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type
TT_DT_FIFO = 1,
#define TT_DT_FIFO TT_DT_FIFO // Named pipe or FIFO
TT_DT_CHR = 2,
#define TT_DT_CHR TT_DT_CHR // Character device
TT_DT_DIR = 4,
#define TT_DT_DIR TT_DT_DIR // Directory
TT_DT_BLK = 6,
#define TT_DT_BLK TT_DT_BLK // Block device
TT_DT_REG = 8,
#define TT_DT_REG TT_DT_REG // Regular file
TT_DT_LNK = 10,
#define TT_DT_LNK TT_DT_LNK // Symbolic link
TT_DT_SOCK = 12,
#define TT_DT_SOCK TT_DT_SOCK // Local-domain socket
TT_DT_WHT = 14
#define TT_DT_WHT TT_DT_WHT // Whiteout inodes
TT_DT_UNKNOWN = 0, // Unknown type
TT_DT_FIFO = 1, // Named pipe or FIFO
TT_DT_CHR = 2, // Character device
TT_DT_DIR = 4, // Directory
TT_DT_BLK = 6, // Block device
TT_DT_REG = 8, // Regular file
TT_DT_LNK = 10, // Symbolic link
TT_DT_SOCK = 12, // Local-domain socket
TT_DT_WHT = 14 // Whiteout inodes
};
#ifdef _WIN32
@ -92,6 +87,10 @@ bool direntSortAlphaAndType(const dirent& left, const dirent& right);
/** A filter for filtering out "." and ".." */
int direntFilterDotEntries(const dirent* entry);
bool isFile(const std::string& path);
bool isDirectory(const std::string& path);
/**
* A scandir()-like implementation that works on ESP32.
* It does not return "." and ".." items but otherwise functions the same.

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