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:
@@ -24,6 +24,7 @@
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/crypt_module.h>
|
||||
#include <tactility/drivers/grove.h>
|
||||
#include <tactility/drivers/rtc.h>
|
||||
#include <tactility/drivers/uart_controller.h>
|
||||
#include <tactility/filesystem/file_system.h>
|
||||
#include <tactility/hal_device_module.h>
|
||||
@@ -85,6 +86,7 @@ namespace service {
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace displayidle { extern const ServiceManifest manifest; }
|
||||
namespace keyboardidle { extern const ServiceManifest manifest; }
|
||||
namespace rtctime { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
namespace screenshot { extern const ServiceManifest manifest; }
|
||||
@@ -280,6 +282,9 @@ static void registerAndStartSecondaryServices() {
|
||||
addService(service::statusbar::manifest);
|
||||
addService(service::memorychecker::manifest);
|
||||
#if defined(ESP_PLATFORM)
|
||||
if (device_exists_of_type(&RTC_TYPE)) {
|
||||
addService(service::rtctime::manifest);
|
||||
}
|
||||
addService(service::displayidle::manifest);
|
||||
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
||||
addService(service::keyboardidle::manifest);
|
||||
|
||||
@@ -16,9 +16,9 @@ namespace tt::app {
|
||||
|
||||
std::string AppPaths::getUserDataPath() const {
|
||||
if (manifest.appLocation.isInternal()) {
|
||||
return std::format("{}{}/user/app/{}", PARTITION_PREFIX, file::DATA_PARTITION_NAME, manifest.appId);
|
||||
return std::format("{}{}/tactility/user/app/{}", PARTITION_PREFIX, file::DATA_PARTITION_NAME, manifest.appId);
|
||||
} else {
|
||||
return std::format("{}/user/app/{}", file::getFirstPathSegment(manifest.appLocation.getPath()), manifest.appId);
|
||||
return std::format("{}/tactility/user/app/{}", file::getFirstPathSegment(manifest.appLocation.getPath()), manifest.appId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration
|
||||
|
||||
#include <Tactility/lvgl/SliderBox.h>
|
||||
|
||||
#include <tactility/lvgl_fonts.h>
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
typedef struct {
|
||||
lv_obj_t obj;
|
||||
lv_obj_t* minusButton;
|
||||
lv_obj_t* slider;
|
||||
lv_obj_t* valueLabel;
|
||||
lv_obj_t* plusButton;
|
||||
int32_t min;
|
||||
int32_t max;
|
||||
int32_t step;
|
||||
} SliderBox;
|
||||
|
||||
static void sliderbox_constructor(const lv_obj_class_t* classPointer, lv_obj_t* obj);
|
||||
|
||||
static lv_obj_class_t sliderbox_class = {
|
||||
.base_class = &lv_obj_class,
|
||||
.constructor_cb = &sliderbox_constructor,
|
||||
.destructor_cb = nullptr,
|
||||
.event_cb = nullptr,
|
||||
.user_data = nullptr,
|
||||
.name = "tt_sliderbox",
|
||||
.width_def = LV_PCT(100),
|
||||
.height_def = LV_SIZE_CONTENT,
|
||||
.editable = false,
|
||||
.group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE,
|
||||
.instance_size = sizeof(SliderBox),
|
||||
.theme_inheritable = false
|
||||
};
|
||||
|
||||
static void sliderbox_constructor(const lv_obj_class_t* classPointer, lv_obj_t* obj) {
|
||||
LV_UNUSED(classPointer);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(obj, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(obj, 4, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(obj, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(obj, 0, LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
static void update_value_label(SliderBox* sliderBox) {
|
||||
auto value = lv_slider_get_value(sliderBox->slider);
|
||||
lv_label_set_text_fmt(sliderBox->valueLabel, "%" LV_PRId32, value);
|
||||
}
|
||||
|
||||
static void update_button_states(SliderBox* sliderBox) {
|
||||
auto value = lv_slider_get_value(sliderBox->slider);
|
||||
|
||||
if (value <= sliderBox->min) {
|
||||
lv_obj_add_state(sliderBox->minusButton, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_remove_state(sliderBox->minusButton, LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
if (value >= sliderBox->max) {
|
||||
lv_obj_add_state(sliderBox->plusButton, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_remove_state(sliderBox->plusButton, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
static void notify_value_changed(lv_obj_t* obj) {
|
||||
lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
}
|
||||
|
||||
static void on_slider_value_changed(lv_event_t* event) {
|
||||
auto* obj = static_cast<lv_obj_t*>(lv_event_get_user_data(event));
|
||||
auto* sliderBox = reinterpret_cast<SliderBox*>(obj);
|
||||
|
||||
auto raw_value = lv_slider_get_value(sliderBox->slider);
|
||||
auto steps_from_min = (raw_value - sliderBox->min + sliderBox->step / 2) / sliderBox->step;
|
||||
auto snapped_value = sliderBox->min + steps_from_min * sliderBox->step;
|
||||
if (snapped_value > sliderBox->max) {
|
||||
snapped_value = sliderBox->max;
|
||||
}
|
||||
|
||||
if (snapped_value != raw_value) {
|
||||
lv_slider_set_value(sliderBox->slider, snapped_value, LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
update_value_label(sliderBox);
|
||||
update_button_states(sliderBox);
|
||||
notify_value_changed(obj);
|
||||
}
|
||||
|
||||
static void on_step_button_clicked(lv_event_t* event, int32_t direction) {
|
||||
auto* obj = static_cast<lv_obj_t*>(lv_event_get_user_data(event));
|
||||
auto* sliderBox = reinterpret_cast<SliderBox*>(obj);
|
||||
|
||||
auto value = lv_slider_get_value(sliderBox->slider);
|
||||
auto new_value = value + direction * sliderBox->step;
|
||||
if (new_value < sliderBox->min) {
|
||||
new_value = sliderBox->min;
|
||||
} else if (new_value > sliderBox->max) {
|
||||
new_value = sliderBox->max;
|
||||
}
|
||||
|
||||
if (new_value != value) {
|
||||
lv_slider_set_value(sliderBox->slider, new_value, LV_ANIM_ON);
|
||||
update_value_label(sliderBox);
|
||||
notify_value_changed(obj);
|
||||
}
|
||||
|
||||
update_button_states(sliderBox);
|
||||
}
|
||||
|
||||
static void on_minus_button_clicked(lv_event_t* event) {
|
||||
on_step_button_clicked(event, -1);
|
||||
}
|
||||
|
||||
static void on_plus_button_clicked(lv_event_t* event) {
|
||||
on_step_button_clicked(event, 1);
|
||||
}
|
||||
|
||||
static lv_obj_t* create_step_button(lv_obj_t* parent, const char* symbol, lv_event_cb_t callback, void* userData, uint32_t size) {
|
||||
auto* button = lv_button_create(parent);
|
||||
lv_obj_remove_flag(button, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(button, size, size);
|
||||
lv_obj_set_style_pad_all(button, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_add_event_cb(button, callback, LV_EVENT_SHORT_CLICKED, userData);
|
||||
|
||||
auto* label = lv_label_create(button);
|
||||
lv_label_set_text(label, symbol);
|
||||
lv_obj_center(label);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
lv_obj_t* sliderbox_create(lv_obj_t* parent, int32_t min, int32_t max, int32_t step, int32_t value) {
|
||||
if (max <= min) {
|
||||
max = min + 1;
|
||||
}
|
||||
if (step <= 0) {
|
||||
step = 1;
|
||||
}
|
||||
|
||||
lv_obj_t* obj = lv_obj_class_create_obj(&sliderbox_class, parent);
|
||||
lv_obj_class_init_obj(obj);
|
||||
|
||||
auto* sliderBox = reinterpret_cast<SliderBox*>(obj);
|
||||
sliderBox->min = min;
|
||||
sliderBox->max = max;
|
||||
sliderBox->step = step;
|
||||
|
||||
auto button_size = lvgl_get_text_font_height(FONT_SIZE_DEFAULT) + 8;
|
||||
|
||||
sliderBox->minusButton = create_step_button(obj, LV_SYMBOL_MINUS, &on_minus_button_clicked, obj, button_size);
|
||||
|
||||
sliderBox->slider = lv_slider_create(obj);
|
||||
lv_obj_remove_flag(sliderBox->slider, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_flex_grow(sliderBox->slider, 1);
|
||||
lv_slider_set_range(sliderBox->slider, min, max);
|
||||
lv_obj_add_event_cb(sliderBox->slider, &on_slider_value_changed, LV_EVENT_VALUE_CHANGED, obj);
|
||||
|
||||
sliderBox->valueLabel = lv_label_create(obj);
|
||||
lv_obj_set_style_text_align(sliderBox->valueLabel, LV_TEXT_ALIGN_RIGHT, LV_STATE_DEFAULT);
|
||||
lv_label_set_long_mode(sliderBox->valueLabel, LV_LABEL_LONG_MODE_CLIP);
|
||||
|
||||
// Reserve enough width for the largest value so the layout doesn't jump around.
|
||||
// +4 padding wasn't enough margin for 3-digit values to never wrap; widened to
|
||||
// a flat per-character estimate instead of trusting raw glyph width too tightly.
|
||||
char buffer[16];
|
||||
lv_snprintf(buffer, sizeof(buffer), "%" LV_PRId32, min);
|
||||
auto width_min = lv_text_get_width(buffer, lv_strlen(buffer), lv_obj_get_style_text_font(sliderBox->valueLabel, LV_PART_MAIN), 0);
|
||||
lv_snprintf(buffer, sizeof(buffer), "%" LV_PRId32, max);
|
||||
auto width_max = lv_text_get_width(buffer, lv_strlen(buffer), lv_obj_get_style_text_font(sliderBox->valueLabel, LV_PART_MAIN), 0);
|
||||
auto max_width = (width_min > width_max) ? width_min : width_max;
|
||||
lv_obj_set_width(sliderBox->valueLabel, max_width + 8);
|
||||
|
||||
sliderBox->plusButton = create_step_button(obj, LV_SYMBOL_PLUS, &on_plus_button_clicked, obj, button_size);
|
||||
|
||||
sliderbox_set_value(obj, value, LV_ANIM_OFF);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
int32_t sliderbox_get_value(lv_obj_t* obj) {
|
||||
auto* sliderBox = reinterpret_cast<SliderBox*>(obj);
|
||||
return lv_slider_get_value(sliderBox->slider);
|
||||
}
|
||||
|
||||
void sliderbox_set_value(lv_obj_t* obj, int32_t value, lv_anim_enable_t anim) {
|
||||
auto* sliderBox = reinterpret_cast<SliderBox*>(obj);
|
||||
|
||||
if (value < sliderBox->min) {
|
||||
value = sliderBox->min;
|
||||
} else if (value > sliderBox->max) {
|
||||
value = sliderBox->max;
|
||||
}
|
||||
|
||||
lv_slider_set_value(sliderBox->slider, value, anim);
|
||||
update_value_label(sliderBox);
|
||||
update_button_states(sliderBox);
|
||||
}
|
||||
|
||||
void sliderbox_add_value_changed_cb(lv_obj_t* obj, lv_event_cb_t callback, void* userData) {
|
||||
lv_obj_add_event_cb(obj, callback, LV_EVENT_VALUE_CHANGED, userData);
|
||||
}
|
||||
|
||||
} // namespace tt::lvgl
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user