Tab5 camera + other stuff (#558)

* Tab5 camera + other stuff

Tab5 camera driver - SC2356
Custom SliderBox widget - slider with plus and minus buttons + value label and snapping
Rtc Time service + rtc api
Sdk release - only include drivers built for that specific target, eg: sc2356 driver is mipi / p4 only.
No more hardcoded manual sdk cmakelists
New function to find device by compatible string match. no more static cast bool wildness when trying to match a single device (like M5Stack PaperS3 for example)

* feedback + fixes

Fixed external app user data path.
fix(gui): block app teardown until onHide() completes, preventing ELF unload racing a still-running app
added camera device type and api

* drain the snake sssem

---------

Co-authored-by: Ken Van Hoeylandt <git@kenvanhoeylandt.net>
This commit is contained in:
Shadowtrance
2026-07-10 16:45:03 +10:00
committed by GitHub
parent 16a61a087c
commit 7a7f09be35
47 changed files with 1894 additions and 83 deletions
@@ -63,6 +63,14 @@ void GuiService::onLoaderEvent(LoaderService::Event event) {
auto app_instance = std::static_pointer_cast<app::AppInstance>(app::getCurrentAppContext());
item = new GuiDispatchItem{this, GuiDispatchType::Show, app_instance};
} else if (event == LoaderService::Event::ApplicationHiding) {
// hideDoneSem is a binary semaphore signaled by every hideApp() completion,
// including the one showApp() triggers internally (GuiDispatchType::Show, when an
// app is already being shown) - that release has no waiter and leaves a stale
// permit sitting available. Drain it before dispatching, or the acquire() below
// could consume that leftover permit instead of the one this specific Hide
// dispatch is about to produce, letting Destroyed run before the real onHide()
// for this app has finished.
hideDoneSem.acquire(0);
item = new GuiDispatchItem{this, GuiDispatchType::Hide, nullptr};
} else {
return;
@@ -71,6 +79,19 @@ void GuiService::onLoaderEvent(LoaderService::Event event) {
if (dispatcher_dispatch(dispatcher, item, onGuiDispatch) != ERROR_NONE) {
LOG_E(TAG, "Failed to dispatch gui event");
delete item;
return;
}
if (event == LoaderService::Event::ApplicationHiding) {
// Block here (still on the Loader thread, inside publish()'s synchronous
// subscriber call) until hideApp() has actually run to completion on the GUI
// task. LoaderService::transitionAppToState(Hiding) must not return - and
// therefore the Destroyed transition right after it, which unloads an ELF app's
// code, must not run - until App::onHide() has fully finished. Bounded so a stuck
// GUI task can't wedge app shutdown forever.
if (!hideDoneSem.acquire(pdMS_TO_TICKS(5000))) {
LOG_E(TAG, "Timed out waiting for hideApp() to complete");
}
}
}
@@ -282,6 +303,14 @@ void GuiService::showApp(std::shared_ptr<app::AppInstance> app) {
}
void GuiService::hideApp() {
// Signals hideDoneSem on every return path (including the early-return guards below) -
// onLoaderEvent() blocks on this to know App::onHide() has actually finished before
// Loader proceeds to destroy the app (see hideDoneSem's declaration for why).
struct SignalOnExit {
Semaphore& sem;
~SignalOnExit() { sem.release(); }
} signal_on_exit { hideDoneSem };
auto lock = mutex.asScopedLock();
lock.lock();
@@ -0,0 +1,26 @@
#include <Tactility/service/rtctime/RtcTime.h>
#ifdef ESP_PLATFORM
#include <Tactility/service/rtctime/RtcTimeService.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#endif
namespace tt::service::rtctime {
#ifdef ESP_PLATFORM
extern const ServiceManifest manifest;
#endif
bool isAvailable() {
#ifdef ESP_PLATFORM
// The service is only registered when an RTC device is present (see Tactility.cpp);
// treat "not registered" the same as "no device bound" rather than asserting.
auto service = findServiceById<RtcTimeService>(manifest.id);
return service != nullptr && service->isAvailable();
#else
return false;
#endif
}
} // namespace tt::service::rtctime
@@ -0,0 +1,152 @@
#ifdef ESP_PLATFORM
#include <Tactility/service/rtctime/RtcTimeService.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <tactility/device.h>
#include <tactility/drivers/rtc.h>
#include <tactility/log.h>
#include <cassert>
#include <ctime>
#include <sys/time.h>
namespace tt::service::rtctime {
constexpr auto* TAG = "RtcTime";
Device* RtcTimeService::findRtcDevice() {
if (!rtcDevice) {
rtcDevice = device_find_first_active_by_type(&RTC_TYPE);
}
return rtcDevice;
}
bool RtcTimeService::isAvailable() const {
return rtcDevice != nullptr;
}
static bool setSystemTimeFromRtc(Device* rtc) {
RtcDateTime dt = {};
error_t err = rtc_get_time(rtc, &dt);
if (err != ERROR_NONE) {
LOG_E(TAG, "Failed to read RTC datetime");
return false;
}
if (dt.year < 2024 || dt.year > 2199 ||
dt.month < 1 || dt.month > 12 ||
dt.day < 1 || dt.day > 31 ||
dt.hour > 23 || dt.minute > 59 || dt.second > 59) {
LOG_W(TAG, "Ignoring invalid RTC datetime: %02d-%02d-%02d %02d:%02d:%02d", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
return false;
}
struct tm tm_struct = {};
tm_struct.tm_year = dt.year - 1900;
tm_struct.tm_mon = dt.month - 1;
tm_struct.tm_mday = dt.day;
tm_struct.tm_hour = dt.hour;
tm_struct.tm_min = dt.minute;
tm_struct.tm_sec = dt.second;
tm_struct.tm_isdst = -1;
time_t t = mktime(&tm_struct);
if (t == -1) {
LOG_E(TAG, "Failed to convert RTC datetime to time_t");
return false;
}
timeval tv = {.tv_sec = t, .tv_usec = 0};
int result = settimeofday(&tv, nullptr);
if (result != 0) {
LOG_E(TAG, "settimeofday failed");
return false;
}
LOG_I(TAG, "System time set from RTC: %02d-%02d-%02d %02d:%02d:%02d", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
return true;
}
static void writeRtcFromSystemTime(Device* rtc) {
time_t now = time(nullptr);
tm tm_struct = {};
if (!localtime_r(&now, &tm_struct)) {
LOG_E(TAG, "localtime_r failed");
return;
}
RtcDateTime dt = {
.year = static_cast<uint16_t>(tm_struct.tm_year + 1900),
.month = static_cast<uint8_t>(tm_struct.tm_mon + 1),
.day = static_cast<uint8_t>(tm_struct.tm_mday),
.hour = static_cast<uint8_t>(tm_struct.tm_hour),
.minute = static_cast<uint8_t>(tm_struct.tm_min),
.second = static_cast<uint8_t>(tm_struct.tm_sec)
};
error_t err = rtc_set_time(rtc, &dt);
if (err != ERROR_NONE) {
LOG_E(TAG, "Failed to write system time to RTC");
} else {
LOG_I(TAG, "RTC updated from system time: %02d-%02d-%02d %02d:%02d:%02d", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
}
}
void RtcTimeService::onTimeChanged(kernel::SystemEvent event) {
if (event == kernel::SystemEvent::Time) {
Device* rtc = findRtcDevice();
if (rtc) {
writeRtcFromSystemTime(rtc);
}
}
}
bool RtcTimeService::onStart(ServiceContext& serviceContext) {
Device* rtc = findRtcDevice();
if (!rtc) {
LOG_W(TAG, "No RTC device found");
return true; // Continue without RTC
}
if (setSystemTimeFromRtc(rtc)) {
// Publish time event so other components know time is now valid
kernel::publishSystemEvent(kernel::SystemEvent::Time);
}
timeEventSubscription = kernel::subscribeSystemEvent(
kernel::SystemEvent::Time,
[this](kernel::SystemEvent event) { onTimeChanged(event); }
);
return true;
}
void RtcTimeService::onStop(ServiceContext& serviceContext) {
if (timeEventSubscription != 0) {
kernel::unsubscribeSystemEvent(timeEventSubscription);
timeEventSubscription = 0;
}
}
extern const ServiceManifest manifest = {
.id = "RtcTime",
.createService = create<RtcTimeService>
};
// Precondition: RtcTimeService must already be registered and started. RtcTime.cpp's
// isAvailable() does NOT use this (it must tolerate the service never having been
// registered on RTC-less devices) - this is for callers that require the service to exist.
std::shared_ptr<RtcTimeService> findRtcTimeService() {
auto service = findServiceById(manifest.id);
assert(service != nullptr);
return std::static_pointer_cast<RtcTimeService>(service);
}
} // namespace tt::service::rtctime
#endif