This commit is contained in:
Ken Van Hoeylandt 2026-07-05 14:00:23 +02:00
parent 66a4eb66d6
commit a2c63686c1
18 changed files with 1132 additions and 147 deletions

View File

@ -1,12 +1,17 @@
#pragma once
#include <Tactility/service/ServiceManifest.h>
#include <tactility/check.h>
#include <tactility/error.h>
#include <tactility/service/service_paths.h>
#include <string>
#include <memory>
namespace tt::service {
// Forward declarations
class ServiceManifest;
constexpr size_t PATH_BUFFER_SIZE = 255;
class ServicePaths {
@ -20,26 +25,44 @@ public:
* The user data directory is intended to survive OS upgrades.
* The path will not end with a "/".
*/
std::string getUserDataDirectory() const;
std::string getUserDataDirectory() const {
char buffer[PATH_BUFFER_SIZE];
check(service_paths_get_user_data_directory(manifest->id.c_str(), buffer, sizeof(buffer)) == ERROR_NONE);
return buffer;
}
/**
* The user data directory is intended to survive OS upgrades.
* Configuration data should be stored here.
* @param[in] childPath the path without a "/" prefix
*/
std::string getUserDataPath(const std::string& childPath) const;
std::string getUserDataPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
char buffer[PATH_BUFFER_SIZE];
check(service_paths_get_user_data_path(manifest->id.c_str(), childPath.c_str(), buffer, sizeof(buffer)) == ERROR_NONE);
return buffer;
}
/**
* You should not store configuration data here.
* The path will not end with a "/".
*/
std::string getAssetsDirectory() const;
std::string getAssetsDirectory() const {
char buffer[PATH_BUFFER_SIZE];
check(service_paths_get_assets_directory(manifest->id.c_str(), buffer, sizeof(buffer)) == ERROR_NONE);
return buffer;
}
/**
* You should not store configuration data here.
* @param[in] childPath the path without a "/" prefix
*/
std::string getAssetsPath(const std::string& childPath) const;
std::string getAssetsPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
char buffer[PATH_BUFFER_SIZE];
check(service_paths_get_assets_path(manifest->id.c_str(), childPath.c_str(), buffer, sizeof(buffer)) == ERROR_NONE);
return buffer;
}
};
}

View File

@ -1,36 +1,43 @@
#pragma once
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/Service.h>
#include <Tactility/Mutex.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServicePaths.h>
#include <tactility/service/service_context.h>
#include <memory>
namespace tt::service {
/**
* Thin wrapper around the running TactilityKernel service context (::ServiceContext,
* which is the same object as ::ServiceInstance in the C API). Non-owning: valid only
* for as long as the underlying kernel instance is running.
*/
class ServiceInstance final : public ServiceContext {
Mutex mutex;
std::shared_ptr<const ServiceManifest> manifest;
std::shared_ptr<Service> service;
State state = State::Stopped;
::ServiceContext* cContext;
std::shared_ptr<const ServiceManifest>& getCppManifest(::ServiceContext* cContext) const {
const auto* cManifest = service_context_get_manifest(cContext);
return *static_cast<std::shared_ptr<const ServiceManifest>*>(cManifest->context);
}
public:
explicit ServiceInstance(std::shared_ptr<const ServiceManifest> manifest);
explicit ServiceInstance(::ServiceContext* cContext) : cContext(cContext) {}
~ServiceInstance() override = default;
/** @return a reference to the service's manifest */
const ServiceManifest& getManifest() const override;
const ServiceManifest& getManifest() const override {
return *getCppManifest(cContext);
}
/** Retrieve the paths that are relevant to this service */
std::unique_ptr<ServicePaths> getPaths() const override;
std::shared_ptr<Service> getService() const { return service; }
State getState() const { return state; }
void setState(State newState) { state = newState; }
std::unique_ptr<ServicePaths> getPaths() const override {
return std::make_unique<ServicePaths>(getCppManifest(cContext));
}
};
}

View File

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

View File

@ -1,29 +0,0 @@
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/Paths.h>
#include <cassert>
#include <format>
namespace tt::service {
std::string ServicePaths::getUserDataDirectory() const {
return std::format("{}/service/{}", tt::getUserDataPath(), manifest->id);
}
std::string ServicePaths::getUserDataPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return std::format("{}/{}", getUserDataDirectory(), childPath);
}
std::string ServicePaths::getAssetsDirectory() const {
return std::format("{}/service/{}/assets", tt::getUserDataPath(), manifest->id);
}
std::string ServicePaths::getAssetsPath(const std::string& childPath) const {
assert(!childPath.starts_with('/'));
return std::format("{}/{}", getAssetsDirectory(), childPath);
}
}

View File

