Skip to content

New kernel drivers, filesystem API, and more#513

Merged
KenVanHoeylandt merged 27 commits intomainfrom
develop
Mar 7, 2026
Merged

New kernel drivers, filesystem API, and more#513
KenVanHoeylandt merged 27 commits intomainfrom
develop

Conversation

@KenVanHoeylandt
Copy link
Contributor

@KenVanHoeylandt KenVanHoeylandt commented Mar 7, 2026

  • New Features

    • BMI270 6-axis IMU driver added; new unified filesystem abstraction for mounted filesystems.
    • Public Wi‑Fi API surface (no implementation yet)
    • SDMMC driver added (kernel drive$)
    • expanded GPIO interrupt/callback support
  • Improvements

    • M5Stack Tab5: revamped GPIO/power initialization and IMU integration.
    • LVGL updates including device fontSize configuration.
    • Updated all code related to SD card device/fs handling
    • Rename LilyGO T-HMI S3 to LilyGO T-HMI
  • Bug Fixes

    • Simplified and consolidated SD card handling and mount discovery.

@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request refactors the SD card architecture from a device-centric to filesystem-centric approach and adds new hardware support. The SdmmcDevice class is removed and replaced with a new FileSystem abstraction layer that provides a unified interface for managing mounted filesystems. A new BMI270 6-axis IMU driver module is added with complete hardware integration. The GPIO controller API is extended with interrupt and callback support, and the pi4ioe5v6408 I/O expander driver is refactored to use the new GPIO controller API. Platform-specific ESP32 SD/MMC driver and device tree bindings are added. Device configurations for multiple boards (lilygo-tdongle-s3, lilygo-thmi-s3, waveshare-esp32-s3-geek) are updated to use the new device tree-based SD/MMC configuration. Applications are refactored to discover storage via filesystem traversal instead of device enumeration.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title is generic and does not clearly specify the primary changes. While the PR does introduce new features, the title 'New kernel drivers, filesystem API, and more' is overly broad and vague ('more') without highlighting the most important changes. Consider using a more specific and descriptive title such as 'Introduce SDMMC driver, filesystem abstraction, and BMI270 IMU support' or 'Add unified filesystem API and device-tree-based SD/MMC support' to better convey the main intent.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@KenVanHoeylandt KenVanHoeylandt changed the title Merge develop into main SDMMC kernel driver and more Mar 7, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
Tactility/Source/service/sdcard/Sdcard.cpp (1)

31-44: ⚠️ Potential issue | 🟠 Major

Add nullptr guard before calling file_system_is_mounted in the update() method.

If the SD card is removed at runtime after onStart() passes, findFirstSdcardFileSystem() will return nullptr on the next update() call. Calling file_system_is_mounted(nullptr) immediately dereferences the null pointer and crashes. Other code in the codebase (e.g., Statusbar.cpp:169, Sdcard.cpp:50) correctly guards against this scenario.

🛡️ Proposed fix
 void update() {
     // TODO: Support multiple SD cards
     auto* file_system = findFirstSdcardFileSystem();

     if (lock(50)) {
-        auto is_mounted = file_system_is_mounted(file_system);
-        if (is_mounted != lastMountedState) {
-            lastMountedState = is_mounted;
-        }
+        bool is_mounted = (file_system != nullptr) && file_system_is_mounted(file_system);
+        lastMountedState = is_mounted;
         unlock();
     } else {
         LOGGER.warn(LOG_MESSAGE_MUTEX_LOCK_FAILED);
     }
 }
TactilityKernel/include/tactility/drivers/gpio.h (1)

12-29: ⚠️ Potential issue | 🟠 Major

Update GPIO_FLAGS_MASK to include the new high-impedance bit.

GPIO_FLAG_HIGH_IMPEDANCE lives at bit 8, but GPIO_FLAGS_MASK still only covers bits 0-4. Any caller that filters flags through this mask will drop the new option, so the flag cannot round-trip reliably.

Suggested fix
-#define GPIO_FLAGS_MASK 0x1f
+#define GPIO_FLAGS_MASK 0x11f
TactilityKernel/source/kernel_symbols.c (1)

1-15: ⚠️ Potential issue | 🟡 Minor

Add direct include for log_generic declaration.

Line 163 exports DEFINE_MODULE_SYMBOL(log_generic), but the declaring header is not directly included. Currently the symbol is accessible only through transitive includes: event_group.hcheck.hlog.h. This undermines the file's stated purpose: "compile headers as C to catch errors that only show up when compiling as C and not as C++." Add the direct include to maintain include hygiene and prevent brittleness from future refactoring.

Suggested change
 `#include` <tactility/error.h>
 `#include` <tactility/filesystem/file_system.h>
+#ifndef ESP_PLATFORM
+#include <tactility/log.h>
+#endif
 `#include` <tactility/module.h>
Tactility/Source/service/statusbar/Statusbar.cpp (1)

166-179: ⚠️ Potential issue | 🟡 Minor

Consider hiding the SD card icon when no SD card filesystem exists.

When findFirstSdcardFileSystem() returns nullptr, the icon visibility is not updated. If an SD card was previously detected and then removed (physically or the filesystem unregistered), the icon would remain visible with stale state.

Depending on the intended behavior, you may want to hide the icon when no SD card filesystem is found:

