Kernel improvements (#468)

* **New Features**
  * Plugin-style modules for platforms and devices with start/stop lifecycle and a runtime-consumable device tree array.

* **Refactor**
  * Consolidated initialization into a kernel/module startup flow.
  * Generated devicetree artifacts moved to the build Generated directory.

* **Changes**
  * Removed legacy no-op registration hooks and I2C lock/unlock wrappers.
  * Driver ownership and private state moved to explicit module/owner model.
  * Added ERROR_NOT_ALLOWED error code.
This commit is contained in:
Ken Van Hoeylandt
2026-01-31 09:18:02 +01:00
committed by GitHub
parent d62314f41f
commit c9185740d7
119 changed files with 1639 additions and 440 deletions
+12 -4
View File
@@ -4,6 +4,13 @@
#include <vector>
#include <tactility/device.h>
#include <tactility/module.h>
static Module module = {
.name = "test_module",
.start = nullptr,
.stop = nullptr
};
TEST_CASE("device_construct and device_destruct should set and unset the correct fields") {
Device device = { 0 };
@@ -11,16 +18,16 @@ TEST_CASE("device_construct and device_destruct should set and unset the correct
error_t error = device_construct(&device);
CHECK_EQ(error, ERROR_NONE);
CHECK_NE(device.internal.data, nullptr);
CHECK_NE(device.internal.device_private, nullptr);
CHECK_NE(device.internal.mutex.handle, nullptr);
CHECK_EQ(device_destruct(&device), ERROR_NONE);
CHECK_EQ(device.internal.data, nullptr);
CHECK_EQ(device.internal.device_private, nullptr);
CHECK_EQ(device.internal.mutex.handle, nullptr);
Device comparison_device = { 0 };
comparison_device.internal.data = device.internal.data;
comparison_device.internal.device_private = device.internal.device_private;
comparison_device.internal.mutex.handle = device.internal.mutex.handle;
// Check that no other data was set
@@ -164,7 +171,8 @@ TEST_CASE("device_is_ready should return true only when it is started") {
.stopDevice = nullptr,
.api = nullptr,
.deviceType = nullptr,
.internal = { 0 }
.owner = &module,
.driver_private = nullptr
};
Device device = { 0 };
@@ -1,6 +1,13 @@
#include "doctest.h"
#include <tactility/driver.h>
#include <tactility/device.h>
#include <tactility/module.h>
static Module module = {
.name = "test_module",
.start = nullptr,
.stop = nullptr
};
struct IntegrationDriverConfig {
int startResult;
@@ -30,7 +37,8 @@ static Driver integration_driver = {
.stopDevice = stop,
.api = nullptr,
.deviceType = nullptr,
.internal = { 0 }
.owner = &module,
.driver_private = nullptr,
};
TEST_CASE("driver with with start success and stop success should start and stop a device") {
+24 -4
View File
@@ -1,13 +1,31 @@
#include "doctest.h"
#include <tactility/driver.h>
#include <tactility/module.h>
static Module module = {
.name = "test_module",
.start = nullptr,
.stop = nullptr
};
TEST_CASE("driver_construct and driver_destruct should set and unset the correct fields") {
Driver driver = { 0 };
driver.owner = &module;
CHECK_EQ(driver_construct(&driver), ERROR_NONE);
CHECK_NE(driver.internal.data, nullptr);
CHECK_NE(driver.driver_private, nullptr);
CHECK_EQ(driver_destruct(&driver), ERROR_NONE);
CHECK_EQ(driver.driver_private, nullptr);
}
TEST_CASE("a driver without a module should not be destructible") {
Driver driver = { 0 };
CHECK_EQ(driver_construct(&driver), ERROR_NONE);
CHECK_EQ(driver_destruct(&driver), ERROR_NOT_ALLOWED);
driver.owner = &module;
CHECK_EQ(driver_destruct(&driver), ERROR_NONE);
CHECK_EQ(driver.internal.data, nullptr);
}
TEST_CASE("driver_is_compatible should return true if a compatible value is found") {
@@ -19,7 +37,8 @@ TEST_CASE("driver_is_compatible should return true if a compatible value is foun
.stopDevice = nullptr,
.api = nullptr,
.deviceType = nullptr,
.internal = { 0 }
.owner = &module,
.driver_private = nullptr
};
CHECK_EQ(driver_is_compatible(&driver, "test_compatible"), true);
CHECK_EQ(driver_is_compatible(&driver, "nope"), false);
@@ -35,7 +54,8 @@ TEST_CASE("driver_find should only find a compatible driver when the driver was
.stopDevice = nullptr,
.api = nullptr,
.deviceType = nullptr,
.internal = { 0 }
.owner = &module,
.driver_private = nullptr
};
Driver* found_driver = driver_find_compatible("test_compatible");
+160
View File
@@ -0,0 +1,160 @@
#include "doctest.h"
#include <tactility/module.h>
static error_t test_start_result = ERROR_NONE;
static bool start_called = false;
static error_t test_start() {
start_called = true;
return test_start_result;
}
static error_t test_stop_result = ERROR_NONE;
static bool stop_called = false;
static error_t test_stop() {
stop_called = true;
return test_stop_result;
}
TEST_CASE("ModuleParent construction and destruction") {
struct ModuleParent parent = { "test_parent", nullptr };
// Test successful construction
CHECK_EQ(module_parent_construct(&parent), ERROR_NONE);
CHECK_NE(parent.module_parent_private, nullptr);
// Test successful destruction
CHECK_EQ(module_parent_destruct(&parent), ERROR_NONE);
CHECK_EQ(parent.module_parent_private, nullptr);
}
TEST_CASE("ModuleParent destruction with children") {
struct ModuleParent parent = { "parent", nullptr };
REQUIRE_EQ(module_parent_construct(&parent), ERROR_NONE);
struct Module module = {
.name = "test",
.start = test_start,
.stop = test_stop,
.internal = {.started = false, .parent = nullptr}
};
REQUIRE_EQ(module_set_parent(&module, &parent), ERROR_NONE);
// Should fail to destruct because it has a child
CHECK_EQ(module_parent_destruct(&parent), ERROR_INVALID_STATE);
CHECK_NE(parent.module_parent_private, nullptr);
// Remove child
REQUIRE_EQ(module_set_parent(&module, nullptr), ERROR_NONE);
// Now it should succeed
CHECK_EQ(module_parent_destruct(&parent), ERROR_NONE);
CHECK_EQ(parent.module_parent_private, nullptr);
}
TEST_CASE("Module parent management") {
struct ModuleParent parent1 = { "parent1", nullptr };
struct ModuleParent parent2 = { "parent2", nullptr };
REQUIRE_EQ(module_parent_construct(&parent1), ERROR_NONE);
REQUIRE_EQ(module_parent_construct(&parent2), ERROR_NONE);
struct Module module = {
.name = "test",
.start = test_start,
.stop = test_stop,
.internal = {.started = false, .parent = nullptr}
};
// Set parent
CHECK_EQ(module_set_parent(&module, &parent1), ERROR_NONE);
CHECK_EQ(module.internal.parent, &parent1);
// Change parent
CHECK_EQ(module_set_parent(&module, &parent2), ERROR_NONE);
CHECK_EQ(module.internal.parent, &parent2);
// Clear parent
CHECK_EQ(module_set_parent(&module, nullptr), ERROR_NONE);
CHECK_EQ(module.internal.parent, nullptr);
// Set same parent (should be NOOP and return ERROR_NONE)
CHECK_EQ(module_set_parent(&module, &parent1), ERROR_NONE);
CHECK_EQ(module_set_parent(&module, &parent1), ERROR_NONE);
CHECK_EQ(module.internal.parent, &parent1);
CHECK_EQ(module_set_parent(&module, nullptr), ERROR_NONE);
CHECK_EQ(module_parent_destruct(&parent1), ERROR_NONE);
CHECK_EQ(module_parent_destruct(&parent2), ERROR_NONE);
}
TEST_CASE("Module lifecycle") {
struct ModuleParent parent = { "parent", nullptr };
REQUIRE_EQ(module_parent_construct(&parent), ERROR_NONE);
start_called = false;
stop_called = false;
test_start_result = ERROR_NONE;
test_stop_result = ERROR_NONE;
struct Module module = {
.name = "test",
.start = test_start,
.stop = test_stop,
.internal = {.started = false, .parent = nullptr}
};
// 1. Cannot start without parent
CHECK_EQ(module_start(&module), ERROR_INVALID_STATE);
CHECK_EQ(module_is_started(&module), false);
CHECK_EQ(start_called, false);
CHECK_EQ(module_set_parent(&module, &parent), ERROR_NONE);
// 2. Successful start
CHECK_EQ(module_start(&module), ERROR_NONE);
CHECK_EQ(module_is_started(&module), true);
CHECK_EQ(start_called, true);
// 3. Start when already started (should return ERROR_NONE)
start_called = false;
CHECK_EQ(module_start(&module), ERROR_NONE);
CHECK_EQ(start_called, false); // start() function should NOT be called again
// 4. Cannot change parent while started
CHECK_EQ(module_set_parent(&module, nullptr), ERROR_INVALID_STATE);
// 5. Successful stop
CHECK_EQ(module_stop(&module), ERROR_NONE);
CHECK_EQ(module_is_started(&module), false);
CHECK_EQ(stop_called, true);
// 6. Stop when already stopped (should return ERROR_NONE)
stop_called = false;
CHECK_EQ(module_stop(&module), ERROR_NONE);
CHECK_EQ(stop_called, false); // stop() function should NOT be called again
// 7. Test failed start
test_start_result = ERROR_NOT_FOUND;
start_called = false;
CHECK_EQ(module_start(&module), ERROR_NOT_FOUND);
CHECK_EQ(module_is_started(&module), false);
CHECK_EQ(start_called, true);
// 8. Test failed stop
CHECK_EQ(module_set_parent(&module, &parent), ERROR_NONE);
test_start_result = ERROR_NONE;
CHECK_EQ(module_start(&module), ERROR_NONE);
test_stop_result = ERROR_NOT_SUPPORTED;
stop_called = false;
CHECK_EQ(module_stop(&module), ERROR_NOT_SUPPORTED);
CHECK_EQ(module_is_started(&module), true); // Should still be started if stop failed
CHECK_EQ(stop_called, true);
// Clean up: fix stop result so we can stop it
test_stop_result = ERROR_NONE;
CHECK_EQ(module_stop(&module), ERROR_NONE);
CHECK_EQ(module_set_parent(&module, nullptr), ERROR_NONE);
CHECK_EQ(module_parent_destruct(&parent), ERROR_NONE);
}