Skip to content

Conversation

@KenVanHoeylandt
Copy link
Contributor

@KenVanHoeylandt KenVanHoeylandt commented Feb 7, 2026

  • Add kernel support for SPI driver
  • Add kernel support for UART driver
  • Implemented ESP32 UART kernel driver
  • Update existing UART-related code in Tactility to use new kernel driver
  • Remove UART from tt::hal::Configuration
  • Remove tt_hal_uart functionality but keep functions for now
  • Update devicetrees for UART changes
  • Kernel mutex and recursive mutex: improved locking API design
  • Other kernel improvements
  • Added device_exists_of_type() and device_find_by_name()

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

Consolidates mutex APIs to require a TickType_t timeout and updates callers and tests accordingly. Replaces the legacy tt::hal::uart C++/ESP/POSIX subsystem with a device-driven UART controller interface: new uart_controller API, kernel symbols, and C wrappers. Adds ESP32 platform drivers and bindings for SPI and UART (C drivers, headers, driver registrations) and many board DTS uart nodes using a new espressif,esp32-uart binding. Migrates GPS, HAL, and apps to the device-based UART APIs and adjusts an LVGL POSIX mutex callsite.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding kernel drivers for SPI/UART and improving locking API consistency.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

No actionable comments were generated in the recent review. 🎉


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.

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: 4

🧹 Nitpick comments (5)
TactilityKernel/Include/tactility/drivers/spi_controller.h (1)

64-71: Clarify timeout units in the public docs.
TickType_t implies FreeRTOS ticks; documenting units avoids callers passing milliseconds by mistake.

✏️ Suggested doc tweak
- * `@param`[in] timeout the maximum time to wait for the lock
+ * `@param`[in] timeout maximum time to wait for the lock, in FreeRTOS ticks
Platforms/PlatformEsp32/Source/drivers/esp32_spi.cpp (1)

16-27: Consider initializing initialized in the struct.

The initialized member is not initialized in the struct definition. While start() sets it to false before use, adding a default initializer improves clarity and prevents potential issues if the struct is used differently in the future.