💡 Suggested enhancement
     void updateSdCardIcon() {
         auto* sdcard_fs = findFirstSdcardFileSystem();
         // TODO: Support multiple SD cards
         if (sdcard_fs != nullptr) {
             auto mounted = file_system_is_mounted(sdcard_fs);
             auto* desired_icon = getSdCardStatusIcon(mounted);
             if (sdcard_last_icon != desired_icon) {
                 lvgl::statusbar_icon_set_image(sdcard_icon_id, desired_icon);
                 lvgl::statusbar_icon_set_visibility(sdcard_icon_id, true);
                 sdcard_last_icon = desired_icon;
             }
             // TODO: Consider tracking how long the SD card has been in unknown status and then show error
+        } else if (sdcard_last_icon != nullptr) {
+            lvgl::statusbar_icon_set_visibility(sdcard_icon_id, false);
+            sdcard_last_icon = nullptr;
         }
     }
🟡 Minor comments (8)
TactilityC/Source/tt_init.cpp-421-431 (1)

421-431: ⚠️ Potential issue | 🟡 Minor

Clarify that ledc_set_fade is superseded, not deprecated.

ledc_set_fade is not formally deprecated but is superseded in favor of time-based (ledc_set_fade_with_time, ledc_set_fade_time_and_start) or step-based (ledc_set_fade_with_step, ledc_set_fade_step_and_start) alternatives for typical fade operations. Consider exporting these newer APIs for alignment with modern ESP-IDF best practices.

Minor: Comment could be // driver/ledc.h for consistency with other header sections.

TactilityKernel/include/tactility/drivers/wifi.h-131-137 (1)

131-137: ⚠️ Potential issue | 🟡 Minor

Documentation error: comment describes IPv6 but function returns SSID.

The doc comment describes "Get the IPv6 address" but the function is station_get_target_ssid. This appears to be a copy-paste error.

📝 Proposed fix
     /**
-     * Get the IPv6 address of the device.
+     * Get the target SSID of the station.
      * `@param`[in] device the device
-     * `@param`[out] ipv6 the buffer to store the IPv6 address (must be at least 33 bytes, will be null-terminated)
+     * `@param`[out] ssid the buffer to store the SSID (must be at least 33 bytes, will be null-terminated)
      * `@return` ERROR_NONE on success
      */
     error_t (*station_get_target_ssid)(struct Device* device, char* ssid);
TactilityKernel/include/tactility/drivers/wifi.h-139-147 (1)

139-147: ⚠️ Potential issue | 🟡 Minor

Clarify password documentation.

The comment states password "must be at least 33 characters" which seems incorrect. Wi-Fi passwords have a maximum length (63 for WPA2-PSK), not a minimum of 33. This likely should describe the buffer size expectation, not a password length requirement.

📝 Suggested clarification
     /**
      * Connect to an access point.
      * `@param`[in] device the wifi device
      * `@param`[in] ssid the SSID of the access point
-     * `@param`[in] password the password of the access point (must be at least 33 characters and null-terminated)
+     * `@param`[in] password the password of the access point (null-terminated, max 63 characters for WPA2)
      * `@param`[in] channel the Wi-Fi channel to connect to (0 means "any" / no preference)
      * `@return` ERROR_NONE on success
      */
Tactility/Source/hal/usb/Usb.cpp-39-44 (1)

39-44: ⚠️ Potential issue | 🟡 Minor

Keep iterating until a usable SDMMC card is found.

The callback returns false on the first compatible SDMMC device even when esp32_sdmmc_get_card(device) returns nullptr. That makes discovery fail early in multi-controller or partially initialized setups.

Suggested fix
     if (sdcard == nullptr) {
         device_for_each(&sdcard, [](auto* device, void* context) {
             if (device_is_ready(device) && device_is_compatible(device, "espressif,esp32-sdmmc")) {
                 auto** sdcard = static_cast<sdmmc_card_t**>(context);
                 *sdcard = esp32_sdmmc_get_card(device);
-                return false;
+                return *sdcard == nullptr;
             }
             return true;
         });
     }
TactilityKernel/include/tactility/drivers/gpio_controller.h-204-211 (1)

204-211: ⚠️ Potential issue | 🟡 Minor

Fix the return docs for gpio_controller_get_controller_context().

This function returns a context pointer, not an error_t, so the current @return ERROR_NONE if successful text is misleading.

Tactility/Source/Tactility.cpp-232-244 (1)

232-244: ⚠️ Potential issue | 🟡 Minor

Minor: Log message shows root path instead of app path.

The log message on line 239 logs path (the filesystem root), but registerInstalledApps is called with app_path (the /app subdirectory). For clarity, consider logging the actual path being scanned.

📝 Suggested fix
         const auto app_path = std::format("{}/app", path);
         if (!app_path.starts_with(file::MOUNT_POINT_SYSTEM) && file::isDirectory(app_path)) {
-            LOGGER.info("Registering apps from {}", path);
+            LOGGER.info("Registering apps from {}", app_path);
             registerInstalledApps(app_path);
         }
Tactility/Source/service/webserver/WebServerService.cpp-775-784 (1)

775-784: ⚠️ Potential issue | 🟡 Minor

Minor: Inconsistent error comparison.

Line 778 compares file_system_get_path result to ESP_OK, but this function returns error_t. While ESP_OK and ERROR_NONE are both typically 0, using ERROR_NONE would be more consistent with the filesystem API contract.