@ -1,43 +1,87 @@
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/Mutex.h>
#include <Tactility/service/ServiceInstance.h>
#include <Tactility/service/ServiceManifest.h>
#include <string>
#include <tactility/service/service_registration.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <cassert>
namespace tt::service {
constexpr auto* TAG = "ServiceRegistration";
typedef std::unordered_map<std::string, std::shared_ptr<const ServiceManifest>> ManifestMap;
typedef std::unordered_map<std::string, std::shared_ptr<ServiceInstance>> ServiceInstanceMap;
static State toCppState(ServiceState state) {
switch (state) {
case SERVICE_STATE_STARTING: return State::Starting;
case SERVICE_STATE_STARTED: return State::Started;
case SERVICE_STATE_STOPPING: return State::Stopping;
case SERVICE_STATE_STOPPED:
default: return State::Stopped;
}
}
static ManifestMap service_manifest_map;
static ServiceInstanceMap service_instance_map;
// Bridges the kernel's context-free C ServiceManifest/Service callbacks to the
// C++ Service instances they wrap. Declared extern "C" to match the linkage of
// the C function-pointer types they're assigned to (see e.g. gpio_controller.cpp).
extern "C" {
static Mutex manifest_mutex;
static Mutex instance_mutex;
static error_t cppOnStartTrampoline(::Service* cService, ::ServiceInstance* cContext) {
auto& servicePtr = *static_cast<std::shared_ptr<Service>*>(cService->data);
ServiceInstance context(cContext);
return servicePtr->onStart(context) ? ERROR_NONE : ERROR_RESOURCE;
}
static void cppOnStopTrampoline(::Service* cService, ::ServiceInstance* cContext) {
auto& servicePtr = *static_cast<std::shared_ptr<Service>*>(cService->data);
ServiceInstance context(cContext);
servicePtr->onStop(context);
}
static ::Service* cppCreateServiceTrampoline(void* context) {
auto& cppManifest = *static_cast<std::shared_ptr<const ServiceManifest>*>(context);
auto* cService = new ::Service();
cService->data = new std::shared_ptr(cppManifest->createService());
cService->on_start = cppOnStartTrampoline;
cService->on_stop = cppOnStopTrampoline;
return cService;
}
static void cppDestroyServiceTrampoline(::Service* cService, void* /*context*/) {
delete static_cast<std::shared_ptr<Service>*>(cService->data);
delete cService;
}
} // extern "C"
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
const auto& id = manifest->id;
LOG_I(TAG, "Adding %s", id.c_str());
manifest_mutex.lock();
if (service_manifest_map[id] == nullptr) {
service_manifest_map[id] = std::move(manifest);
} else {
if (service_registration_find_manifest(id.c_str()) != nullptr) {
LOG_E(TAG, "Service id in use: %s", id.c_str());
return;
}
manifest_mutex.unlock();
if (autoStart) {
startService(id);
// The bridging C manifest and the C++ manifest it carries in .context are
// intentionally never freed: services are registered once and live for the
// process lifetime (there is no removeService()), matching the previous
// implementation's static, never-erased manifest map.
auto* cppManifestPtr = new std::shared_ptr(manifest);
auto* cManifest = new ::ServiceManifest {
.id = (*cppManifestPtr)->id.c_str(),
.create_service = cppCreateServiceTrampoline,
.destroy_service = cppDestroyServiceTrampoline,
.context = cppManifestPtr
};
error_t error = service_registration_add(cManifest, autoStart);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to add service %s: %s", id.c_str(), error_to_string(error));
}
}
@ -46,93 +90,53 @@ void addService(const ServiceManifest& manifest, bool autoStart) {
}
std::shared_ptr<const ServiceManifest> findManifestById(const std::string& id) {
manifest_mutex.lock();
auto iterator = service_manifest_map.find(id);
auto manifest = iterator != service_manifest_map.end() ? iterator->second : nullptr;
manifest_mutex.unlock();
return manifest;
const auto* cManifest = service_registration_find_manifest(id.c_str());
if (cManifest == nullptr) {
return nullptr;
}
return *static_cast<std::shared_ptr<const ServiceManifest>*>(cManifest->context);
}
static std::shared_ptr<ServiceInstance> findServiceInstanceById(const std::string& id) {
manifest_mutex.lock();
auto iterator = service_instance_map.find(id);
auto service = iterator != service_instance_map.end() ? iterator->second : nullptr;
manifest_mutex.unlock();
return service;
}
// TODO: Return proper error/status instead of BOOL?
bool startService(const std::string& id) {
LOG_I(TAG, "Starting %s", id.c_str());
auto manifest = findManifestById(id);
if (manifest == nullptr) {
LOG_E(TAG, "manifest not found for service %s", id.c_str());
error_t error = service_registration_start(id.c_str());
if (error != ERROR_NONE) {
LOG_E(TAG, "Starting %s failed: %s", id.c_str(), error_to_string(error));
return false;
}
auto service_instance = std::make_shared<ServiceInstance>(manifest);
// Register first, so that a service can retrieve itself during onStart()
instance_mutex.lock();
service_instance_map[manifest->id] = service_instance;
instance_mutex.unlock();
service_instance->setState(State::Starting);
if (service_instance->getService()->onStart(*service_instance)) {
service_instance->setState(State::Started);
} else {
LOG_E(TAG, "Starting %s failed", id.c_str());
service_instance->setState(State::Stopped);
instance_mutex.lock();
service_instance_map.erase(manifest->id);
instance_mutex.unlock();
}
LOG_I(TAG, "Started %s", id.c_str());
return true;
}
std::shared_ptr<ServiceContext> findServiceContextById(const std::string& id) {
return findServiceInstanceById(id);
auto* cContext = service_registration_find_context(id.c_str());
if (cContext == nullptr) {
return nullptr;
}
return std::make_shared<ServiceInstance>(cContext);
}
std::shared_ptr<Service> findServiceById(const std::string& id) {
auto instance = findServiceInstanceById(id);
return instance != nullptr ? instance->getService() : nullptr;
auto* cService = service_registration_find_service(id.c_str());
if (cService == nullptr) {
return nullptr;
}
return *static_cast<std::shared_ptr<Service>*>(cService->data);
}
bool stopService(const std::string& id) {
LOG_I(TAG, "Stopping %s", id.c_str());
auto service_instance = findServiceInstanceById(id);
if (service_instance == nullptr) {
error_t error = service_registration_stop(id.c_str());
if (error != ERROR_NONE) {
LOG_W(TAG, "Service not running: %s", id.c_str());
return false;
}
service_instance->setState(State::Stopping);
service_instance->getService()->onStop(*service_instance);
service_instance->setState(State::Stopped);
instance_mutex.lock();
service_instance_map.erase(id);
instance_mutex.unlock();
if (service_instance.use_count() > 1) {
LOG_W(TAG, "Possible memory leak: service %s still has %d references", service_instance->getManifest().id.c_str(), (int)(service_instance.use_count() - 1));
}
LOG_I(TAG, "Stopped %s", id.c_str());
return true;
}
State getState(const std::string& id) {
auto service_instance = findServiceInstanceById(id);
if (service_instance == nullptr) {
return State::Stopped;
}
return service_instance->getState();
return toCppState(service_registration_get_state(id.c_str()));
}
} // namespace

View File

@ -0,0 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Get the root path for user data. Survives OS upgrades.
* @param[out] out_path buffer to store the path (no trailing "/")
* @param[in] out_path_size size of the output buffer
* @retval ERROR_NOT_FOUND if the configured storage location isn't available (e.g. no SD card)
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t paths_get_user_data_path(char* out_path, size_t out_path_size);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
// ServiceContext (declared in service_context.h) is a typedef alias of ServiceInstance,
// so callbacks below are declared directly in terms of ServiceInstance to avoid a
// conflicting forward-declaration of an unrelated "ServiceContext" struct tag.
struct ServiceInstance;
/**
* A service is a long-running background process (e.g. Wi-Fi, GPS, GUI).
* Concrete services keep their own state behind the `data` pointer.
*/
struct Service {
/** Service-specific data, owned by the service implementation. Can be NULL. */
void* data;
/**
* Called when the service is starting.
* Can be NULL, in which case starting always succeeds.
* @param[in,out] service this service
* @param[in,out] context the context (a ServiceInstance) for this running service
* @return ERROR_NONE if the service started successfully
*/
error_t (*on_start)(struct Service* service, struct ServiceInstance* context);
/**
* Called when the service is stopping.
* Can be NULL, in which case stopping is a no-op.
* @param[in,out] service this service
* @param[in,out] context the context (a ServiceInstance) for this running service
*/
void (*on_stop)(struct Service* service, struct ServiceInstance* context);
};
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/service/service_instance.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* The context handed to a service's on_start/on_stop callbacks.
* This is the same object as the ServiceInstance that owns the service (C has no
* interface/inheritance to distinguish a restricted "context" view from the full
* instance, so the two are deliberately the same type under different names).
*/
typedef struct ServiceInstance ServiceContext;
/**
* @brief Get the manifest of the running service.
* @param[in] context non-null service context pointer
* @return the manifest
*/
const struct ServiceManifest* service_context_get_manifest(ServiceContext* context);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,78 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <tactility/service/service_manifest.h>
#ifdef __cplusplus
extern "C" {
#endif
struct ServiceInstanceInternal;
/** The lifecycle state of a ServiceInstance. */
typedef enum {
SERVICE_STATE_STARTING,
SERVICE_STATE_STARTED,
SERVICE_STATE_STOPPING,
SERVICE_STATE_STOPPED,
} ServiceState;
/** Represents a running (or about-to-run) instance of a service. */
struct ServiceInstance {
/** The manifest that spawned this instance. */
const struct ServiceManifest* manifest;
/** The service created via manifest->create_service(). */
struct Service* service;
/**
* Internal state managed by the kernel.
* ServiceInstance implementers should initialize this to NULL.
* Do not access or modify directly; use service_instance_* functions.
*/
struct ServiceInstanceInternal* internal;
};
/**
* @brief Construct a service instance.
* @details This calls manifest->create_service() to build the service.
* @param[in,out] instance a service instance with manifest set and internal set to NULL
* @param[in] manifest non-null manifest to construct the instance from
* @retval ERROR_OUT_OF_MEMORY if internal data allocation failed
* @retval ERROR_NONE on success
*/
error_t service_instance_construct(struct ServiceInstance* instance, const struct ServiceManifest* manifest);
/**
* @brief Destruct a service instance.
* @details This calls manifest->destroy_service() on the service.
* @param[in,out] instance non-null service instance pointer
* @retval ERROR_INVALID_STATE if the instance is not stopped
* @retval ERROR_NONE on success
*/
error_t service_instance_destruct(struct ServiceInstance* instance);
/**
* @brief Get the manifest of a service instance.
* @param[in] instance non-null service instance pointer
* @return the manifest
*/
const struct ServiceManifest* service_instance_get_manifest(struct ServiceInstance* instance);
/**
* @brief Get the service of a service instance.
* @param[in] instance non-null service instance pointer
* @return the service
*/
struct Service* service_instance_get_service(struct ServiceInstance* instance);
/**
* @brief Get the state of a service instance.
* @param[in] instance non-null service instance pointer
* @return the current state
*/
ServiceState service_instance_get_state(struct ServiceInstance* instance);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/service/service.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Allocates and initializes a new Service instance.
* @param[in] context the manifest's context (ServiceManifest::context)
* @return the new service, never NULL
*/
typedef struct Service* (*ServiceCreate)(void* context);
/**
* Frees a Service instance that was created by the matching ServiceCreate function.
* @param[in] service the service to free
* @param[in] context the manifest's context (ServiceManifest::context)
*/
typedef void (*ServiceDestroy)(struct Service* service, void* context);
/**
* Describes a registrable service type.
* One manifest exists per service id, shared across start/stop cycles.
*/
struct ServiceManifest {
/** Unique service identifier. Should never be NULL. */
const char* id;
/** Allocates and initializes a new Service instance. Should never be NULL. */
ServiceCreate create_service;
/** Frees a Service instance created by create_service. Should never be NULL. */
ServiceDestroy destroy_service;
/** Opaque context passed to create_service and destroy_service. Can be NULL. */
void* context;
};
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Get the user data directory for a service. Survives OS upgrades. No trailing "/".
* @param[in] service_id non-null service id
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t service_paths_get_user_data_directory(const char* service_id, char* out_path, size_t out_path_size);
/**
* @brief Get a path within the user data directory for a service.
* @param[in] service_id non-null service id
* @param[in] child_path path without a "/" prefix
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t service_paths_get_user_data_path(const char* service_id, const char* child_path, char* out_path, size_t out_path_size);
/**
* @brief Get the assets directory for a service. Do not store configuration data here. No trailing "/".
* @param[in] service_id non-null service id
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t service_paths_get_assets_directory(const char* service_id, char* out_path, size_t out_path_size);
/**
* @brief Get a path within the assets directory for a service.
* @param[in] service_id non-null service id
* @param[in] child_path path without a "/" prefix
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t service_paths_get_assets_path(const char* service_id, const char* child_path, char* out_path, size_t out_path_size);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,82 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <tactility/error.h>
#include <tactility/service/service_context.h>
#include <tactility/service/service_instance.h>
#include <tactility/service/service_manifest.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Register a service manifest.
* @param[in] manifest non-null manifest to register
* @param[in] auto_start if true, the service is started immediately after registration
* @retval ERROR_INVALID_ARGUMENT if a manifest with the same id is already registered
* @retval ERROR_RESOURCE if auto_start is true and starting the service failed
* @retval ERROR_NONE on success
*/
error_t service_registration_add(const struct ServiceManifest* manifest, bool auto_start);
/**
* @brief Unregister a previously-added manifest.
* @param[in] id non-null service id
* @retval ERROR_INVALID_STATE if the service is still running
* @retval ERROR_NOT_FOUND if no manifest with this id is registered
* @retval ERROR_NONE on success
*/
error_t service_registration_remove(const char* id);
/**
* @brief Start a registered service by id.
* @param[in] id non-null service id
* @retval ERROR_NOT_FOUND if no manifest with this id is registered
* @retval ERROR_INVALID_STATE if the service is already running
* @retval ERROR_RESOURCE if the service's on_start callback failed
* @retval ERROR_NONE on success
*/
error_t service_registration_start(const char* id);
/**
* @brief Stop a running service by id.
* @param[in] id non-null service id
* @retval ERROR_NOT_FOUND if no service with this id is running
* @retval ERROR_NONE on success
*/
error_t service_registration_stop(const char* id);
/**
* @brief Get the state of a service by id.
* @param[in] id non-null service id
* @return the current state, or SERVICE_STATE_STOPPED if the id is unknown
*/
ServiceState service_registration_get_state(const char* id);
/**
* @brief Find a registered manifest by id.
* @param[in] id non-null service id
* @return the manifest, or NULL if not found
*/
const struct ServiceManifest* service_registration_find_manifest(const char* id);
/**
* @brief Find the context of a running service by id.
* @param[in] id non-null service id
* @return the context, or NULL if the service isn't running
*/
ServiceContext* service_registration_find_context(const char* id);
/**
* @brief Find the service instance of a running service by id.
* @param[in] id non-null service id
* @return the service, or NULL if not running
*/
struct Service* service_registration_find_service(const char* id);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/paths.h>
#include <tactility/device.h>
#include <tactility/drivers/sdcard.h>
#include <tactility/filesystem/file_system.h>
#include <cstdio>
#include <cstring>
static error_t get_user_data_root_path(char* out_path, size_t out_path_size) {
#if defined(CONFIG_TT_USER_DATA_LOCATION_INTERNAL)
#ifdef ESP_PLATFORM
const char* mount_point = "/data";
#else
const char* mount_point = "data";
#endif
if (std::strlen(mount_point) + 1 > out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
std::strcpy(out_path, mount_point);
return ERROR_NONE;
#elif defined(CONFIG_TT_USER_DATA_LOCATION_SD)
struct FileSystem* found = nullptr;
file_system_for_each(&found, [](FileSystem* fs, void* context) {
auto* owner = file_system_get_owner(fs);
if (owner == nullptr || device_get_type(owner) != &SDCARD_TYPE) {
return true;
}
*static_cast<FileSystem**>(context) = fs;
return false;
});
if (found == nullptr) {
return ERROR_NOT_FOUND;
}
return file_system_get_path(found, out_path, out_path_size);
#else
#error CONFIG_TT_USER_DATA_* not set or unsupported
#endif
}
extern "C" {
error_t paths_get_user_data_path(char* out_path, size_t out_path_size) {
#ifdef ESP_PLATFORM
char root[64];
error_t error = get_user_data_root_path(root, sizeof(root));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/tactility", root);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
#else
const char* fixed_path = "data";
if (std::strlen(fixed_path) + 1 > out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
std::strcpy(out_path, fixed_path);
return ERROR_NONE;
#endif
}
} // extern "C"

View File

@ -0,0 +1,81 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/service/service_instance.h>
#include <tactility/service/service_context.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>
#include <new>
#define TAG "service_instance"
struct ServiceInstanceInternal {
Mutex mutex {};
ServiceState state = SERVICE_STATE_STOPPED;
};
extern "C" {
error_t service_instance_construct(ServiceInstance* instance, const ServiceManifest* manifest) {
instance->internal = new(std::nothrow) ServiceInstanceInternal;
if (instance->internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
mutex_construct(&instance->internal->mutex);
instance->manifest = manifest;
instance->service = manifest->create_service(manifest->context);
LOG_D(TAG, "construct %s", manifest->id);
return ERROR_NONE;
}
error_t service_instance_destruct(ServiceInstance* instance) {
auto* internal = instance->internal;
mutex_lock(&internal->mutex);
if (internal->state != SERVICE_STATE_STOPPED) {
mutex_unlock(&internal->mutex);
return ERROR_INVALID_STATE;
}
mutex_unlock(&internal->mutex);
LOG_D(TAG, "destruct %s", instance->manifest->id);
instance->manifest->destroy_service(instance->service, instance->manifest->context);
instance->service = nullptr;
instance->internal = nullptr;
mutex_destruct(&internal->mutex);
delete internal;
return ERROR_NONE;
}
const ServiceManifest* service_instance_get_manifest(ServiceInstance* instance) {
return instance->manifest;
}
Service* service_instance_get_service(ServiceInstance* instance) {
return instance->service;
}
ServiceState service_instance_get_state(ServiceInstance* instance) {
mutex_lock(&instance->internal->mutex);
ServiceState state = instance->internal->state;
mutex_unlock(&instance->internal->mutex);
return state;
}
/** Internal-only: mutate the state of a service instance. Used by service_registration.cpp. */
void service_instance_set_state(ServiceInstance* instance, ServiceState state) {
mutex_lock(&instance->internal->mutex);
instance->internal->state = state;
mutex_unlock(&instance->internal->mutex);
}
const ServiceManifest* service_context_get_manifest(ServiceContext* context) {
return service_instance_get_manifest(context);
}
} // extern "C"

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/service/service_paths.h>
#include <tactility/paths.h>
#include <cstdio>
extern "C" {
error_t service_paths_get_user_data_directory(const char* service_id, char* out_path, size_t out_path_size) {
char root[192];
error_t error = paths_get_user_data_path(root, sizeof(root));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/service/%s", root, service_id);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t service_paths_get_user_data_path(const char* service_id, const char* child_path, char* out_path, size_t out_path_size) {
char directory[224];
error_t error = service_paths_get_user_data_directory(service_id, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t service_paths_get_assets_directory(const char* service_id, char* out_path, size_t out_path_size) {
char directory[224];
error_t error = service_paths_get_user_data_directory(service_id, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", directory);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t service_paths_get_assets_path(const char* service_id, const char* child_path, char* out_path, size_t out_path_size) {
char directory[224];
error_t error = service_paths_get_assets_directory(service_id, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
} // extern "C"

View File

@ -0,0 +1,207 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/service/service_registration.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>
#include <new>
#include <string>
#include <unordered_map>
#define TAG "service_registration"
// Defined in service_instance.cpp. Internal-only: lets the registry drive state
// transitions without exposing mutation on the public ServiceInstance API.
extern "C" void service_instance_set_state(ServiceInstance* instance, ServiceState state);
struct ManifestLedger {
std::unordered_map<std::string, const ServiceManifest*> manifests;
Mutex mutex {};
ManifestLedger() { mutex_construct(&mutex); }
~ManifestLedger() { mutex_destruct(&mutex); }
};
struct InstanceLedger {
std::unordered_map<std::string, ServiceInstance*> instances;
Mutex mutex {};
InstanceLedger() { mutex_construct(&mutex); }
~InstanceLedger() { mutex_destruct(&mutex); }
};
static ManifestLedger& get_manifest_ledger() {
static ManifestLedger ledger;
return ledger;
}
static InstanceLedger& get_instance_ledger() {
static InstanceLedger ledger;
return ledger;
}
#define manifest_ledger get_manifest_ledger()
#define instance_ledger get_instance_ledger()
extern "C" {
error_t service_registration_add(const ServiceManifest* manifest, bool auto_start) {
mutex_lock(&manifest_ledger.mutex);
if (manifest_ledger.manifests.contains(manifest->id)) {
mutex_unlock(&manifest_ledger.mutex);
LOG_E(TAG, "Manifest with id '%s' is already registered", manifest->id);
return ERROR_INVALID_ARGUMENT;
}
manifest_ledger.manifests[manifest->id] = manifest;
mutex_unlock(&manifest_ledger.mutex);
LOG_I(TAG, "add %s", manifest->id);
if (auto_start) {
return service_registration_start(manifest->id);
}
return ERROR_NONE;
}
error_t service_registration_remove(const char* id) {
if (service_registration_find_context(id) != nullptr) {
return ERROR_INVALID_STATE;
}
mutex_lock(&manifest_ledger.mutex);
const auto iterator = manifest_ledger.manifests.find(id);
if (iterator == manifest_ledger.manifests.end()) {
mutex_unlock(&manifest_ledger.mutex);
return ERROR_NOT_FOUND;
}
manifest_ledger.manifests.erase(iterator);
mutex_unlock(&manifest_ledger.mutex);
LOG_I(TAG, "remove %s", id);
return ERROR_NONE;
}
error_t service_registration_start(const char* id) {
mutex_lock(&manifest_ledger.mutex);
const auto manifest_iterator = manifest_ledger.manifests.find(id);
if (manifest_iterator == manifest_ledger.manifests.end()) {
mutex_unlock(&manifest_ledger.mutex);
return ERROR_NOT_FOUND;
}
const ServiceManifest* manifest = manifest_iterator->second;
mutex_unlock(&manifest_ledger.mutex);
mutex_lock(&instance_ledger.mutex);
if (instance_ledger.instances.contains(id)) {
mutex_unlock(&instance_ledger.mutex);
return ERROR_INVALID_STATE;
}
auto* instance = new(std::nothrow) ServiceInstance { .manifest = nullptr, .service = nullptr, .internal = nullptr };
if (instance == nullptr) {
mutex_unlock(&instance_ledger.mutex);
return ERROR_OUT_OF_MEMORY;
}
error_t error = service_instance_construct(instance, manifest);
if (error != ERROR_NONE) {
mutex_unlock(&instance_ledger.mutex);
delete instance;
return error;
}
// Register before on_start() so a service can find itself while starting.
instance_ledger.instances[id] = instance;
mutex_unlock(&instance_ledger.mutex);
service_instance_set_state(instance, SERVICE_STATE_STARTING);
LOG_I(TAG, "start %s", id);
Service* service = instance->service;
error = (service->on_start != nullptr) ? service->on_start(service, instance) : ERROR_NONE;
if (error == ERROR_NONE) {
service_instance_set_state(instance, SERVICE_STATE_STARTED);
return ERROR_NONE;
}
LOG_E(TAG, "Failed to start %s: %s", id, error_to_string(error));
service_instance_set_state(instance, SERVICE_STATE_STOPPED);
mutex_lock(&instance_ledger.mutex);
instance_ledger.instances.erase(id);
mutex_unlock(&instance_ledger.mutex);
service_instance_destruct(instance);
delete instance;
return ERROR_RESOURCE;
}
error_t service_registration_stop(const char* id) {
mutex_lock(&instance_ledger.mutex);
const auto iterator = instance_ledger.instances.find(id);
if (iterator == instance_ledger.instances.end()) {
mutex_unlock(&instance_ledger.mutex);
return ERROR_NOT_FOUND;
}
ServiceInstance* instance = iterator->second;
mutex_unlock(&instance_ledger.mutex);
LOG_I(TAG, "stop %s", id);
service_instance_set_state(instance, SERVICE_STATE_STOPPING);
Service* service = instance->service;
if (service->on_stop != nullptr) {
service->on_stop(service, instance);
}
service_instance_set_state(instance, SERVICE_STATE_STOPPED);
mutex_lock(&instance_ledger.mutex);
instance_ledger.instances.erase(id);
mutex_unlock(&instance_ledger.mutex);
service_instance_destruct(instance);
delete instance;
return ERROR_NONE;
}
ServiceState service_registration_get_state(const char* id) {
mutex_lock(&instance_ledger.mutex);
const auto iterator = instance_ledger.instances.find(id);
if (iterator == instance_ledger.instances.end()) {
mutex_unlock(&instance_ledger.mutex);
return SERVICE_STATE_STOPPED;
}
ServiceInstance* instance = iterator->second;
mutex_unlock(&instance_ledger.mutex);
return service_instance_get_state(instance);
}
const ServiceManifest* service_registration_find_manifest(const char* id) {
mutex_lock(&manifest_ledger.mutex);
const auto iterator = manifest_ledger.manifests.find(id);
const ServiceManifest* manifest = (iterator != manifest_ledger.manifests.end()) ? iterator->second : nullptr;
mutex_unlock(&manifest_ledger.mutex);
return manifest;
}
ServiceContext* service_registration_find_context(const char* id) {
mutex_lock(&instance_ledger.mutex);
const auto iterator = instance_ledger.instances.find(id);
ServiceInstance* instance = (iterator != instance_ledger.instances.end()) ? iterator->second : nullptr;
mutex_unlock(&instance_ledger.mutex);
return instance;
}
Service* service_registration_find_service(const char* id) {
ServiceInstance* instance = service_registration_find_context(id);
return (instance != nullptr) ? instance->service : nullptr;
}
} // extern "C"

View File

@ -0,0 +1,69 @@
#include "doctest.h"
#include <tactility/paths.h>
#include <tactility/service/service_paths.h>
#include <cstring>
#include <string>
TEST_CASE("paths_get_user_data_path returns a non-empty path") {
char buffer[192];
CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_NONE);
CHECK_GT(std::strlen(buffer), 0);
}
TEST_CASE("paths_get_user_data_path reports overflow for a too-small buffer") {
char buffer[1];
CHECK_EQ(paths_get_user_data_path(buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
}
TEST_CASE("service_paths_get_user_data_directory includes the service id") {
char root[192];
REQUIRE_EQ(paths_get_user_data_path(root, sizeof(root)), ERROR_NONE);
char buffer[224];
CHECK_EQ(service_paths_get_user_data_directory("my-service", buffer, sizeof(buffer)), ERROR_NONE);
std::string expected = std::string(root) + "/service/my-service";
CHECK_EQ(std::string(buffer), expected);
}
TEST_CASE("service_paths_get_user_data_path appends the child path") {
char directory[224];
REQUIRE_EQ(service_paths_get_user_data_directory("my-service", directory, sizeof(directory)), ERROR_NONE);
char buffer[256];
CHECK_EQ(service_paths_get_user_data_path("my-service", "settings.properties", buffer, sizeof(buffer)), ERROR_NONE);
std::string expected = std::string(directory) + "/settings.properties";
CHECK_EQ(std::string(buffer), expected);
}
TEST_CASE("service_paths_get_assets_directory is nested under the user data directory") {
char directory[224];
REQUIRE_EQ(service_paths_get_user_data_directory("my-service", directory, sizeof(directory)), ERROR_NONE);
char buffer[256];
CHECK_EQ(service_paths_get_assets_directory("my-service", buffer, sizeof(buffer)), ERROR_NONE);
std::string expected = std::string(directory) + "/assets";
CHECK_EQ(std::string(buffer), expected);
}
TEST_CASE("service_paths_get_assets_path appends the child path") {
char directory[224];
REQUIRE_EQ(service_paths_get_assets_directory("my-service", directory, sizeof(directory)), ERROR_NONE);
char buffer[256];
CHECK_EQ(service_paths_get_assets_path("my-service", "icon.png", buffer, sizeof(buffer)), ERROR_NONE);
std::string expected = std::string(directory) + "/icon.png";
CHECK_EQ(std::string(buffer), expected);
}
TEST_CASE("service_paths functions report overflow for a too-small buffer") {
char buffer[1];
CHECK_EQ(service_paths_get_user_data_directory("my-service", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
CHECK_EQ(service_paths_get_user_data_path("my-service", "child", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
CHECK_EQ(service_paths_get_assets_directory("my-service", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
CHECK_EQ(service_paths_get_assets_path("my-service", "child", buffer, sizeof(buffer)), ERROR_BUFFER_OVERFLOW);
}

View File

@ -0,0 +1,162 @@
#include "doctest.h"
#include <tactility/service/service_registration.h>
static int create_called = 0;
static int destroy_called = 0;
static int on_start_called = 0;
static int on_stop_called = 0;
static error_t on_start_result = ERROR_NONE;
static void* last_create_context = nullptr;
static void* last_destroy_context = nullptr;
static Service* test_create_service(void* context) {
create_called++;
last_create_context = context;
static Service service;
service.data = nullptr;
service.on_start = [](Service*, ServiceContext*) -> error_t {
on_start_called++;
return on_start_result;
};
service.on_stop = [](Service*, ServiceContext*) {
on_stop_called++;
};
return &service;
}
static void test_destroy_service(Service*, void* context) {
destroy_called++;
last_destroy_context = context;
}
static void reset_counters() {
create_called = 0;
destroy_called = 0;
on_start_called = 0;
on_stop_called = 0;
on_start_result = ERROR_NONE;
last_create_context = nullptr;
last_destroy_context = nullptr;
}
TEST_CASE("ServiceInstance construction and destruction") {
reset_counters();
static int context_marker = 0;
static const ServiceManifest manifest = {
.id = "instance-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service,
.context = &context_marker
};
ServiceInstance instance = { .manifest = nullptr, .service = nullptr, .internal = nullptr };
CHECK_EQ(service_instance_construct(&instance, &manifest), ERROR_NONE);
CHECK_NE(instance.internal, nullptr);
CHECK_EQ(instance.manifest, &manifest);
CHECK_NE(instance.service, nullptr);
CHECK_EQ(create_called, 1);
CHECK_EQ(last_create_context, &context_marker);
CHECK_EQ(service_instance_get_state(&instance), SERVICE_STATE_STOPPED);
CHECK_EQ(service_instance_destruct(&instance), ERROR_NONE);
CHECK_EQ(instance.internal, nullptr);
CHECK_EQ(destroy_called, 1);
CHECK_EQ(last_destroy_context, &context_marker);
}
TEST_CASE("service_registration_add rejects duplicate ids") {
reset_counters();
static const ServiceManifest manifest = {
.id = "duplicate-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service
};
CHECK_EQ(service_registration_add(&manifest, false), ERROR_NONE);
CHECK_EQ(service_registration_add(&manifest, false), ERROR_INVALID_ARGUMENT);
CHECK_EQ(service_registration_remove("duplicate-test"), ERROR_NONE);
}
TEST_CASE("service_registration start/stop lifecycle") {
reset_counters();
static const ServiceManifest manifest = {
.id = "lifecycle-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service
};
CHECK_EQ(service_registration_add(&manifest, false), ERROR_NONE);
CHECK_EQ(service_registration_get_state("lifecycle-test"), SERVICE_STATE_STOPPED);
CHECK_EQ(service_registration_start("lifecycle-test"), ERROR_NONE);
CHECK_EQ(on_start_called, 1);
CHECK_EQ(service_registration_get_state("lifecycle-test"), SERVICE_STATE_STARTED);
CHECK_NE(service_registration_find_context("lifecycle-test"), nullptr);
CHECK_NE(service_registration_find_service("lifecycle-test"), nullptr);
// Starting again while already started should fail
CHECK_EQ(service_registration_start("lifecycle-test"), ERROR_INVALID_STATE);
// Removing while running should fail
CHECK_EQ(service_registration_remove("lifecycle-test"), ERROR_INVALID_STATE);
CHECK_EQ(service_registration_stop("lifecycle-test"), ERROR_NONE);
CHECK_EQ(on_stop_called, 1);
CHECK_EQ(service_registration_get_state("lifecycle-test"), SERVICE_STATE_STOPPED);
CHECK_EQ(service_registration_find_context("lifecycle-test"), nullptr);
// Stopping again while already stopped should fail
CHECK_EQ(service_registration_stop("lifecycle-test"), ERROR_NOT_FOUND);
CHECK_EQ(service_registration_remove("lifecycle-test"), ERROR_NONE);
}
TEST_CASE("service_registration_add with auto_start") {
reset_counters();
static const ServiceManifest manifest = {
.id = "auto-start-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service
};
CHECK_EQ(service_registration_add(&manifest, true), ERROR_NONE);
CHECK_EQ(on_start_called, 1);
CHECK_EQ(service_registration_get_state("auto-start-test"), SERVICE_STATE_STARTED);
CHECK_EQ(service_registration_stop("auto-start-test"), ERROR_NONE);
CHECK_EQ(service_registration_remove("auto-start-test"), ERROR_NONE);
}
TEST_CASE("service_registration_start failure leaves service stopped") {
reset_counters();
on_start_result = ERROR_RESOURCE;
static const ServiceManifest manifest = {
.id = "failing-start-test",
.create_service = test_create_service,
.destroy_service = test_destroy_service
};
CHECK_EQ(service_registration_add(&manifest, false), ERROR_NONE);
CHECK_EQ(service_registration_start("failing-start-test"), ERROR_RESOURCE);
CHECK_EQ(service_registration_get_state("failing-start-test"), SERVICE_STATE_STOPPED);
CHECK_EQ(service_registration_find_context("failing-start-test"), nullptr);
CHECK_EQ(service_registration_remove("failing-start-test"), ERROR_NONE);
}
TEST_CASE("service_registration lookup functions with unknown id") {
CHECK_EQ(service_registration_get_state("unknown-service-id"), SERVICE_STATE_STOPPED);
CHECK_EQ(service_registration_find_manifest("unknown-service-id"), nullptr);
CHECK_EQ(service_registration_find_context("unknown-service-id"), nullptr);
CHECK_EQ(service_registration_find_service("unknown-service-id"), nullptr);
CHECK_EQ(service_registration_start("unknown-service-id"), ERROR_NOT_FOUND);
CHECK_EQ(service_registration_stop("unknown-service-id"), ERROR_NOT_FOUND);
CHECK_EQ(service_registration_remove("unknown-service-id"), ERROR_NOT_FOUND);
}