♻️ Suggested improvement
 struct InternalData {
     Mutex mutex;
-    bool initialized;
+    bool initialized = false;
 
     InternalData() {
Platforms/PlatformEsp32/Source/drivers/esp32_uart.cpp (2)

34-35: Macro definitions include trailing semicolons.

The lock and unlock macros include trailing semicolons. When invoked as lock(data);, this produces double semicolons. While harmless, it's unconventional and could cause issues with certain coding styles or static analyzers.

♻️ Suggested improvement
-#define lock(data) mutex_lock(&data->mutex);
-#define unlock(data) mutex_unlock(&data->mutex);
+#define lock(data) mutex_lock(&data->mutex)
+#define unlock(data) mutex_unlock(&data->mutex)

126-137: Partial reads return ERROR_TIMEOUT without indicating bytes actually read.

When uart_read_bytes returns fewer bytes than requested (line 134), the function returns ERROR_TIMEOUT. The caller has no way to know how many bytes were actually read. Consider whether the API should provide this information, or document that partial reads discard the data.

TactilityKernel/Include/tactility/drivers/uart_controller.h (1)

90-90: Add const qualifier to write buffer parameters.

The buffer parameter in write_bytes is an input buffer that should not be modified. Adding const improves API safety and allows passing const buffers.

♻️ Suggested fix in header
-    error_t (*write_bytes)(struct Device* device, uint8_t* buffer, size_t buffer_size, TickType_t timeout);
+    error_t (*write_bytes)(struct Device* device, const uint8_t* buffer, size_t buffer_size, TickType_t timeout);
-error_t uart_controller_write_bytes(struct Device* device, uint8_t* buffer, size_t buffer_size, TickType_t timeout);
+error_t uart_controller_write_bytes(struct Device* device, const uint8_t* buffer, size_t buffer_size, TickType_t timeout);

This change would also require updating the implementation in uart_controller.cpp and esp32_uart.cpp.

Also applies to: 148-148

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: 3

🧹 Nitpick comments (4)
Modules/hal-device-module/Source/drivers/hal_device.cpp (1)

113-115: Consider using designated initializer for consistency.

Other DeviceType definitions in this PR use .name = "..." syntax (e.g., i2s_controller.cpp, gpio_controller.cpp). Using the same style here improves readability and consistency.

♻️ Suggested change
 const struct DeviceType HAL_DEVICE_TYPE {
-    "hal-device"
+    .name = "hal-device"
 };
Platforms/PlatformEsp32/Source/drivers/esp32_spi.cpp (1)

89-101: Consider locking mutex during stop for consistency with UART driver.

The stop() function accesses and frees the SPI bus without holding the mutex. While the device framework may guarantee no concurrent access during stop, the UART driver (esp32_uart.cpp lines 233-235) locks before cleanup. Consider adding locking for consistency and defensive safety.

♻️ Suggested refactor
 static error_t stop(Device* device) {
     ESP_LOGI(TAG, "stop %s", device->name);
     auto* driver_data = GET_DATA(device);
     auto* dts_config = GET_CONFIG(device);

+    mutex_lock(&driver_data->mutex);
     if (driver_data->initialized) {
         spi_bus_free(dts_config->host);
     }
+    mutex_unlock(&driver_data->mutex);

     device_set_driver_data(device, nullptr);
     delete driver_data;
     return ERROR_NONE;
 }
Platforms/PlatformEsp32/Source/drivers/esp32_uart.cpp (2)

17-36: Minor: Lock/unlock macros include trailing semicolons.

The macro definitions on lines 35-36 end with semicolons, causing double-semicolons at call sites (e.g., lock(driver_data); expands to mutex_lock(&driver_data->mutex);;). This is harmless but unconventional.

♻️ Suggested fix
-#define lock(data) mutex_lock(&data->mutex);
-#define unlock(data) mutex_unlock(&data->mutex);
+#define lock(data) mutex_lock(&data->mutex)
+#define unlock(data) mutex_unlock(&data->mutex)

219-219: Minor: Inconsistent logging functions used.

ESP_LOGI is used directly (lines 219, 229, 243) while LOG_E from tactility/log.h is used elsewhere (line 191). Consider using LOG_I/LOG_E consistently throughout for uniformity with the rest of the codebase.

Also applies to: 229-229, 243-243

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: 5

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: 16

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)
Devices/guition-jc8048w550c/guition,jc8048w550c.dts (1)

27-44: ⚠️ Potential issue | 🟠 Major

UART1 pins conflict with I2C external bus.

GPIO 17 and 18 are assigned to both i2c_external (SDA/SCL on lines 31–32) and uart1 (RX/TX on lines 41–40). Enabling both nodes simultaneously will cause hardware contention. Consider disabling uart1 by default with status = "disabled", or document that these are mutually exclusive configurations.

🛠️ Example mitigation (disable UART until explicitly enabled)
 uart1 {
     compatible = "espressif,esp32-uart";
     port = <UART_NUM_1>;
     pin-tx = <18>;
     pin-rx = <17>;
     pin-cts = <GPIO_PIN_NONE>;
     pin-rtx = <GPIO_PIN_NONE>;
+    status = "disabled";
 };
Tactility/Source/hal/gps/Ublox.cpp (2)

61-85: ⚠️ Potential issue | 🟠 Major

Timeout loop mixes getMillis with getTicks.

startTime uses kernel::getMillis() while the loop compares kernel::getTicks() against waitMillis. If ticks aren’t milliseconds, the timeout becomes incorrect. Use a single time base (or convert waitMillis to ticks).

🛠️ Proposed fix (use millis consistently)
-    uint32_t startTime = kernel::getMillis();
+    uint32_t startTime = kernel::getMillis();
@@
-    while (kernel::getTicks() - startTime < waitMillis) {
+    while (kernel::getMillis() - startTime < waitMillis) {

184-189: ⚠️ Potential issue | 🟠 Major

uart_controller_read_bytes result ignored; ACK payload path always fails.

read_bytes is never assigned, so the read_bytes != needRead check always fails and the ACK handler never returns payload data. Capture the API result and gate on success.

🛠️ Proposed fix
-                    auto read_bytes = 0U;
-                    uart_controller_read_bytes(uart, buffer, needRead, 250 / portTICK_PERIOD_MS);
-                    if (read_bytes != needRead) {
+                    const auto err = uart_controller_read_bytes(uart, buffer, needRead, 250 / portTICK_PERIOD_MS);
+                    if (err != ERROR_NONE) {
                         ubxFrameCounter = 0;
                     } else {
                         // return payload length
 `#ifdef` GPS_DEBUG
                         LOGGER.info("Got ACK for class {:02X} message {:02X} in {}ms", requestedClass, requestedId, kernel::getMillis() - startTime);
 `#endif`
                         return needRead;
                     }
Tactility/Source/hal/gps/GpsInit.cpp (1)

79-93: ⚠️ Potential issue | 🟠 Major

Timeout loop mixes getMillis with getTicks.

startTime is from kernel::getMillis() but the loop compares kernel::getTicks() to waitMillis. If ticks aren’t milliseconds, the timeout is incorrect. Use the same time base or convert waitMillis to ticks.

🛠️ Proposed fix (use millis consistently)
-    uint32_t startTime = kernel::getMillis();
+    uint32_t startTime = kernel::getMillis();
@@
-    while (kernel::getTicks() - startTime < waitMillis) {
+    while (kernel::getMillis() - startTime < waitMillis) {
🟡 Minor comments (8)
Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts-42-42 (1)

42-42: ⚠️ Potential issue | 🟡 Minor

Typo: pin-rtx should be pin-rts.

Standard UART flow control uses RTS (Request To Send), not "RTX".

Proposed fix
-		pin-rtx = <GPIO_PIN_NONE>;
+		pin-rts = <GPIO_PIN_NONE>;
Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts-36-43 (1)

36-43: ⚠️ Potential issue | 🟡 Minor

Add a clarifying comment to document Grove port mutual exclusivity.

The uart_grove and i2c_grove nodes legitimately share GPIO pins 32 and 33 on M5Stack hardware—this is consistent with the M5Stack StickC Plus design where a single Grove connector supports both I2C and UART protocols but not simultaneously. However, the DTS file lacks documentation explaining this mutual exclusivity. Add an inline comment to the i2c_grove or uart_grove node clarifying that the Grove port must be configured as either I2C or UART but not both.

Devices/m5stack-core2/m5stack,core2.dts-57-57 (1)

57-57: ⚠️ Potential issue | 🟡 Minor

Typo: pin-rtx should be pin-rts.

Proposed fix
-		pin-rtx = <GPIO_PIN_NONE>;
+		pin-rts = <GPIO_PIN_NONE>;
Devices/m5stack-cores3/m5stack,cores3.dts-79-79 (1)

79-79: ⚠️ Potential issue | 🟡 Minor

Typo: pin-rtx should be pin-rts.

Proposed fix
-		pin-rtx = <GPIO_PIN_NONE>;
+		pin-rts = <GPIO_PIN_NONE>;
TactilityKernel/Include/tactility/device.h-49-49 (1)

49-49: ⚠️ Potential issue | 🟡 Minor

Minor typo in comment: trailing ::.

Proposed fix
- * This is used by the devicetree code generator and the application init sequence::.
+ * This is used by the devicetree code generator and the application init sequence.
Devices/cyd-2432s028rv3/cyd,2432s028rv3.dts-31-31 (1)

31-31: ⚠️ Potential issue | 🟡 Minor

Typo: pin-rtx should be pin-rts.

Same issue as in other DTS files—RTS is the standard UART flow control signal name.

Proposed fix
-		pin-rtx = <GPIO_PIN_NONE>;
+		pin-rts = <GPIO_PIN_NONE>;
Devices/elecrow-crowpanel-basic-35/elecrow,crowpanel-basic-35.dts-33-33 (1)

33-33: ⚠️ Potential issue | 🟡 Minor

Typo: pin-rtx should be pin-rts.

RTS (Request To Send) is the standard UART flow control signal name. This appears to be a typo.

Proposed fix
-		pin-rtx = <GPIO_PIN_NONE>;
+		pin-rts = <GPIO_PIN_NONE>;
Tactility/Private/Tactility/hal/gps/Ublox.h-17-22 (1)

17-22: ⚠️ Potential issue | 🟡 Minor

sendPacket is unused code and never called anywhere in the codebase.

The function builds a packet buffer but doesn't transmit it. However, this isn't a problem with hidden dependencies—sendPacket has zero call sites. Either remove this dead code or, if it's intended as infrastructure for future use, implement it by calling uart_controller_write_bytes (as done elsewhere in Ublox.cpp, Probe.cpp, and GpsInit.cpp).

🧹 Nitpick comments (3)
TactilityC/Include/tt_hal_uart.h (1)

12-17: Minor formatting inconsistency in comment block.

Line 16 is missing a leading space before the asterisk, breaking the standard multi-line comment alignment.

Suggested fix
 /**
  * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  * WARNING: THIS API IS NON-FUNCTIONAL AND DEPRECATED.
  * IT WILL BE REMOVED IN A FUTURE RELEASE ONCE OFFICIAL APPS ARE MIGRATED.
-* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  */
Tactility/Source/hal/gps/Probe.cpp (1)

100-107: Avoid hard-coded byte counts for NMEA commands.

The manual lengths (e.g., Line 102-108) are easy to miscount and can truncate/extend writes. Consider sizeof("...") - 1 or a constexpr string to keep the size in sync; apply the same pattern to the other literals in this function.

♻️ Example refactor
-    uart_controller_write_bytes(uart, (const uint8_t*)"$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n", 40, 500);
+    constexpr char kCloseAllNmea[] = "$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n";
+    uart_controller_write_bytes(uart, (const uint8_t*)kCloseAllNmea, sizeof(kCloseAllNmea) - 1, 500);
Tactility/Source/hal/gps/GpsInit.cpp (1)

175-186: Prefer sizeof for command byte counts.

The hard-coded literal lengths (Line 176-186) are easy to miscount. Using sizeof("...") - 1 or a constexpr string keeps the length correct and maintainable; apply the same pattern to the other literals in this file.

♻️ Example refactor
-    uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR066,1,0,1,0,0,1*3B\r\n", 25, 250); // Enable GPS+GALILEO+NAVIC
+    constexpr char kEnableGnss[] = "$PAIR066,1,0,1,0,0,1*3B\r\n";
+    uart_controller_write_bytes(uart, (const uint8_t*)kEnableGnss, sizeof(kEnableGnss) - 1, 250); // Enable GPS+GALILEO+NAVIC

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

Caution

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

⚠️ Outside diff range comments (1)
Tactility/Source/hal/gps/GpsDevice.cpp (1)

46-63: ⚠️ Potential issue | 🔴 Critical

UART resource leak on early-return paths after open.

If uart_controller_open succeeds (line 40) but probe (line 48) or init (line 59) fails, threadMain returns -1 without calling uart_controller_close. This leaves the ESP UART driver installed and the device marked as open, preventing future re-opens.

🐛 Proposed fix — close UART before early returns
     GpsModel model = configuration.model;
     if (model == GpsModel::Unknown) {
         model = probe(uart);
         if (model == GpsModel::Unknown) {
             LOGGER.error("Probe failed");
             setState(State::Error);
+            uart_controller_close(uart);
             return -1;
         }
     }
     mutex.lock();
     this->model = model;
     mutex.unlock();
 
     if (!init(uart, model)) {
         LOGGER.error("Init failed");
         setState(State::Error);
+        uart_controller_close(uart);
         return -1;
     }
🧹 Nitpick comments (2)
Platforms/PlatformEsp32/Source/drivers/esp32_uart.cpp (2)

32-33: Parenthesize macro parameter device in GET_CONFIG.

Same pattern as the SPI driver — device->config should be (device)->config to be safe against complex macro arguments.

♻️ Proposed fix
-#define GET_CONFIG(device) ((Esp32UartConfig*)device->config)
-#define GET_DATA(device) ((Esp32UartInternal*)device_get_driver_data(device))
+#define GET_CONFIG(device) ((Esp32UartConfig*)(device)->config)
+#define GET_DATA(device) ((Esp32UartInternal*)device_get_driver_data(device))

274-290: uart_driver_delete return value is unchecked.

If uart_driver_delete fails (e.g., due to pending I/O), the driver state is still marked as closed. Consider logging or propagating the error.

♻️ Suggested improvement
-    uart_driver_delete(dts_config->port);
-    driver_data->is_open = false;
-    unlock(driver_data);
+    esp_err_t err = uart_driver_delete(dts_config->port);
+    if (err != ESP_OK) {
+        LOG_E(TAG, "Failed to delete UART driver: %s", esp_err_to_name(err));
+        unlock(driver_data);
+        return esp_err_to_error(err);
+    }
+    driver_data->is_open = false;
+    unlock(driver_data);

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: 2

Caution

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

⚠️ Outside diff range comments (1)
Tactility/Source/hal/gps/GpsInit.cpp (1)

81-93: ⚠️ Potential issue | 🔴 Critical

Bug: startTime is in milliseconds but the loop compares it against ticks.

Line 81 captures startTime via kernel::getMillis() (milliseconds), but line 93 subtracts it from kernel::getTicks() (ticks) and compares against waitTicks (also ticks). When configTICK_RATE_HZ != 1000, this produces an incorrect timeout — either exiting immediately or spinning far too long.

Use milliseconds consistently, or ticks consistently:

🐛 Proposed fix (use ticks consistently)
-    uint32_t startTime = kernel::getMillis();
     uint8_t buffer[CAS_ACK_NACK_MSG_SIZE] = {0};
     uint8_t bufferPos = 0;
     TickType_t waitTicks = pdMS_TO_TICKS(waitMillis);
+    TickType_t startTick = kernel::getTicks();
 
     ...
 
-    while (kernel::getTicks() - startTime < waitTicks) {
+    while (kernel::getTicks() - startTick < waitTicks) {

Note: if kernel::getMillis() is still needed for the debug log on line 121, you can capture it separately or compute elapsed from ticks.

🧹 Nitpick comments (3)
TactilityKernel/Include/tactility/device.h (1)

262-276: New lookup APIs look good; minor note on device_find_by_name lifetime guarantees.

Both new functions are well-documented and the implementations correctly hold the ledger lock during iteration. One subtle point: device_find_by_name returns a Device* after releasing the ledger lock, so callers face a TOCTOU window if the device can be concurrently removed. This is a common pattern in device registries so not necessarily a problem, but consider adding a brief doc note (e.g., "The caller must ensure the device is not removed concurrently, or lock the device before use") if this is a concern for your use cases.

Tactility/Source/hal/gps/GpsInit.cpp (1)

94-97: Return values from uart_controller_* calls are silently discarded throughout this file.

All uart_controller_write_bytes, uart_controller_read_byte, and uart_controller_get_available calls return error_t, but no call site checks the result. A failed write/read would go unnoticed, potentially causing silent GPS misconfiguration. Consider at least logging on error for the critical ACK-read path here, and optionally for the init write sequences.

Tactility/Source/hal/gps/Ublox.cpp (1)

215-230: Minor: Log messages lost device context.

Previously these warnings likely included baud rate info. The replacements on Lines 226 and 229 now just say "No GNSS Module" and "UBlox Frame Errors" without identifying which UART device. Consider logging the device name for easier debugging on boards with multiple UARTs.

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 (2)
Tactility/Source/hal/gps/Ublox.cpp (1)

185-195: ⚠️ Potential issue | 🔴 Critical

Critical bug: read_bytes is never assigned the result of uart_controller_read_bytes, so the payload is always discarded.

auto read_bytes = 0U; is initialized to zero, and the return value of uart_controller_read_bytes (an error_t) is discarded on line 186. Consequently, read_bytes != needRead on line 187 is always true (when needRead > 0), causing every payload read to be treated as a failure. This function can never successfully return a parsed UBX response.

The old code likely used a UART read that returned the byte count. The new uart_controller_read_bytes returns error_t, so you need to check for ERROR_NONE instead of comparing byte counts.

Additionally, 250 / portTICK_PERIOD_MS should be pdMS_TO_TICKS(250) for correct and consistent ms-to-ticks conversion.

🐛 Proposed fix
-                    auto read_bytes = 0U;
-                    uart_controller_read_bytes(uart, buffer, needRead, 250 / portTICK_PERIOD_MS);
-                    if (read_bytes != needRead) {
+                    error_t err = uart_controller_read_bytes(uart, buffer, needRead, pdMS_TO_TICKS(250));
+                    if (err != ERROR_NONE) {
Devices/m5stack-cardputer/Source/Configuration.cpp (1)

14-16: ⚠️ Potential issue | 🟡 Minor

initBoot is missing the static keyword.

Every other device's Configuration.cpp declares initBoot as static. Without it, this symbol has external linkage, which is inconsistent and could cause link-time symbol collisions.

Proposed fix
-bool initBoot() {
+static bool initBoot() {
     return driver::pwmbacklight::init(LCD_PIN_BACKLIGHT, 512);
 }

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: 4

🧹 Nitpick comments (4)
Platforms/PlatformEsp32/Source/drivers/esp32_uart.cpp (3)

32-33: Parenthesize macro parameter to prevent operator-precedence bugs.

Same issue previously flagged in the SPI driver: if GET_CONFIG or GET_DATA is ever invoked with an expression (e.g., pointer arithmetic), the -> will bind incorrectly.

Proposed fix
-#define GET_CONFIG(device) ((Esp32UartConfig*)device->config)
-#define GET_DATA(device) ((Esp32UartInternal*)device_get_driver_data(device))
+#define GET_CONFIG(device) ((Esp32UartConfig*)(device)->config)
+#define GET_DATA(device) ((Esp32UartInternal*)device_get_driver_data(device))

Note: GET_DATA is fine since device is passed as a function argument, but GET_CONFIG directly uses device->config.


326-340: stop() unlocks the mutex after clearing driver data, then deletes — potential use-after-free window.

After unlock(driver_data) on line 336, a concurrent thread that already obtained the pointer via GET_DATA (before the lock) would unblock and operate on freed memory after delete driver_data on line 338. This is a narrow race but exists if stop and an I/O call overlap.

If the lifecycle guarantees that stop is only called when no other operations are in flight (similar to the SPI driver which doesn't lock at all in stop), this is acceptable — but worth a brief comment documenting that precondition.


68-85: Mutex is held for the entire duration of blocking uart_read_bytes — all other device operations stall.

read_byte and read_bytes hold the driver mutex across uart_read_bytes(…, timeout), which can block for the full timeout period. During that time, close(), get_available(), is_open(), etc. will all block. This is a deliberate correctness-vs-concurrency tradeoff, but for long timeouts it can cause unexpected stalls. Consider documenting this behavior or using a separate "open guard" (e.g., an atomic flag) for the open-state check so that the mutex only serializes lifecycle transitions.

Also applies to: 148-166

Platforms/PlatformEsp32/Source/drivers/esp32_spi.cpp (1)

93-105: stop() does not lock the mutex before accessing driver_data.

Unlike the UART driver's stop() which acquires the lock, this one directly accesses driver_data->initialized without locking. This is consistent with the pattern that stop is only called during exclusive lifecycle teardown, but it's inconsistent with the UART driver. Consider aligning the two for maintainability, or adding a brief comment noting the lifecycle precondition.

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: 2

🧹 Nitpick comments (3)
Platforms/PlatformEsp32/Source/drivers/esp32_spi.cpp (1)

111-120: const qualifier is silently cast away on line 116.

esp32_spi_api is declared const static but is cast to void*, discarding const. If the Driver::api field can be const void*, that would be preferable. If not, consider dropping const from the declaration to avoid the inconsistency.

Platforms/PlatformEsp32/Source/drivers/esp32_uart.cpp (2)

35-36: Consider inline functions instead of lock/unlock macros.

Function-like macros that expand to statements (mutex_lock, mutex_unlock) can cause surprises with scoping and debuggability. static inline helpers would give the same zero-overhead behavior with type safety and proper debugger stepping.

Example
-#define lock(data) mutex_lock(&data->mutex)
-#define unlock(data) mutex_unlock(&data->mutex)
+static inline void lock(Esp32UartInternal* data) { mutex_lock(&data->mutex); }
+static inline void unlock(Esp32UartInternal* data) { mutex_unlock(&data->mutex); }

236-236: RX buffer is 1024 bytes with TX buffer at 0 (blocking TX) — consider a brief comment.

uart_driver_install(dts_config->port, 1024, 0, ...) uses a 1 KiB RX ring buffer and blocking TX (no TX buffer). This is a reasonable default, but a one-line comment would prevent future confusion about the intentional choice (especially since a past review iteration had an inaccurate comment here).

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.

Caution

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

⚠️ Outside diff range comments (1)
Tactility/Source/hal/gps/Ublox.cpp (1)

19-26: ⚠️ Potential issue | 🔴 Critical

Critical: Macro SEND_UBX_PACKET references undefined symbol TIMEOUT.

Line 23 calls getAck(UART, TYPE, ID, TIMEOUT) but TIMEOUT is never defined anywhere in the codebase. The macro parameter TIMEOUT_MILLIS is converted and passed to uart_controller_write_bytes (line 22), but the undefined TIMEOUT symbol is passed to getAck instead, causing a compilation error.

Fix: Pass TIMEOUT_MILLIS to getAck as well:

if (getAck(UART, TYPE, ID, TIMEOUT_MILLIS) != GpsResponse::Ok) {

Alternatively, if different timeouts are intended for the write and read operations, add a second parameter to the macro for the ACK timeout.

🧹 Nitpick comments (3)
Platforms/PlatformEsp32/Source/drivers/esp32_uart.cpp (1)

236-236: Consider documenting the RX buffer size choice.

The RX buffer of 1024 bytes and TX buffer of 0 (blocking mode) are reasonable defaults, but the rationale isn't documented. A brief inline comment (e.g., why 1024 and not larger) would help future maintainers.

Tactility/Source/hal/gps/Ublox.cpp (2)

92-95: Unchecked error_t returns from uart_controller_get_available and uart_controller_read_byte.

Both calls discard their error_t return values. For get_available, the fallback is safe (available stays 0). For read_byte, a silent failure leaves b with stale data that feeds into the ACK state machine, potentially producing a false positive match. Consider at minimum logging or resetting ack on failure.

The same pattern repeats in the second getAck overload (lines 139–143).


61-67: Tick/ms unit consistency is improved but debug log on line 88 still mixes units.

startTime is now correctly assigned from kernel::getTicks() (line 66). However, the debug log on line 88 computes kernel::getMillis() - startTime, mixing milliseconds with a tick-based start time. This is numerically correct only when configTICK_RATE_HZ == 1000. Since line 88 is in a #ifdef GPS_DEBUG block and unchanged, this is very low priority.

Also applies to: 85-85

@KenVanHoeylandt KenVanHoeylandt merged commit 74127a5 into main Feb 7, 2026
52 checks passed
@KenVanHoeylandt KenVanHoeylandt deleted the develop branch February 7, 2026 20:28
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