Proposed fix
-            if (file_system_is_mounted(fs) && file_system_get_path(fs, path, sizeof(path)) == ESP_OK && strcmp(path, "/system") != 0) {
+            if (file_system_is_mounted(fs) && file_system_get_path(fs, path, sizeof(path)) == ERROR_NONE && strcmp(path, "/system") != 0) {
Platforms/platform-esp32/source/drivers/esp32_sdmmc.cpp-117-120 (1)

117-120: ⚠️ Potential issue | 🟡 Minor

Inconsistent logging macro and unused variable.

  1. Line 118 uses ESP_LOGI while the rest of the file uses LOG_I - should be consistent.
  2. Line 120 declares dts_config but it's never used.
Proposed fix
 static error_t stop(Device* device) {
-    ESP_LOGI(TAG, "stop %s", device->name);
+    LOG_I(TAG, "stop %s", device->name);
     auto* data = GET_DATA(device);
-    auto* dts_config = GET_CONFIG(device);
 
     if (file_system_is_mounted(data->file_system)) {
🧹 Nitpick comments (9)
Devices/guition-jc1060p470ciwy/Source/devices/SdCard.cpp (1)

21-21: TODO comment added to track pending migration.

The TODO clearly documents that this implementation still needs migration to the new espressif,esp32-sdmmc driver and identifies the LDO code porting as the blocking dependency.

Would you like me to open a tracking issue for this migration task? I can help document the LDO-specific code (lines 63-77) that needs to be ported and the context from this PR's broader migration effort.

Tactility/Source/service/sdcard/Sdcard.cpp (1)

48-63: Consider initializing lastMountedState to actual mount status.

The service starts with lastMountedState = false, but the SD card may already be mounted. This means the first update() call will detect a "state change" even if nothing changed since service start.

♻️ Proposed fix to initialize state correctly
 bool onStart(ServiceContext& serviceContext) override {
     auto* sdcard_fs = findFirstSdcardFileSystem();
     if (sdcard_fs == nullptr) {
         LOGGER.warn("No SD card device found - not starting Service");
         return false;
     }

+    // Initialize to actual mounted state
+    lastMountedState = file_system_is_mounted(sdcard_fs);
+
     auto service = findServiceById<SdCardService>(manifest.id);
     updateTimer = std::make_unique<Timer>(Timer::Type::Periodic, 1000, [service] {
         service->update();
     });
TactilityKernel/include/tactility/filesystem/file_system.h (1)

21-33: Document the handle and iteration contract before this API spreads.

This header exposes raw FileSystem* handles and a bool-returning iterator callback, but it never defines ownership, retention rules, or whether the callback returns true to continue or to stop. That ambiguity is easy to bake into downstream callers once this becomes the public surface.

📝 Suggested header docs
 struct FileSystem* file_system_add(const struct FileSystemApi* fs_api, void* data);
 
 void file_system_remove(struct FileSystem* fs);
 
+/**
+ * Iterate over registered file systems.
+ * `@param` callback_context User-provided context passed to `callback`.
+ * `@param` callback Return true to continue iteration, false to stop.
+ * `@note` `FileSystem*` handles are owned by the registry; callers must not free
+ *       them and should not retain them past their valid lifetime.
+ */
 void file_system_for_each(void* callback_context, bool (*callback)(struct FileSystem* fs, void* context));
Tactility/Include/Tactility/Paths.h (1)

12-16: Align the new helper names with the existing SdCard spelling.

findFirstMountedSdCardPath() already establishes the public naming in this header. Adding findFirstMountedSdcardFileSystem() and findFirstSdcardFileSystem() freezes two spellings for the same concept into the API.

✏️ Suggested rename
 bool hasMountedSdCard();
 
-FileSystem* findFirstMountedSdcardFileSystem();
+FileSystem* findFirstMountedSdCardFileSystem();
 
-FileSystem* findFirstSdcardFileSystem();
+FileSystem* findFirstSdCardFileSystem();
Tactility/Source/app/fileselection/State.cpp (1)

36-62: Consider thread safety consistency with files/State.cpp.

The files/State.cpp version of setEntriesForPath acquires a mutex lock before modifying dir_entries and current_path, but this implementation does not. If dir_entries can be accessed concurrently (e.g., via getDirent which does use a mutex), there may be a race condition when updating the entries.

Tactility/Source/PartitionsEsp.cpp (1)

95-103: Prefer static storage for these fixed mount descriptors.

Lines 95 and 103 allocate process-lifetime PartitionFsData objects on the heap. These mount points are singletons, so static storage makes the ownership explicit and avoids leaking two small objects at boot.

♻️ Suggested change
     } else {
         LOGGER.info("Mounted /system");
-        file_system_add(&partition_fs_api, new PartitionFsData("/system"));
+        static PartitionFsData system_fs_data{"/system"};
+        file_system_add(&partition_fs_api, &system_fs_data);
     }
@@
     } else {
         LOGGER.info("Mounted /data");
-        file_system_add(&partition_fs_api, new PartitionFsData("/data"));
+        static PartitionFsData data_fs_data{"/data"};
+        file_system_add(&partition_fs_api, &data_fs_data);
     }
Tactility/Source/MountPoints.cpp (1)

16-17: Minor: Redundant clear() call.

The dir_entries vector was just default-constructed and is already empty. The clear() call on line 17 is unnecessary.

♻️ Proposed fix
 std::vector<dirent> getFileSystemDirents() {
     std::vector<dirent> dir_entries;
-    dir_entries.clear();
 
     file_system_for_each(&dir_entries, [](auto* fs, void* context) {
Tactility/Source/Paths.cpp (1)

25-53: Optional: Reduce duplication between findFirstMountedSdcardFileSystem() and findFirstSdcardFileSystem().

These two functions share nearly identical logic, differing only in whether they check file_system_is_mounted(). Consider extracting a shared helper to reduce duplication.

♻️ Proposed refactor
+static FileSystem* findSdcardFileSystem(bool mustBeMounted) {
+    FileSystem* found = nullptr;
+    file_system_for_each(&found, [](auto* fs, void* context) {
+        char path[128];
+        if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
+        // TODO: Find a better way to identify SD card paths
+        if (std::string(path).starts_with("/sdcard")) {
+            *static_cast<FileSystem**>(context) = fs;
+            return false;
+        }
+        return true;
+    });
+    if (found && mustBeMounted && !file_system_is_mounted(found)) {
+        return nullptr;
+    }
+    return found;
+}
+
 FileSystem* findFirstMountedSdcardFileSystem() {
-    FileSystem* found = nullptr;
-    file_system_for_each(&found, [](auto* fs, void* context) {
-        char path[128];
-        if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
-        // TODO: Find a better way to identify SD card paths
-        if (std::string(path).starts_with("/sdcard") && file_system_is_mounted(fs)) {
-            *static_cast<FileSystem**>(context) = fs;
-            return false;
-        }
-        return true;
-    });
-    return found;
+    return findSdcardFileSystem(true);
 }

 FileSystem* findFirstSdcardFileSystem() {
-    FileSystem* found = nullptr;
-    file_system_for_each(&found, [](auto* fs, void* context) {
-        char path[128];
-        if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
-        // TODO: Find a better way to identify SD card paths
-        if (std::string(path).starts_with("/sdcard")) {
-            *static_cast<FileSystem**>(context) = fs;
-            return false;
-        }
-        return true;
-    });
-    return found;
+    return findSdcardFileSystem(false);
 }
Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp (1)

156-160: Inconsistent null check in is_mounted.

The function checks fs_data == nullptr but based on the project's Linux kernel style guideline, internal APIs should assume valid pointers. However, more importantly, checking fs_data->card == nullptr before calling sdmmc_get_status is correct since the card may not be mounted yet.

Consider removing the fs_data == nullptr check for consistency with project style, or document this as a public API boundary where null checks are appropriate.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 74e8e770-aeb3-4a32-8a7b-d3c3559ccc4a

📥 Commits

Reviewing files that changed from the base of the PR and between 2de35b2 and dbefa0a.

📒 Files selected for processing (78)
  • Devices/guition-jc1060p470ciwy/Source/devices/SdCard.cpp
  • Devices/lilygo-tdongle-s3/Source/Configuration.cpp
  • Devices/lilygo-tdongle-s3/Source/devices/Sdcard.cpp
  • Devices/lilygo-tdongle-s3/Source/devices/Sdcard.h
  • Devices/lilygo-tdongle-s3/device.properties
  • Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts
  • Devices/lilygo-thmi-s3/Source/Configuration.cpp
  • Devices/lilygo-thmi-s3/Source/devices/SdCard.cpp
  • Devices/lilygo-thmi-s3/Source/devices/SdCard.h
  • Devices/lilygo-thmi-s3/lilygo,thmi-s3.dts
  • Devices/m5stack-tab5/CMakeLists.txt
  • Devices/m5stack-tab5/Source/Configuration.cpp
  • Devices/m5stack-tab5/devicetree.yaml
  • Devices/m5stack-tab5/m5stack,tab5.dts
  • Devices/waveshare-esp32-s3-geek/Source/Configuration.cpp
  • Devices/waveshare-esp32-s3-geek/Source/devices/SdCard.cpp
  • Devices/waveshare-esp32-s3-geek/Source/devices/SdCard.h
  • Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts
  • Drivers/bmi270-module/CMakeLists.txt
  • Drivers/bmi270-module/LICENSE-Apache-2.0.md
  • Drivers/bmi270-module/README.md
  • Drivers/bmi270-module/bindings/bosch,bmi270.yaml
  • Drivers/bmi270-module/devicetree.yaml
  • Drivers/bmi270-module/include/bindings/bmi270.h
  • Drivers/bmi270-module/include/bmi270_module.h
  • Drivers/bmi270-module/include/drivers/bmi270.h
  • Drivers/bmi270-module/private/drivers/bmi270_config_data.h
  • Drivers/bmi270-module/source/bmi270.cpp
  • Drivers/bmi270-module/source/module.cpp
  • Drivers/bmi270-module/source/symbols.c
  • Drivers/pi4ioe5v6408-module/include/drivers/pi4ioe5v6408.h
  • Drivers/pi4ioe5v6408-module/source/module.cpp
  • Drivers/pi4ioe5v6408-module/source/pi4ioe5v6408.cpp
  • Drivers/pi4ioe5v6408-module/source/symbols.c
  • Modules/lvgl-module/source/symbols.c
  • Platforms/platform-esp32/CMakeLists.txt
  • Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml
  • Platforms/platform-esp32/include/tactility/bindings/esp32_sdmmc.h
  • Platforms/platform-esp32/include/tactility/drivers/esp32_sdmmc.h
  • Platforms/platform-esp32/private/tactility/drivers/esp32_sdmmc_fs.h
  • Platforms/platform-esp32/source/drivers/esp32_gpio.cpp
  • Platforms/platform-esp32/source/drivers/esp32_sdmmc.cpp
  • Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp
  • Platforms/platform-esp32/source/drivers/esp32_spi.cpp
  • Platforms/platform-esp32/source/module.cpp
  • Tactility/Include/Tactility/MountPoints.h
  • Tactility/Include/Tactility/Paths.h
  • Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h
  • Tactility/Include/Tactility/hal/sdcard/SdmmcDevice.h
  • Tactility/Source/MountPoints.cpp
  • Tactility/Source/PartitionsEsp.cpp
  • Tactility/Source/Paths.cpp
  • Tactility/Source/Tactility.cpp
  • Tactility/Source/app/files/FilesApp.cpp
  • Tactility/Source/app/files/State.cpp
  • Tactility/Source/app/fileselection/State.cpp
  • Tactility/Source/app/screenshot/Screenshot.cpp
  • Tactility/Source/app/systeminfo/SystemInfo.cpp
  • Tactility/Source/hal/sdcard/SdCardDevice.cpp
  • Tactility/Source/hal/sdcard/SdmmcDevice.cpp
  • Tactility/Source/hal/usb/Usb.cpp
  • Tactility/Source/service/sdcard/Sdcard.cpp
  • Tactility/Source/service/statusbar/Statusbar.cpp
  • Tactility/Source/service/webserver/WebServerService.cpp
  • Tactility/Source/service/wifi/WifiBootSplashInit.cpp
  • Tactility/Source/settings/BootSettings.cpp
  • TactilityC/Source/symbols/stl.cpp
  • TactilityC/Source/tt_init.cpp
  • TactilityKernel/include/tactility/drivers/gpio.h
  • TactilityKernel/include/tactility/drivers/gpio_controller.h
  • TactilityKernel/include/tactility/drivers/gpio_descriptor.h
  • TactilityKernel/include/tactility/drivers/wifi.h
  • TactilityKernel/include/tactility/filesystem/file_system.h
  • TactilityKernel/include/tactility/module.h
  • TactilityKernel/source/drivers/gpio_controller.cpp
  • TactilityKernel/source/filesystem/file_system.cpp
  • TactilityKernel/source/kernel_symbols.c
  • TactilityKernel/source/module.cpp
💤 Files with no reviewable changes (13)
  • Devices/lilygo-tdongle-s3/Source/devices/Sdcard.h
  • Devices/lilygo-tdongle-s3/Source/devices/Sdcard.cpp
  • Tactility/Source/hal/sdcard/SdmmcDevice.cpp
  • Devices/lilygo-thmi-s3/Source/Configuration.cpp
  • Drivers/pi4ioe5v6408-module/include/drivers/pi4ioe5v6408.h
  • Tactility/Source/app/files/FilesApp.cpp
  • Tactility/Include/Tactility/hal/sdcard/SdmmcDevice.h
  • Drivers/pi4ioe5v6408-module/source/symbols.c
  • Devices/lilygo-thmi-s3/Source/devices/SdCard.cpp
  • Devices/waveshare-esp32-s3-geek/Source/devices/SdCard.h
  • Devices/waveshare-esp32-s3-geek/Source/devices/SdCard.cpp
  • Devices/waveshare-esp32-s3-geek/Source/Configuration.cpp
  • Devices/lilygo-thmi-s3/Source/devices/SdCard.h

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
Platforms/platform-esp32/source/drivers/esp32_sdmmc.cpp (1)

118-121: Inconsistent logging macro and unused variable.

Line 119 uses ESP_LOGI while the rest of the file uses LOG_I/LOG_E. Additionally, dts_config on line 121 is declared but never used.

♻️ Proposed fix
 static error_t stop(Device* device) {
-    ESP_LOGI(TAG, "stop %s", device->name);
+    LOG_I(TAG, "stop %s", device->name);
     auto* data = GET_DATA(device);
-    auto* dts_config = GET_CONFIG(device);
Tactility/Source/hal/sdcard/SdCardDevice.cpp (1)

27-32: Minor: getMountPath() called twice.

This creates a slight inefficiency and a theoretical TOCTOU window if the path could change between calls. Consider caching the result in a local variable.

♻️ Proposed fix
 static error_t get_path(void* data, char* out_path, size_t out_path_size) {
     auto* device = static_cast<SdCardDevice*>(data);
-    if (device->getMountPath().size() >= out_path_size) return ERROR_BUFFER_OVERFLOW;
-    strncpy(out_path, device->getMountPath().c_str(), out_path_size);
+    auto path = device->getMountPath();
+    if (path.size() >= out_path_size) return ERROR_BUFFER_OVERFLOW;
+    strncpy(out_path, path.c_str(), out_path_size);
     return ERROR_NONE;
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee677091-607c-4e1a-b818-e2b8cd74e8fb

📥 Commits

Reviewing files that changed from the base of the PR and between dbefa0a and f12a52e.

📒 Files selected for processing (7)
  • Platforms/platform-esp32/include/tactility/drivers/esp32_sdmmc.h
  • Platforms/platform-esp32/private/tactility/drivers/esp32_sdmmc_fs.h
  • Platforms/platform-esp32/source/drivers/esp32_sdmmc.cpp
  • Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp
  • Platforms/platform-esp32/source/module.cpp
  • Tactility/Source/hal/sdcard/SdCardDevice.cpp
  • Tactility/Source/hal/usb/Usb.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • Platforms/platform-esp32/source/module.cpp
  • Platforms/platform-esp32/include/tactility/drivers/esp32_sdmmc.h

@KenVanHoeylandt KenVanHoeylandt changed the title SDMMC kernel driver and more New kernel drivers, filesystem API, and more Mar 7, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

♻️ Duplicate comments (7)
Tactility/Source/hal/sdcard/SdCardDevice.cpp (1)

9-13: ⚠️ Potential issue | 🟠 Major

Mount uses transient state instead of a stable target path.

Line 11 reads the current mount path, but getMountPath() is documented to return an empty string while the device is unmounted. That makes the first call on Line 12 effectively device->mount(""), so this wrapper cannot perform an initial mount reliably. Please source the mount point from configuration or other persistent state instead of the current mounted state.

Devices/m5stack-tab5/Source/Configuration.cpp (1)

102-130: ⚠️ Potential issue | 🟠 Major

Missing check() assertions for pin acquisitions in initExpander1.

Unlike initExpander0 (lines 59-72), this function does not verify that gpio_descriptor_acquire() succeeds before using the returned descriptors. If any acquisition fails (returning nullptr), subsequent calls will dereference null pointers.

🛡️ Proposed fix to add assertions
 static void initExpander1(::Device* io_expander1) {

     auto* c6_wlan_enable_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_C6_WLAN_ENABLE, GPIO_OWNER_GPIO);
+    check(c6_wlan_enable_pin);
     auto* usb_a_5v_enable_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_USB_A_5V_ENABLE, GPIO_OWNER_GPIO);
+    check(usb_a_5v_enable_pin);
     auto* device_power_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_DEVICE_POWER, GPIO_OWNER_GPIO);
+    check(device_power_pin);
     auto* ip2326_ncharge_qc_enable_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_IP2326_NCHG_QC_EN, GPIO_OWNER_GPIO);
+    check(ip2326_ncharge_qc_enable_pin);
     auto* ip2326_charge_state_led_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_IP2326_CHG_STAT_LED, GPIO_OWNER_GPIO);
+    check(ip2326_charge_state_led_pin);
     auto* ip2326_charge_enable_pin = gpio_descriptor_acquire(io_expander1, GPIO_EXP1_PIN_IP2326_CHG_EN, GPIO_OWNER_GPIO);
+    check(ip2326_charge_enable_pin);
TactilityKernel/source/drivers/gpio_controller.cpp (1)

174-177: ⚠️ Potential issue | 🟠 Major

Data race on owner_type read.

gpio_descriptor_get_owner_type() reads descriptor->owner_type without acquiring the mutex, while gpio_descriptor_acquire() and gpio_descriptor_release() modify it under the lock. This can return stale or torn values on concurrent access.

🔒 Proposed fix to add mutex protection
 error_t gpio_descriptor_get_owner_type(GpioDescriptor* descriptor, GpioOwnerType* owner_type) {
+    auto* data = static_cast<struct GpioControllerData*>(device_get_driver_data(descriptor->controller));
+    mutex_lock(&data->mutex);
     *owner_type = descriptor->owner_type;
+    mutex_unlock(&data->mutex);
     return ERROR_NONE;
 }
Tactility/Source/app/systeminfo/SystemInfo.cpp (1)

345-347: ⚠️ Potential issue | 🟠 Major

Only update the SD-card bar after it exists.

Lines 621-623 create sdcardStorageBar only when the card is mounted during onShow(), but Lines 345-347 recompute the path and update it unconditionally. If the filesystem appears after the creation pass but before updateStorage() runs, updateMemoryBar() gets {nullptr, nullptr} and LVGL will dereference null widgets.

🛠️ Minimal guard
-        std::string sdcard_path;
-        if (findFirstMountedSdCardPath(sdcard_path) && esp_vfs_fat_info(sdcard_path.c_str(), &storage_total, &storage_free) == ESP_OK) {
-            updateMemoryBar(sdcardStorageBar, storage_free, storage_total);
+        if (sdcardStorageBar.bar != nullptr) {
+            std::string sdcard_path;
+            if (findFirstMountedSdCardPath(sdcard_path) &&
+                esp_vfs_fat_info(sdcard_path.c_str(), &storage_total, &storage_free) == ESP_OK) {
+                updateMemoryBar(sdcardStorageBar, storage_free, storage_total);
+            }
         }

Also applies to: 621-623

Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp (1)

121-125: ⚠️ Potential issue | 🔴 Critical

Use fs_data in the mount-failure cleanup block.

Line 122 still dereferences data as if it were Esp32SdmmcFsData*. That block will not compile when SOC_SD_PWR_CTRL_SUPPORTED is enabled, and it skips the same cleanup target used in the rest of the file.

#!/bin/bash
# Expectation: every power-control access in this file should use `fs_data->pwr_ctrl_handle`.
rg -n 'data->pwr_ctrl_handle|fs_data->pwr_ctrl_handle' Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp
🐛 Minimal fix
 `#if` SOC_SD_PWR_CTRL_SUPPORTED
-        if (data->pwr_ctrl_handle) {
-            sd_pwr_ctrl_del_on_chip_ldo(data->pwr_ctrl_handle);
-            data->pwr_ctrl_handle = nullptr;
+        if (fs_data->pwr_ctrl_handle) {
+            sd_pwr_ctrl_del_on_chip_ldo(fs_data->pwr_ctrl_handle);
+            fs_data->pwr_ctrl_handle = nullptr;
         }
 `#endif`
Platforms/platform-esp32/private/tactility/drivers/esp32_sdmmc_fs.h (1)

5-20: ⚠️ Potential issue | 🔴 Critical

Make this header self-contained and valid C.

Line 18 exposes sdmmc_card_t* without declaring the type, and Line 20 uses FileSystemApi as if it were a typedef. In the provided tactility/filesystem/file_system.h, only struct FileSystemApi is declared, so a C translation unit including this header will fail unless unrelated headers happened to be included first.

Suggested fix
 `#include` <tactility/filesystem/file_system.h>
+#include <sd_protocol_types.h>
@@
-extern const FileSystemApi esp32_sdmmc_fs_api;
+extern const struct FileSystemApi esp32_sdmmc_fs_api;
#!/bin/bash
set -e

printf '%s\n' '--- esp32_sdmmc_fs.h ---'
cat -n Platforms/platform-esp32/private/tactility/drivers/esp32_sdmmc_fs.h

printf '\n%s\n' '--- FileSystemApi declarations/typedefs ---'
rg -n -C2 '\bstruct FileSystemApi\b|typedef\s+struct\s+FileSystemApi\b|typedef\b.*\bFileSystemApi\b'

printf '\n%s\n' '--- sdmmc_card_t declarations/includes ---'
rg -n -C2 '\bsdmmc_card_t\b|sd_protocol_types\.h'
TactilityKernel/source/filesystem/file_system.cpp (1)

47-58: ⚠️ Potential issue | 🔴 Critical

Coordinate remove() with the public ops before deleting fs.

file_system_remove() erases and deletes fs under the ledger mutex, but file_system_mount(), file_system_unmount(), file_system_is_mounted(), and file_system_get_path() dereference the same pointer without any shared lifetime coordination. Another thread can free fs between lookup and use, turning these calls into use-after-free. The mounted check on Line 48 is also outside that coordination window.

Also applies to: 72-87

🧹 Nitpick comments (1)
Tactility/Source/hal/usb/Usb.cpp (1)

29-55: Route SD discovery through the filesystem layer, not concrete drivers.

getCard() now knows about both sdcard::SpiSdCardDevice and the "espressif,esp32-sdmmc" driver. That reintroduces backend-specific storage discovery in USB HAL right as this PR is moving SD access behind filesystem-backed implementations. The next removable backend will need another branch here, and this path can drift from whatever the filesystem registry considers mounted/active. Please resolve the mounted filesystem first and only derive the sdmmc_card_t* from that selected filesystem.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4d15669-3f8d-4959-967c-a6f42fa92496

📥 Commits

Reviewing files that changed from the base of the PR and between f12a52e and 9efc7c7.

📒 Files selected for processing (27)
  • Devices/m5stack-tab5/Source/Configuration.cpp
  • Platforms/platform-esp32/include/tactility/drivers/esp32_sdmmc.h
  • Platforms/platform-esp32/private/tactility/drivers/esp32_sdmmc_fs.h
  • Platforms/platform-esp32/source/drivers/esp32_gpio.cpp
  • Platforms/platform-esp32/source/drivers/esp32_sdmmc.cpp
  • Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp
  • Platforms/platform-esp32/source/module.cpp
  • Tactility/Include/Tactility/Paths.h
  • Tactility/Source/MountPoints.cpp
  • Tactility/Source/PartitionsEsp.cpp
  • Tactility/Source/Paths.cpp
  • Tactility/Source/Tactility.cpp
  • Tactility/Source/app/fileselection/State.cpp
  • Tactility/Source/app/systeminfo/SystemInfo.cpp
  • Tactility/Source/hal/sdcard/SdCardDevice.cpp
  • Tactility/Source/hal/usb/Usb.cpp
  • Tactility/Source/service/sdcard/Sdcard.cpp
  • Tactility/Source/service/statusbar/Statusbar.cpp
  • Tactility/Source/service/webserver/WebServerService.cpp
  • TactilityC/Source/tt_init.cpp
  • TactilityKernel/include/tactility/drivers/gpio.h
  • TactilityKernel/include/tactility/drivers/gpio_controller.h
  • TactilityKernel/include/tactility/drivers/wifi.h
  • TactilityKernel/include/tactility/filesystem/file_system.h
  • TactilityKernel/source/drivers/gpio_controller.cpp
  • TactilityKernel/source/filesystem/file_system.cpp
  • TactilityKernel/source/kernel_symbols.c
💤 Files with no reviewable changes (1)
  • Tactility/Source/service/sdcard/Sdcard.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • Tactility/Source/app/fileselection/State.cpp
  • Tactility/Include/Tactility/Paths.h

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Tactility/Include/Tactility/service/wifi/Wifi.h (1)

16-32: ⚠️ Potential issue | 🔴 Critical

Removing wifi_auth_mode_t typedef breaks ApRecord on non-ESP platforms.

The change from } wifi_auth_mode_t; to }; removes the type alias, but line 69 still uses wifi_auth_mode_t auth_mode; in the ApRecord struct. This will cause a compilation error on non-ESP platforms (e.g., simulator/posix) where esp_wifi.h is not included.

Additionally, WifiMock.cpp (lines 78-113) creates ApRecord instances with .auth_mode = WIFI_AUTH_WPA2_PSK, which depend on this typedef.

🔧 Proposed fix: restore the typedef
     WIFI_AUTH_WPA3_EXT_PSK_MIXED_MODE, /**< authenticate mode: WPA3_PSK + WPA3_PSK_EXT_KEY */
     WIFI_AUTH_MAX
-};
+} wifi_auth_mode_t;
 `#endif`
♻️ Duplicate comments (6)
TactilityKernel/include/tactility/drivers/wifi.h (1)

62-63: ⚠️ Potential issue | 🟡 Minor

Incorrect comment for WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT.

The comment says "WifiAccessPointState changed" but this event represents a station connection result, not an access point state change. This appears to be a copy-paste error from line 64.

📝 Proposed fix
     /** WifiStationState changed */
     WIFI_EVENT_TYPE_STATION_STATE_CHANGED,
-    /** WifiAccessPointState changed */
+    /** Station connection result available */
     WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT,
     /** WifiAccessPointState changed */
     WIFI_EVENT_TYPE_ACCESS_POINT_STATE_CHANGED,
TactilityKernel/source/filesystem/file_system.cpp (2)

49-60: ⚠️ Potential issue | 🔴 Critical

Serialize FileSystem* destruction with the public ops.

file_system_remove() frees fs at Line 57, but Lines 75, 79, 83, and 87 dereference the same raw handle without any shared lifetime guard. Line 50 also checks mount state before that deletion is serialized. A concurrent remove() can still turn these entry points into a use-after-free.

Also applies to: 72-87


63-69: ⚠️ Potential issue | 🔴 Critical

Don't let callbacks mutate the live registry during this iteration.

Because the callback runs inside the range-for over ledger.file_systems, a reentrant file_system_add() or file_system_remove() can reallocate or erase the std::vector and invalidate the loop iterators mid-iteration. Either forbid registry mutation from callbacks or iterate over stable ownership instead of the live container.

Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp (1)

121-125: ⚠️ Potential issue | 🔴 Critical

Use fs_data in the LDO cleanup path.

Lines 122-124 still access data->pwr_ctrl_handle, but data is the raw void* parameter here. That does not compile when SOC_SD_PWR_CTRL_SUPPORTED is enabled, and it bypasses the intended cleanup. Use the casted fs_data consistently.

🐛 Proposed fix
 `#if` SOC_SD_PWR_CTRL_SUPPORTED
-        if (data->pwr_ctrl_handle) {
-            sd_pwr_ctrl_del_on_chip_ldo(data->pwr_ctrl_handle);
-            data->pwr_ctrl_handle = nullptr;
+        if (fs_data->pwr_ctrl_handle) {
+            sd_pwr_ctrl_del_on_chip_ldo(fs_data->pwr_ctrl_handle);
+            fs_data->pwr_ctrl_handle = nullptr;
         }
 `#endif`

Run this to confirm there are no remaining raw data->pwr_ctrl_handle accesses in this file:

#!/bin/bash
set -euo pipefail
fd 'esp32_sdmmc_fs\.cpp$' -x sh -c '
  for f; do
    echo ">>> $f"
    sed -n "118,126p" "$f"
    echo
    rg -n "data->pwr_ctrl_handle|fs_data->pwr_ctrl_handle" "$f"
  done
' sh {}

Expected result: the cleanup block only uses fs_data->pwr_ctrl_handle.

Tactility/Source/hal/sdcard/SdCardDevice.cpp (1)

48-49: ⚠️ Potential issue | 🔴 Critical

Deregister the filesystem before base destruction starts.

SdCardDevice::~SdCardDevice() calls file_system_remove(), and that path immediately asks the registered API whether the device is mounted. At this point the base destructor is already running, so that callback can observe derived state after teardown has begun. Move deregistration into the derived shutdown path, or otherwise perform it before SdCardDevice::~SdCardDevice() executes.

Run this to inspect the mounted-state call chain involved in teardown:

#!/bin/bash
set -euo pipefail

printf '== SdCardDevice declaration ==\n'
fd 'SdCardDevice\.h$' -x sed -n '1,220p' {}

printf '\n== SdCardDevice implementation ==\n'
fd 'SdCardDevice\.cpp$' -x sed -n '1,120p' {}

printf '\n== Mounted-state call chain ==\n'
rg -n -C3 '\bisMounted\s*\(|\bgetState\s*\(|file_system_remove\s*\('

Expected result: file_system_remove() reaches mounted-state logic during base destruction.

Platforms/platform-esp32/source/drivers/esp32_sdmmc.cpp (1)

136-153: ⚠️ Potential issue | 🟠 Major

stop() skips all teardown after an unmount error.

If Lines 137-140 are hit, the driver keeps the filesystem registration, filesystem handle, GPIO reservations, and device driver data. That leaves the device half-stopped on any unmount failure. Either continue cleanup after logging, or make the stop path explicitly retry-safe instead of returning with ownership still held.

🧹 Nitpick comments (1)
Platforms/platform-esp32/source/drivers/esp32_gpio.cpp (1)

148-153: Consider using std::nothrow for allocation consistency.

Line 151 uses new Esp32GpioInternal() which throws on allocation failure, while the kernel's gpio_controller_init_descriptors uses new(std::nothrow). If exceptions are disabled (common in ESP-IDF), the throwing new typically aborts on failure.

For consistency with kernel code patterns, consider:

♻️ Optional refactor for allocation consistency
 static error_t start(Device* device) {
     LOG_I(TAG, "start %s", device->name);
     const Esp32GpioConfig* config = GET_CONFIG(device);
-    auto* internal = new Esp32GpioInternal();
+    auto* internal = new(std::nothrow) Esp32GpioInternal();
+    if (!internal) return ERROR_OUT_OF_MEMORY;
     return gpio_controller_init_descriptors(device, config->gpioCount, internal);
 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20d534da-9288-40d6-8138-61d3a744920a

📥 Commits

Reviewing files that changed from the base of the PR and between 9efc7c7 and 4b48ec8.

📒 Files selected for processing (8)
  • Platforms/platform-esp32/source/drivers/esp32_gpio.cpp
  • Platforms/platform-esp32/source/drivers/esp32_sdmmc.cpp
  • Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp
  • Tactility/Include/Tactility/service/wifi/Wifi.h
  • Tactility/Source/hal/sdcard/SdCardDevice.cpp
  • TactilityKernel/include/tactility/drivers/gpio_controller.h
  • TactilityKernel/include/tactility/drivers/wifi.h
  • TactilityKernel/source/filesystem/file_system.cpp

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
TactilityKernel/include/tactility/drivers/wifi.h (1)

62-63: ⚠️ Potential issue | 🟡 Minor

Fix the mismatched comment for WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT.

The comment says "WifiAccessPointState changed" but this event type is for station connection results. This appears to be a copy-paste error from line 64.

📝 Proposed fix
-    /** WifiAccessPointState changed */
+    /** Station connection result available */
     WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT,
🧹 Nitpick comments (1)
TactilityKernel/include/tactility/drivers/wifi.h (1)

1-13: Add forward declaration for struct DeviceType.

The header uses struct DeviceType on line 201 but doesn't include tactility/device.h or provide a forward declaration. While this compiles in C (the struct keyword introduces the type), making the header self-contained improves usability for consumers.

🔧 Proposed fix
 struct Device;
+struct DeviceType;

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7dd643ff-b6d6-4791-bba1-59d47b34aaf9

📥 Commits

Reviewing files that changed from the base of the PR and between 4b48ec8 and f8f01ed.

📒 Files selected for processing (1)
  • TactilityKernel/include/tactility/drivers/wifi.h

@KenVanHoeylandt KenVanHoeylandt merged commit aa7530e into main Mar 7, 2026
82 of 103 checks passed
@KenVanHoeylandt KenVanHoeylandt deleted the develop branch March 7, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant