diff --git a/CODING_STYLE_C.md b/CODING_STYLE_C.md index f48219db6..3ef701889 100644 --- a/CODING_STYLE_C.md +++ b/CODING_STYLE_C.md @@ -1,6 +1,6 @@ # C coding Style -## Naming +## Files & Folders ### Files @@ -8,7 +8,7 @@ Files are lower snake case. - Files: `^[0-9a-z_]+$` - Directories: `^[0-9a-z_]+$` - + Example: ```c some_feature.c @@ -22,6 +22,8 @@ Project folders include: - `private` for private header files - `include` for projects that require separate header files +## C language + ### Macros and consts These are all upper snake case: @@ -94,3 +96,35 @@ Examples: ```c typedef uint32_t thread_id_t; ``` + +### Function comments + +```c +/** + * @brief Validates a number + * @param[in] number the integer to validate + * @return true if validation was succesful and there were no issues + */ +bool validate(int number); + +/** + * @brief Run the action. + * @param timeout[in] the maximum time the task should run + * @retval ERROR_TIMEOUT when the task couldn't be completed on time + * @retval ERROR_NONE when the task completed successfully + */ +error_t runAction(TickType_t timeout); + +/** + * @brief Increase a number. + * @param[inout] number + */ +void increase(int* number); + +/** + * A function with a longer description here. + * + * @brief short description + */ +void something(); +``` diff --git a/Platforms/PlatformEsp32/Source/drivers/esp32_i2c.cpp b/Platforms/PlatformEsp32/Source/drivers/esp32_i2c.cpp index 081ed2994..23c8067fa 100644 --- a/Platforms/PlatformEsp32/Source/drivers/esp32_i2c.cpp +++ b/Platforms/PlatformEsp32/Source/drivers/esp32_i2c.cpp @@ -9,6 +9,7 @@ #include #define TAG LOG_TAG(esp32_i2c) +#define ACK_CHECK_EN 1 struct InternalData { Mutex mutex { 0 }; @@ -30,8 +31,9 @@ struct InternalData { extern "C" { -static int read(Device* device, uint8_t address, uint8_t* data, size_t data_size, TickType_t timeout) { - vPortAssertIfInISR(); +static error_t read(Device* device, uint8_t address, uint8_t* data, size_t data_size, TickType_t timeout) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + if (data_size == 0) return ERROR_INVALID_ARGUMENT; auto* driver_data = GET_DATA(device); lock(driver_data); const esp_err_t esp_error = i2c_master_read_from_device(GET_CONFIG(device)->port, address, data, data_size, timeout); @@ -40,18 +42,19 @@ static int read(Device* device, uint8_t address, uint8_t* data, size_t data_size return esp_err_to_error(esp_error); } -static int write(Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { - vPortAssertIfInISR(); +static error_t write(Device* device, uint8_t address, const uint8_t* data, uint16_t data_size, TickType_t timeout) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + if (data_size == 0) return ERROR_INVALID_ARGUMENT; auto* driver_data = GET_DATA(device); lock(driver_data); - const esp_err_t esp_error = i2c_master_write_to_device(GET_CONFIG(device)->port, address, data, dataSize, timeout); + const esp_err_t esp_error = i2c_master_write_to_device(GET_CONFIG(device)->port, address, data, data_size, timeout); unlock(driver_data); ESP_ERROR_CHECK_WITHOUT_ABORT(esp_error); return esp_err_to_error(esp_error); } -static int write_read(Device* device, uint8_t address, const uint8_t* write_data, size_t write_data_size, uint8_t* read_data, size_t read_data_size, TickType_t timeout) { - vPortAssertIfInISR(); +static error_t write_read(Device* device, uint8_t address, const uint8_t* write_data, size_t write_data_size, uint8_t* read_data, size_t read_data_size, TickType_t timeout) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; auto* driver_data = GET_DATA(device); lock(driver_data); const esp_err_t esp_error = i2c_master_write_read_device(GET_CONFIG(device)->port, address, write_data, write_data_size, read_data, read_data_size, timeout); @@ -60,14 +63,63 @@ static int write_read(Device* device, uint8_t address, const uint8_t* write_data return esp_err_to_error(esp_error); } -static int start(Device* device) { +static error_t read_register(Device* device, uint8_t address, uint8_t reg, uint8_t* data, size_t data_size, TickType_t timeout) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + if (data_size == 0) return ERROR_INVALID_ARGUMENT; + auto* driver_data = GET_DATA(device); + + lock(driver_data); + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + // Set address pointer + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN); + i2c_master_write(cmd, ®, 1, ACK_CHECK_EN); + // Read length of response from current pointer + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_READ, ACK_CHECK_EN); + if (data_size > 1) { + i2c_master_read(cmd, data, data_size - 1, I2C_MASTER_ACK); + } + i2c_master_read_byte(cmd, data + data_size - 1, I2C_MASTER_NACK); + i2c_master_stop(cmd); + // TODO: We're passing an inaccurate timeout value as we already lost time with locking + esp_err_t esp_error = i2c_master_cmd_begin(GET_CONFIG(device)->port, cmd, timeout); + i2c_cmd_link_delete(cmd); + unlock(driver_data); + + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_error); + return esp_err_to_error(esp_error); +} + +static error_t write_register(Device* device, uint8_t address, uint8_t reg, const uint8_t* data, uint16_t data_size, TickType_t timeout) { + if (xPortInIsrContext()) return ERROR_ISR_STATUS; + if (data_size == 0) return ERROR_INVALID_ARGUMENT; + auto* driver_data = GET_DATA(device); + + lock(driver_data); + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, ACK_CHECK_EN); + i2c_master_write_byte(cmd, reg, ACK_CHECK_EN); + i2c_master_write(cmd, (uint8_t*) data, data_size, ACK_CHECK_EN); + i2c_master_stop(cmd); + // TODO: We're passing an inaccurate timeout value as we already lost time with locking + esp_err_t esp_error = i2c_master_cmd_begin(GET_CONFIG(device)->port, cmd, timeout); + i2c_cmd_link_delete(cmd); + unlock(driver_data); + + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_error); + return esp_err_to_error(esp_error); +} + +static error_t start(Device* device) { ESP_LOGI(TAG, "start %s", device->name); auto* data = new InternalData(); device_set_driver_data(device, data); return ERROR_NONE; } -static int stop(Device* device) { +static error_t stop(Device* device) { ESP_LOGI(TAG, "stop %s", device->name); auto* driver_data = static_cast(device_get_driver_data(device)); device_set_driver_data(device, nullptr); @@ -75,10 +127,13 @@ static int stop(Device* device) { return ERROR_NONE; } + const static I2cControllerApi esp32_i2c_api = { .read = read, .write = write, - .write_read = write_read + .write_read = write_read, + .read_register = read_register, + .write_register = write_register }; Driver esp32_i2c_driver = { diff --git a/TactilityKernel/CMakeLists.txt b/TactilityKernel/CMakeLists.txt index 5644a3178..780a9782e 100644 --- a/TactilityKernel/CMakeLists.txt +++ b/TactilityKernel/CMakeLists.txt @@ -7,6 +7,8 @@ if (DEFINED ENV{ESP_IDF_VERSION}) idf_component_register( SRCS ${SOURCES} INCLUDE_DIRS "Include/" + # TODO move the related logic for esp_time in Tactility/time.h into the Platform/ subproject + REQUIRES esp_timer ) else () diff --git a/TactilityKernel/Include/tactility/concurrent/thread.h b/TactilityKernel/Include/tactility/concurrent/thread.h new file mode 100644 index 000000000..b78a57145 --- /dev/null +++ b/TactilityKernel/Include/tactility/concurrent/thread.h @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "tactility/error.h" +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef ESP_PLATFORM +#include +#endif + +#include +#include + +#include +#include +#include + +typedef enum { + THREAD_STATE_STOPPED, + THREAD_STATE_STARTING, + THREAD_STATE_RUNNING, +} ThreadState; + +/** ThreadPriority */ +enum ThreadPriority { + THREAD_PRIORITY_NONE = 0U, + THREAD_PRIORITY_IDLE = 1U, + THREAD_PRIORITY_LOWER = 2U, + THREAD_PRIORITY_LOW = 3U, + THREAD_PRIORITY_NORMAL = 4U, + THREAD_PRIORITY_HIGH = 5U, + THREAD_PRIORITY_HIGHER = 6U, + THREAD_PRIORITY_CRITICAL = 7U +}; + +typedef int32_t (*thread_main_fn_t)(void* context); +typedef void (*thread_state_callback_t)(ThreadState state, void* context); + +struct Thread; +typedef struct Thread Thread; + +/** + * @brief Creates a new thread instance with default settings. + * @return A pointer to the created Thread instance, or NULL if allocation failed. + */ +Thread* thread_alloc(void); + +/** + * @brief Creates a new thread instance with specified parameters. + * @param[in] name The name of the thread. + * @param[in] stack_size The size of the thread stack in bytes. + * @param[in] function The main function to be executed by the thread. + * @param[in] function_context A pointer to the context to be passed to the main function. + * @param[in] affinity The CPU core affinity for the thread (e.g., tskNO_AFFINITY). + * @return A pointer to the created Thread instance, or NULL if allocation failed. + */ +Thread* thread_alloc_full( + const char* name, + configSTACK_DEPTH_TYPE stack_size, + thread_main_fn_t function, + void* function_context, + portBASE_TYPE affinity +); + +/** + * @brief Destroys a thread instance. + * @param[in] thread The thread instance to destroy. + * @note The thread must be in the STOPPED state. + */ +void thread_free(Thread* thread); + +/** + * @brief Sets the name of the thread. + * @param[in] thread The thread instance. + * @param[in] name The new name for the thread. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_name(Thread* thread, const char* name); + +/** + * @brief Sets the stack size for the thread. + * @param[in] thread The thread instance. + * @param[in] stack_size The stack size in bytes. Must be a multiple of 4. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_stack_size(Thread* thread, size_t stack_size); + +/** + * @brief Sets the CPU core affinity for the thread. + * @param[in] thread The thread instance. + * @param[in] affinity The CPU core affinity. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_affinity(Thread* thread, portBASE_TYPE affinity); + +/** + * @brief Sets the main function and context for the thread. + * @param[in] thread The thread instance. + * @param[in] function The main function to be executed. + * @param[in] context A pointer to the context to be passed to the main function. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_main_function(Thread* thread, thread_main_fn_t function, void* context); + +/** + * @brief Sets the priority for the thread. + * @param[in] thread The thread instance. + * @param[in] priority The thread priority. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_priority(Thread* thread, enum ThreadPriority priority); + +/** + * @brief Sets a callback to be invoked when the thread state changes. + * @param[in] thread The thread instance. + * @param[in] callback The callback function. + * @param[in] context A pointer to the context to be passed to the callback function. + * @note Can only be called when the thread is in the STOPPED state. + */ +void thread_set_state_callback(Thread* thread, thread_state_callback_t callback, void* context); + +/** + * @brief Gets the current state of the thread. + * @param[in] thread The thread instance. + * @return The current ThreadState. + */ +ThreadState thread_get_state(Thread* thread); + +/** + * @brief Starts the thread execution. + * @param[in] thread The thread instance. + * @note The thread must be in the STOPPED state and have a main function set. + * @retval ERROR_NONE when the thread was started + * @retval ERROR_UNDEFINED when the thread failed to start + */ +error_t thread_start(Thread* thread); + +/** + * @brief Waits for the thread to finish execution. + * @param[in] thread The thread instance. + * @param[in] timeout The maximum time to wait in ticks. + * @param[in] poll_interval The interval between status checks in ticks. + * @retval ERROR_NONE when the thread was stopped + * @retval ERROR_TIMEOUT when the thread was not stopped because the timeout has passed + * @note Cannot be called from the thread being joined. + */ +error_t thread_join(Thread* thread, TickType_t timeout, TickType_t poll_interval); + +/** + * @brief Gets the FreeRTOS task handle associated with the thread. + * @param[in] thread The thread instance. + * @return The TaskHandle_t, or NULL if the thread is not running. + */ +TaskHandle_t thread_get_task_handle(Thread* thread); + +/** + * @brief Gets the return code from the thread's main function. + * @param[in] thread The thread instance. + * @return The return code of the thread's main function. + * @note The thread must be in the STOPPED state. + */ +int32_t thread_get_return_code(Thread* thread); + +/** + * @brief Gets the minimum remaining stack space for the thread since it started. + * @param[in] thread The thread instance. + * @return The minimum remaining stack space in bytes. + * @note The thread must be in the RUNNING state. + */ +uint32_t thread_get_stack_space(Thread* thread); + +/** + * @brief Gets the current thread instance. + * @return A pointer to the current Thread instance, or NULL if not called from a thread created by this module. + */ +Thread* thread_get_current(void); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/Include/tactility/concurrent/timer.h b/TactilityKernel/Include/tactility/concurrent/timer.h new file mode 100644 index 000000000..7a3e7c2aa --- /dev/null +++ b/TactilityKernel/Include/tactility/concurrent/timer.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include "tactility/error.h" +#include "tactility/freertos/timers.h" +#include "tactility/concurrent/thread.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum TimerType { + TIMER_TYPE_ONCE = 0, // Timer triggers once after time has passed + TIMER_TYPE_PERIODIC = 1 // Timer triggers repeatedly after time has passed +}; + +typedef void (*timer_callback_t)(void* context); +typedef void (*timer_pending_callback_t)(void* context, uint32_t arg); + +struct Timer; + +/** + * @brief Creates a new timer instance. + * @param[in] type The timer type. + * @param[in] ticks The timer period in ticks. + * @param[in] callback The callback function. + * @param[in] context The context to pass to the callback function. + * @return A pointer to the created timer instance, or NULL if allocation failed. + */ +struct Timer* timer_alloc(enum TimerType type, TickType_t ticks, timer_callback_t callback, void* context); + +/** + * @brief Destroys a timer instance. + * @param[in] timer The timer instance to destroy. + */ +void timer_free(struct Timer* timer); + +/** + * @brief Starts the timer. + * @param[in] timer The timer instance. + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_start(struct Timer* timer); + +/** + * @brief Stops the timer. + * @param[in] timer The timer instance. + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_stop(struct Timer* timer); + +/** + * @brief Set a new interval and reset the timer. + * @param[in] timer The timer instance. + * @param[in] interval The new timer interval in ticks. + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_reset_with_interval(struct Timer* timer, TickType_t interval); + +/** + * @brief Reset the timer. + * @param[in] timer The timer instance. + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_reset(struct Timer* timer); + +/** + * @brief Check if the timer is running. + * @param[in] timer The timer instance. + * @return true when the timer is running. + */ +bool timer_is_running(struct Timer* timer); + +/** + * @brief Gets the expiry time of the timer. + * @param[in] timer The timer instance. + * @return The expiry time in ticks. + */ +TickType_t timer_get_expiry_time(struct Timer* timer); + +/** + * @brief Calls xTimerPendFunctionCall internally. + * @param[in] timer The timer instance. + * @param[in] callback the function to call + * @param[in] context the first function argument + * @param[in] arg the second function argument + * @param[in] timeout the function timeout (must set to 0 in ISR mode) + * @return ERROR_NONE on success, ERROR_TIMEOUT if the command queue was full. + */ +error_t timer_set_pending_callback(struct Timer* timer, timer_pending_callback_t callback, void* context, uint32_t arg, TickType_t timeout); + +/** + * @brief Set callback priority (priority of the timer daemon task). + * @param[in] timer The timer instance. + * @param[in] priority The priority. + */ +void timer_set_callback_priority(struct Timer* timer, enum ThreadPriority priority); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/Include/tactility/drivers/gpio_controller.h b/TactilityKernel/Include/tactility/drivers/gpio_controller.h index 15038dc81..ef6dfdd2c 100644 --- a/TactilityKernel/Include/tactility/drivers/gpio_controller.h +++ b/TactilityKernel/Include/tactility/drivers/gpio_controller.h @@ -10,17 +10,85 @@ extern "C" { #include struct GpioControllerApi { + /** + * @brief Sets the logical level of a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[in] high true to set the pin high, false to set it low + * @return ERROR_NONE if successful + */ error_t (*set_level)(struct Device* device, gpio_pin_t pin, bool high); + + /** + * @brief Gets the logical level of a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[out] high pointer to store the pin level + * @return ERROR_NONE if successful + */ error_t (*get_level)(struct Device* device, gpio_pin_t pin, bool* high); + + /** + * @brief Configures the options for a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[in] options configuration flags (direction, pull-up/down, etc.) + * @return ERROR_NONE if successful + */ error_t (*set_options)(struct Device* device, gpio_pin_t pin, gpio_flags_t options); + + /** + * @brief Gets the configuration options for a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[out] options pointer to store the configuration flags + * @return ERROR_NONE if successful + */ error_t (*get_options)(struct Device* device, gpio_pin_t pin, gpio_flags_t* options); }; +/** + * @brief Sets the logical level of a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[in] high true to set the pin high, false to set it low + * @return ERROR_NONE if successful + */ error_t gpio_controller_set_level(struct Device* device, gpio_pin_t pin, bool high); + +/** + * @brief Gets the logical level of a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[out] high pointer to store the pin level + * @return ERROR_NONE if successful + */ error_t gpio_controller_get_level(struct Device* device, gpio_pin_t pin, bool* high); + +/** + * @brief Configures the options for a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[in] options configuration flags (direction, pull-up/down, etc.) + * @return ERROR_NONE if successful + */ error_t gpio_controller_set_options(struct Device* device, gpio_pin_t pin, gpio_flags_t options); + +/** + * @brief Gets the configuration options for a GPIO pin. + * @param[in] device the GPIO controller device + * @param[in] pin the pin index + * @param[out] options pointer to store the configuration flags + * @return ERROR_NONE if successful + */ error_t gpio_controller_get_options(struct Device* device, gpio_pin_t pin, gpio_flags_t* options); +/** + * @brief Configures the options for a GPIO pin using a pin configuration structure. + * @param[in] device the GPIO controller device + * @param[in] config the pin configuration structure + * @return ERROR_NONE if successful + */ static inline error_t gpio_set_options_config(struct Device* device, const struct GpioPinConfig* config) { return gpio_controller_set_options(device, config->pin, config->flags); } diff --git a/TactilityKernel/Include/tactility/drivers/i2c_controller.h b/TactilityKernel/Include/tactility/drivers/i2c_controller.h index 2995002bc..fe54628f2 100644 --- a/TactilityKernel/Include/tactility/drivers/i2c_controller.h +++ b/TactilityKernel/Include/tactility/drivers/i2c_controller.h @@ -13,18 +13,154 @@ extern "C" { #include #include +/** + * @brief API for I2C controller drivers. + */ struct I2cControllerApi { + /** + * @brief Reads data from an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[out] data the buffer to store the read data + * @param[in] dataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the read operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ error_t (*read)(struct Device* device, uint8_t address, uint8_t* data, size_t dataSize, TickType_t timeout); + + /** + * @brief Writes data to an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] data the buffer containing the data to write + * @param[in] dataSize the number of bytes to write + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the write operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ error_t (*write)(struct Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout); + + /** + * @brief Writes data to then reads data from an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] writeData the buffer containing the data to write + * @param[in] writeDataSize the number of bytes to write + * @param[out] readData the buffer to store the read data + * @param[in] readDataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ error_t (*write_read)(struct Device* device, uint8_t address, const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout); + + /** + * @brief Reads data from a register of an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] reg the register address to read from + * @param[out] data the buffer to store the read data + * @param[in] dataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the read operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ + error_t (*read_register)(struct Device* device, uint8_t address, uint8_t reg, uint8_t* data, size_t dataSize, TickType_t timeout); + + /** + * @brief Writes data to a register of an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] reg the register address to write to + * @param[in] data the buffer containing the data to write + * @param[in] dataSize the number of bytes to write + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the write operation was successful + * @retval ERROR_TIMEOUT when the operation timed out + */ + error_t (*write_register)(struct Device* device, uint8_t address, uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout); }; +/** + * @brief Reads data from an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[out] data the buffer to store the read data + * @param[in] dataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the read operation was successful + */ error_t i2c_controller_read(struct Device* device, uint8_t address, uint8_t* data, size_t dataSize, TickType_t timeout); +/** + * @brief Writes data to an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] data the buffer containing the data to write + * @param[in] dataSize the number of bytes to write + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the write operation was successful + */ error_t i2c_controller_write(struct Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout); +/** + * @brief Writes data to then reads data from an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] writeData the buffer containing the data to write + * @param[in] writeDataSize the number of bytes to write + * @param[out] readData the buffer to store the read data + * @param[in] readDataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the operation was successful + */ error_t i2c_controller_write_read(struct Device* device, uint8_t address, const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout); +/** + * @brief Reads data from a register of an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] reg the register address to read from + * @param[out] data the buffer to store the read data + * @param[in] dataSize the number of bytes to read + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the read operation was successful + */ +error_t i2c_controller_read_register(struct Device* device, uint8_t address, uint8_t reg, uint8_t* data, size_t dataSize, TickType_t timeout); + +/** + * @brief Writes data to a register of an I2C device using the specified controller. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] reg the register address to write to + * @param[in] data the buffer containing the data to write + * @param[in] dataSize the number of bytes to write + * @param[in] timeout the maximum time to wait for the operation to complete + * @retval ERROR_NONE when the write operation was successful + */ +error_t i2c_controller_write_register(struct Device* device, uint8_t address, uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout); + +/** + * @brief Writes an array of register-value pairs to an I2C device. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address of the slave device + * @param[in] data an array of bytes where even indices are register addresses and odd indices are values + * @param[in] dataSize the number of bytes in the data array (must be even) + * @param[in] timeout the maximum time to wait for each operation to complete + * @retval ERROR_NONE when all write operations were successful + */ +error_t i2c_controller_write_register_array(struct Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout); + +/** + * @brief Checks if an I2C device is present at the specified address. + * @param[in] device the I2C controller device + * @param[in] address the 7-bit I2C address to check + * @param[in] timeout the maximum time to wait for the check to complete + * @retval ERROR_NONE when a device responded at the address + */ +error_t i2c_controller_has_device_at_address(struct Device* device, uint8_t address, TickType_t timeout); + extern const struct DeviceType I2C_CONTROLLER_TYPE; #ifdef __cplusplus diff --git a/TactilityKernel/Source/concurrent/dispatcher.cpp b/TactilityKernel/Source/concurrent/dispatcher.cpp index 9fbc8f2a6..41873c058 100644 --- a/TactilityKernel/Source/concurrent/dispatcher.cpp +++ b/TactilityKernel/Source/concurrent/dispatcher.cpp @@ -11,7 +11,7 @@ #include #include -#define TAG LOG_TAG("Dispatcher") +#define TAG LOG_TAG(Dispatcher) static constexpr EventBits_t BACKPRESSURE_WARNING_COUNT = 100U; static constexpr EventBits_t WAIT_FLAG = 1U; diff --git a/TactilityKernel/Source/concurrent/thread.cpp b/TactilityKernel/Source/concurrent/thread.cpp new file mode 100644 index 000000000..c7e6f5cbe --- /dev/null +++ b/TactilityKernel/Source/concurrent/thread.cpp @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static const size_t LOCAL_STORAGE_SELF_POINTER_INDEX = 0; +static const char* TAG = LOG_TAG(Thread); + +struct Thread { + TaskHandle_t taskHandle = nullptr; + ThreadState state = THREAD_STATE_STOPPED; + thread_main_fn_t mainFunction = nullptr; + void* mainFunctionContext = nullptr; + int32_t callbackResult = 0; + thread_state_callback_t stateCallback = nullptr; + void* stateCallbackContext = nullptr; + std::string name = "unnamed"; + enum ThreadPriority priority = THREAD_PRIORITY_NORMAL; + struct Mutex mutex = { 0 }; + configSTACK_DEPTH_TYPE stackSize = 4096; + portBASE_TYPE affinity = -1; + + Thread() { + mutex_construct(&mutex); + } + + ~Thread() { + mutex_destruct(&mutex); + } + + void lock() { mutex_lock(&mutex); } + + void unlock() { mutex_unlock(&mutex); } +}; + +static void thread_set_state_internal(Thread* thread, ThreadState newState) { + thread->lock(); + thread->state = newState; + auto cb = thread->stateCallback; + auto cb_ctx = thread->stateCallbackContext; + thread->unlock(); + if (cb) { + cb(newState, cb_ctx); + } +} + +static void thread_main_body(void* context) { + check(context != nullptr); + auto* thread = static_cast(context); + + // Save Thread instance pointer to task local storage + check(pvTaskGetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX) == nullptr); + vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, thread); + + LOG_I(TAG, "Starting %s", thread->name.c_str()); // No need to lock as we don't allow mutation after thread start + check(thread->state == THREAD_STATE_STARTING); + thread_set_state_internal(thread, THREAD_STATE_RUNNING); + + int32_t result = thread->mainFunction(thread->mainFunctionContext); + thread->lock(); + thread->callbackResult = result; + thread->unlock(); + + check(thread->state == THREAD_STATE_RUNNING); + thread_set_state_internal(thread, THREAD_STATE_STOPPED); + LOG_I(TAG, "Stopped %s", thread->name.c_str()); // No need to lock as we don't allow mutation after thread start + + vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, nullptr); + + thread->lock(); + thread->taskHandle = nullptr; + thread->unlock(); + + vTaskDelete(nullptr); +} + +extern "C" { + +Thread* thread_alloc(void) { + auto* thread = new(std::nothrow) Thread(); + if (thread == nullptr) { + return nullptr; + } + return thread; +} + +Thread* thread_alloc_full( + const char* name, + configSTACK_DEPTH_TYPE stack_size, + thread_main_fn_t function, + void* function_context, + portBASE_TYPE affinity +) { + auto* thread = new(std::nothrow) Thread(); + if (thread != nullptr) { + thread_set_name(thread, name); + thread_set_stack_size(thread, stack_size); + thread_set_main_function(thread, function, function_context); + thread_set_affinity(thread, affinity); + } + return thread; +} + +void thread_free(Thread* thread) { + check(thread); + check(thread->state == THREAD_STATE_STOPPED); + check(thread->taskHandle == nullptr); + delete thread; +} + +void thread_set_name(Thread* thread, const char* name) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->name = name; + thread->unlock(); +} + +void thread_set_stack_size(Thread* thread, size_t stack_size) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->stackSize = stack_size; + thread->unlock(); +} + +void thread_set_affinity(Thread* thread, portBASE_TYPE affinity) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->affinity = affinity; + thread->unlock(); +} + +void thread_set_main_function(Thread* thread, thread_main_fn_t function, void* context) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->mainFunction = function; + thread->mainFunctionContext = context; + thread->unlock(); +} + +void thread_set_priority(Thread* thread, enum ThreadPriority priority) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->priority = priority; + thread->unlock(); +} + +void thread_set_state_callback(Thread* thread, thread_state_callback_t callback, void* context) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + thread->stateCallback = callback; + thread->stateCallbackContext = context; + thread->unlock(); +} + +ThreadState thread_get_state(Thread* thread) { + check(xPortInIsrContext() == pdFALSE); + thread->lock(); + ThreadState state = thread->state; + thread->unlock(); + return state; +} + +error_t thread_start(Thread* thread) { + thread->lock(); + check(thread->mainFunction != nullptr); + check(thread->state == THREAD_STATE_STOPPED); + check(thread->stackSize); + thread->unlock(); + + thread_set_state_internal(thread, THREAD_STATE_STARTING); + + thread->lock(); + uint32_t stack_depth = thread->stackSize / sizeof(StackType_t); + enum ThreadPriority priority = thread->priority; + portBASE_TYPE affinity = thread->affinity; + thread->unlock(); + + BaseType_t result; + if (affinity != -1) { +#ifdef ESP_PLATFORM + result = xTaskCreatePinnedToCore( + thread_main_body, + thread->name.c_str(), + stack_depth, + thread, + (UBaseType_t)priority, + &thread->taskHandle, + affinity + ); +#else + result = xTaskCreate( + thread_main_body, + thread->name.c_str(), + stack_depth, + thread, + (UBaseType_t)priority, + &thread->taskHandle + ); +#endif + } else { + result = xTaskCreate( + thread_main_body, + thread->name.c_str(), + stack_depth, + thread, + (UBaseType_t)priority, + &thread->taskHandle + ); + } + + if (result != pdPASS) { + thread_set_state_internal(thread, THREAD_STATE_STOPPED); + thread->lock(); + thread->taskHandle = nullptr; + thread->unlock(); + return ERROR_UNDEFINED; + } + + return ERROR_NONE; +} + +error_t thread_join(Thread* thread, TickType_t timeout, TickType_t poll_interval) { + check(thread_get_current() != thread); + + TickType_t start_ticks = get_ticks(); + while (thread_get_task_handle(thread)) { + delay_ticks(poll_interval); + if (get_ticks() - start_ticks > timeout) { + return ERROR_TIMEOUT; + } + } + + return ERROR_NONE; +} + +TaskHandle_t thread_get_task_handle(Thread* thread) { + thread->lock(); + auto* handle = thread->taskHandle; + thread->unlock(); + return handle; +} + +int32_t thread_get_return_code(Thread* thread) { + thread->lock(); + check(thread->state == THREAD_STATE_STOPPED); + auto result = thread->callbackResult; + thread->unlock(); + return result; +} + +uint32_t thread_get_stack_space(Thread* thread) { + if (xPortInIsrContext() == pdTRUE) { + return 0; + } + thread->lock(); + assert(thread->state == THREAD_STATE_RUNNING); + auto result = uxTaskGetStackHighWaterMark(thread->taskHandle) * sizeof(StackType_t); + thread->unlock(); + return result; +} + +Thread* thread_get_current(void) { + check(xPortInIsrContext() == pdFALSE); + return (Thread*)pvTaskGetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX); +} + +} diff --git a/TactilityKernel/Source/concurrent/timer.c b/TactilityKernel/Source/concurrent/timer.c new file mode 100644 index 000000000..6360ab2ac --- /dev/null +++ b/TactilityKernel/Source/concurrent/timer.c @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include + +struct Timer { + TimerHandle_t handle; + timer_callback_t callback; + void* context; +}; + +static void timer_callback_internal(TimerHandle_t xTimer) { + struct Timer* timer = (struct Timer*)pvTimerGetTimerID(xTimer); + if (timer != NULL && timer->callback != NULL) { + timer->callback(timer->context); + } +} + +struct Timer* timer_alloc(enum TimerType type, TickType_t ticks, timer_callback_t callback, void* context) { + check(xPortInIsrContext() == pdFALSE); + check(callback != NULL); + + struct Timer* timer = (struct Timer*)malloc(sizeof(struct Timer)); + if (timer == NULL) { + return NULL; + } + + timer->callback = callback; + timer->context = context; + + BaseType_t auto_reload = (type == TIMER_TYPE_ONCE) ? pdFALSE : pdTRUE; + timer->handle = xTimerCreate(NULL, ticks, auto_reload, timer, timer_callback_internal); + + if (timer->handle == NULL) { + free(timer); + return NULL; + } + + return timer; +} + +void timer_free(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + // MAX_TICKS or a reasonable timeout for the timer command queue + xTimerDelete(timer->handle, portMAX_DELAY); + free(timer); +} + +error_t timer_start(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return (xTimerStart(timer->handle, portMAX_DELAY) == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +error_t timer_stop(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return (xTimerStop(timer->handle, portMAX_DELAY) == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +error_t timer_reset_with_interval(struct Timer* timer, TickType_t interval) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + if (xTimerChangePeriod(timer->handle, interval, portMAX_DELAY) != pdPASS) { + return ERROR_TIMEOUT; + } + return (xTimerReset(timer->handle, portMAX_DELAY) == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +error_t timer_reset(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return (xTimerReset(timer->handle, portMAX_DELAY) == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +bool timer_is_running(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return xTimerIsTimerActive(timer->handle) != pdFALSE; +} + +TickType_t timer_get_expiry_time(struct Timer* timer) { + check(xPortInIsrContext() == pdFALSE); + check(timer != NULL); + return xTimerGetExpiryTime(timer->handle); +} + +error_t timer_set_pending_callback(struct Timer* timer, timer_pending_callback_t callback, void* context, uint32_t arg, TickType_t timeout) { + (void)timer; // Unused in this implementation but kept for API consistency if needed later + BaseType_t result; + if (xPortInIsrContext() == pdTRUE) { + check(timeout == 0); + result = xTimerPendFunctionCallFromISR(callback, context, arg, NULL); + } else { + result = xTimerPendFunctionCall(callback, context, arg, timeout); + } + return (result == pdPASS) ? ERROR_NONE : ERROR_TIMEOUT; +} + +void timer_set_callback_priority(struct Timer* timer, enum ThreadPriority priority) { + (void)timer; // Unused in this implementation but kept for API consistency if needed later + check(xPortInIsrContext() == pdFALSE); + + TaskHandle_t task_handle = xTimerGetTimerDaemonTaskHandle(); + check(task_handle != NULL); + + vTaskPrioritySet(task_handle, (UBaseType_t)priority); +} diff --git a/TactilityKernel/Source/crash.cpp b/TactilityKernel/Source/crash.cpp index ff728d9fc..846383ca2 100644 --- a/TactilityKernel/Source/crash.cpp +++ b/TactilityKernel/Source/crash.cpp @@ -1,7 +1,7 @@ #include #include -static const auto* TAG = LOG_TAG("Kernel"); +static const auto* TAG = LOG_TAG(Kernel); static void log_memory_info() { #ifdef ESP_PLATFORM diff --git a/TactilityKernel/Source/drivers/i2c_controller.cpp b/TactilityKernel/Source/drivers/i2c_controller.cpp index 1efaf2565..be3af8c70 100644 --- a/TactilityKernel/Source/drivers/i2c_controller.cpp +++ b/TactilityKernel/Source/drivers/i2c_controller.cpp @@ -23,6 +23,34 @@ error_t i2c_controller_write_read(Device* device, uint8_t address, const uint8_t return I2C_DRIVER_API(driver)->write_read(device, address, writeData, writeDataSize, readData, readDataSize, timeout); } +error_t i2c_controller_read_register(Device* device, uint8_t address, uint8_t reg, uint8_t* data, size_t dataSize, TickType_t timeout) { + const auto* driver = device_get_driver(device); + return I2C_DRIVER_API(driver)->read_register(device, address, reg, data, dataSize, timeout); +} + +error_t i2c_controller_write_register(Device* device, uint8_t address, uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + const auto* driver = device_get_driver(device); + return I2C_DRIVER_API(driver)->write_register(device, address, reg, data, dataSize, timeout); +} + +error_t i2c_controller_write_register_array(Device* device, uint8_t address, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + const auto* driver = device_get_driver(device); + if (dataSize % 2 != 0) { + return ERROR_INVALID_ARGUMENT; + } + for (int i = 0; i < dataSize; i += 2) { + error_t error = I2C_DRIVER_API(driver)->write_register(device, address, data[i], &data[i + 1], 1, timeout); + if (error != ERROR_NONE) return error; + } + return ERROR_NONE; +} + +error_t i2c_controller_has_device_at_address(Device* device, uint8_t address, TickType_t timeout) { + const auto* driver = device_get_driver(device); + uint8_t message[2] = { 0, 0 }; + return I2C_DRIVER_API(driver)->write(device, address, message, 2, timeout); +} + const struct DeviceType I2C_CONTROLLER_TYPE { 0 }; } diff --git a/Tests/TactilityKernel/ThreadTest.cpp b/Tests/TactilityKernel/ThreadTest.cpp new file mode 100644 index 000000000..1bc4e9f05 --- /dev/null +++ b/Tests/TactilityKernel/ThreadTest.cpp @@ -0,0 +1,112 @@ +#include "doctest.h" + +#include +#include + +TEST_CASE("when a thread is started then its callback should be called") { + bool has_called = false; + auto* thread = thread_alloc_full( + "immediate return task", + 4096, + [](void* context) { + auto* has_called_ptr = static_cast(context); + *has_called_ptr = true; + return 0; + }, + &has_called, + -1 + ); + + CHECK(!has_called); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + thread_free(thread); + CHECK(has_called); +} + +TEST_CASE("a thread can be started and stopped") { + bool interrupted = false; + auto* thread = thread_alloc_full( + "interruptable thread", + 4096, + [](void* context) { + auto* interrupted_ptr = static_cast(context); + while (!*interrupted_ptr) { + delay_millis(1); + } + return 0; + }, + &interrupted, + -1 + ); + + CHECK(thread); + CHECK_EQ(thread_start(thread), ERROR_NONE); + interrupted = true; + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + thread_free(thread); +} + +TEST_CASE("thread id should only be set at when thread is started") { + bool interrupted = false; + auto* thread = thread_alloc_full( + "interruptable thread", + 4096, + [](void* context) { + auto* interrupted_ptr = static_cast(context); + while (!*interrupted_ptr) { + delay_millis(1); + } + return 0; + }, + &interrupted, + -1 + ); + CHECK_EQ(thread_get_task_handle(thread), nullptr); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_NE(thread_get_task_handle(thread), nullptr); + interrupted = true; + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + CHECK_EQ(thread_get_task_handle(thread), nullptr); + thread_free(thread); +} + +TEST_CASE("thread state should be correct") { + bool interrupted = false; + auto* thread = thread_alloc_full( + "interruptable thread", + 4096, + [](void* context) { + auto* interrupted_ptr = static_cast(context); + while (!*interrupted_ptr) { + delay_millis(1); + } + return 0; + }, + &interrupted, + -1 + + ); + CHECK_EQ(thread_get_state(thread), THREAD_STATE_STOPPED); + thread_start(thread); + auto state = thread_get_state(thread); + CHECK((state == THREAD_STATE_STARTING || state == THREAD_STATE_RUNNING)); + interrupted = true; + CHECK_EQ(thread_join(thread, 10, 1), ERROR_NONE); + CHECK_EQ(thread_get_state(thread), THREAD_STATE_STOPPED); + thread_free(thread); +} + +TEST_CASE("thread id should only be set at when thread is started") { + auto* thread = thread_alloc_full( + "return code", + 4096, + [](void* context) { return 123; }, + nullptr, + -1 + ); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_EQ(thread_join(thread, 1, 1), ERROR_NONE); + CHECK_EQ(thread_get_return_code(thread), 123); + thread_free(thread); +} diff --git a/Tests/TactilityKernel/TimerTest.cpp b/Tests/TactilityKernel/TimerTest.cpp new file mode 100644 index 000000000..c2d4e8c90 --- /dev/null +++ b/Tests/TactilityKernel/TimerTest.cpp @@ -0,0 +1,147 @@ +#include "doctest.h" + +#include + +#include +#include + +TEST_CASE("timer_alloc and timer_free should handle allocation and deallocation") { + auto callback = [](void* context) {}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, callback, nullptr); + CHECK_NE(timer, nullptr); + timer_free(timer); +} + +TEST_CASE("timer_start and timer_stop should change running state") { + auto callback = [](void* context) {}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, callback, nullptr); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_is_running(timer), false); + CHECK_EQ(timer_start(timer), ERROR_NONE); + CHECK_EQ(timer_is_running(timer), true); + CHECK_EQ(timer_stop(timer), ERROR_NONE); + CHECK_EQ(timer_is_running(timer), false); + + timer_free(timer); +} + +TEST_CASE("one-shot timer should fire callback once") { + std::atomic call_count{0}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, [](void* context) { + auto* count = static_cast*>(context); + (*count)++; + }, &call_count); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_start(timer), ERROR_NONE); + delay_millis(20); + + CHECK_EQ(call_count.load(), 1); + CHECK_EQ(timer_is_running(timer), false); + + timer_free(timer); +} + +TEST_CASE("periodic timer should fire callback multiple times") { + std::atomic call_count{0}; + struct Timer* timer = timer_alloc(TIMER_TYPE_PERIODIC, 10, [](void* context) { + auto* count = static_cast*>(context); + (*count)++; + }, &call_count); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_start(timer), ERROR_NONE); + delay_millis(35); // Should fire around 3 times + + CHECK_GE(call_count.load(), 3); + CHECK_EQ(timer_is_running(timer), true); + + timer_stop(timer); + timer_free(timer); +} + +TEST_CASE("timer_reset should restart the timer") { + std::atomic call_count{0}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 20, [](void* context) { + auto* count = static_cast*>(context); + (*count)++; + }, &call_count); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_start(timer), ERROR_NONE); + delay_millis(10); + CHECK_EQ(call_count.load(), 0); + + // Resetting should push the expiry further + CHECK_EQ(timer_reset(timer), ERROR_NONE); + delay_millis(15); + CHECK_EQ(call_count.load(), 0); // Still shouldn't have fired if reset worked + + delay_millis(10); + CHECK_EQ(call_count.load(), 1); // Now it should have fired + + timer_free(timer); +} + +TEST_CASE("timer_reset_with_interval should change the period") { + std::atomic call_count{0}; + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 40, [](void* context) { + auto* count = static_cast*>(context); + (*count)++; + }, &call_count); + REQUIRE_NE(timer, nullptr); + + CHECK_EQ(timer_start(timer), ERROR_NONE); + // Change to a much shorter interval + CHECK_EQ(timer_reset_with_interval(timer, 10), ERROR_NONE); + + delay_millis(20); + CHECK_EQ(call_count.load(), 1); + + timer_free(timer); +} + +TEST_CASE("timer_get_expiry_time should return a valid time") { + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, [](void* context) {}, nullptr); + REQUIRE_NE(timer, nullptr); + + timer_start(timer); + TickType_t expiry = timer_get_expiry_time(timer); + // Expiry should be in the future + CHECK_GT(expiry, xTaskGetTickCount()); + + timer_free(timer); +} + +TEST_CASE("timer_set_pending_callback should execute callback in timer task") { + std::atomic called{false}; + struct Context { + std::atomic* called; + uint32_t expected_arg; + uint32_t received_arg; + } context = { &called, 0x12345678, 0 }; + + auto pending_cb = [](void* ctx, uint32_t arg) { + auto* c = static_cast(ctx); + c->received_arg = arg; + c->called->store(true); + }; + + // timer_set_pending_callback doesn't actually use the timer object in current implementation + // but we need one for the API + struct Timer* timer = timer_alloc(TIMER_TYPE_ONCE, 10, [](void* context) {}, nullptr); + + CHECK_EQ(timer_set_pending_callback(timer, pending_cb, &context, context.expected_arg, portMAX_DELAY), ERROR_NONE); + + // Wait for timer task to process the callback + int retries = 10; + while (!called.load() && retries-- > 0) { + delay_millis(10); + } + + CHECK(called.load()); + CHECK_EQ(context.received_arg, context.expected_arg); + + timer_free(timer); +}