Refactor app loading and window management (#609)
This commit is contained in:
committed by
GitHub
parent
dc3f6104b8
commit
37c507544b
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* @brief key-value storage for general purpose.
|
||||
* Maps strings on a fixed set of data types.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A dictionary that maps keys (strings) onto several atomary types.
|
||||
* Opaque handle - allocate with bundle_alloc(), release with bundle_free().
|
||||
*/
|
||||
typedef struct Bundle Bundle;
|
||||
|
||||
Bundle* bundle_alloc(void);
|
||||
Bundle* bundle_clone(const Bundle* bundle);
|
||||
void bundle_free(Bundle* bundle);
|
||||
|
||||
/** @warning Undefined if @a key is absent or not a bool - check with bundle_has_bool()/bundle_opt_bool() first. */
|
||||
bool bundle_get_bool(const Bundle* bundle, const char* key);
|
||||
/** @warning Undefined if @a key is absent or not an int32 - check with bundle_has_int32()/bundle_opt_int32() first. */
|
||||
int32_t bundle_get_int32(const Bundle* bundle, const char* key);
|
||||
/** @warning Undefined if @a key is absent or not an int64 - check with bundle_has_int64()/bundle_opt_int64() first. */
|
||||
int64_t bundle_get_int64(const Bundle* bundle, const char* key);
|
||||
/**
|
||||
* @warning Undefined if @a key is absent or not a string - check with bundle_has_string()/bundle_opt_string() first.
|
||||
* @retval ERROR_BUFFER_OVERFLOW out_value_size is too small
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t bundle_get_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size);
|
||||
|
||||
bool bundle_has_bool(const Bundle* bundle, const char* key);
|
||||
bool bundle_has_int32(const Bundle* bundle, const char* key);
|
||||
bool bundle_has_int64(const Bundle* bundle, const char* key);
|
||||
bool bundle_has_string(const Bundle* bundle, const char* key);
|
||||
|
||||
bool bundle_opt_bool(const Bundle* bundle, const char* key, bool* out_value);
|
||||
bool bundle_opt_int32(const Bundle* bundle, const char* key, int32_t* out_value);
|
||||
bool bundle_opt_int64(const Bundle* bundle, const char* key, int64_t* out_value);
|
||||
/**
|
||||
* @retval ERROR_NOT_FOUND @a key is absent or not a string - @a out_value is left untouched
|
||||
* @retval ERROR_BUFFER_OVERFLOW out_value_size is too small - @a out_value is left untouched
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t bundle_opt_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size);
|
||||
|
||||
void bundle_put_bool(Bundle* bundle, const char* key, bool value);
|
||||
void bundle_put_int32(Bundle* bundle, const char* key, int32_t value);
|
||||
void bundle_put_int64(Bundle* bundle, const char* key, int64_t value);
|
||||
void bundle_put_string(Bundle* bundle, const char* key, const char* value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -66,6 +66,17 @@ struct KeyboardApi {
|
||||
* @retval ERROR_NOT_SUPPORTED when this device has no backlight
|
||||
*/
|
||||
error_t (*get_backlight)(struct Device* device, struct Device** backlight_device);
|
||||
|
||||
/**
|
||||
* @brief Optional: reports whether the keyboard is physically present right now. Only
|
||||
* meaningful for hot-pluggable/detachable keyboards (e.g. a removable accessory) whose
|
||||
* kernel device is constructed and started once at boot regardless of physical attachment -
|
||||
* leave NULL for a keyboard that's always physically present whenever its device is active
|
||||
* (the common case; callers must treat NULL the same as "always present").
|
||||
* @param[in] device the keyboard device
|
||||
* @return true if physically attached/present
|
||||
*/
|
||||
bool (*is_present)(struct Device* device);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -83,6 +94,14 @@ error_t keyboard_read_key(struct Device* device, struct KeyboardKeyData* data);
|
||||
*/
|
||||
error_t keyboard_get_backlight(struct Device* device, struct Device** backlight_device);
|
||||
|
||||
/**
|
||||
* @brief Whether the keyboard device is physically present right now. True when the driver
|
||||
* doesn't implement KeyboardApi::is_present (i.e. it's always physically present whenever its
|
||||
* device is active) - see that field's doc comment.
|
||||
* @param[in] device the keyboard device
|
||||
*/
|
||||
bool keyboard_is_present(struct Device* device);
|
||||
|
||||
extern const struct DeviceType KEYBOARD_TYPE;
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* @brief Key-value settings, persisted as a .properties file on disk (instead of NVS/in-memory).
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Opaque handle - open with preferences_open(), release with preferences_close().
|
||||
*/
|
||||
typedef struct Preferences Preferences;
|
||||
|
||||
/**
|
||||
* Open (or create) a preferences store backed by the properties file at @a path. The parent
|
||||
* directory is created (recursively, like mkdir -p) if it doesn't already exist. The file is
|
||||
* read into memory now; changes made with preferences_put_*() are only written back to disk by
|
||||
* preferences_close().
|
||||
* @param[in] path absolute or relative file path (e.g. "/data/settings.properties")
|
||||
* @return the new instance, or NULL if the parent directory couldn't be created, or on
|
||||
* allocation failure
|
||||
*/
|
||||
Preferences* preferences_open(const char* path);
|
||||
|
||||
/** Writes any pending preferences_put_*() changes to the backing file, then releases the
|
||||
* instance. */
|
||||
void preferences_close(Preferences* preferences);
|
||||
|
||||
bool preferences_has_bool(const Preferences* preferences, const char* key);
|
||||
bool preferences_has_int32(const Preferences* preferences, const char* key);
|
||||
bool preferences_has_int64(const Preferences* preferences, const char* key);
|
||||
bool preferences_has_string(const Preferences* preferences, const char* key);
|
||||
|
||||
bool preferences_opt_bool(const Preferences* preferences, const char* key, bool* out_value);
|
||||
bool preferences_opt_int32(const Preferences* preferences, const char* key, int32_t* out_value);
|
||||
bool preferences_opt_int64(const Preferences* preferences, const char* key, int64_t* out_value);
|
||||
/**
|
||||
* @retval ERROR_NOT_FOUND @a key is absent or not a string - @a out_value is left untouched
|
||||
* @retval ERROR_BUFFER_OVERFLOW out_value_size is too small - @a out_value is left untouched
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t preferences_opt_string(const Preferences* preferences, const char* key, char* out_value, size_t out_value_size);
|
||||
|
||||
/** Sets the value in the in-memory cache; only persisted to the backing file by
|
||||
* preferences_close(). */
|
||||
void preferences_put_bool(Preferences* preferences, const char* key, bool value);
|
||||
void preferences_put_int32(Preferences* preferences, const char* key, int32_t value);
|
||||
void preferences_put_int64(Preferences* preferences, const char* key, int64_t value);
|
||||
void preferences_put_string(Preferences* preferences, const char* key, const char* value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* @brief Generic string key-value ".properties" file.
|
||||
* @note Safely acquires/releases the filesystem mutex registered for the file's path (see
|
||||
* tactility/filesystem/file_mutex.h) - manual locking isn't needed.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Opaque handle - open with properties_file_open(), release with properties_file_close().
|
||||
*/
|
||||
typedef struct PropertiesFile PropertiesFile;
|
||||
|
||||
/**
|
||||
* Open (or create) a properties file at @a path. The file is read into memory now; changes
|
||||
* made with properties_file_set() are only written back to disk by properties_file_close().
|
||||
* @param[in] path absolute or relative file path (e.g. "/data/settings.properties") - the
|
||||
* parent directory must already exist
|
||||
* @return the new instance, or NULL on allocation failure, or NULL if @a path exists but a
|
||||
* genuine error prevented opening or reading it, e.g. a permissions error (a missing file is
|
||||
* not an error - the instance starts out empty in that case)
|
||||
*/
|
||||
PropertiesFile* properties_file_open(const char* path);
|
||||
|
||||
/**
|
||||
* Writes any pending properties_file_set() changes to the backing file (atomically - via a
|
||||
* temporary file in the same directory, renamed over the real path - so a write failure leaves
|
||||
* the previous on-disk content untouched rather than a truncated/partial file), then releases
|
||||
* the instance either way.
|
||||
* @retval ERROR_NONE the backing file was fully updated
|
||||
* @retval ERROR_RESOURCE writing failed (full filesystem, I/O error, ...) - the previous
|
||||
* on-disk content, if any, is unchanged; the in-memory changes are lost along with the instance
|
||||
*/
|
||||
error_t properties_file_close(PropertiesFile* file);
|
||||
|
||||
bool properties_file_has(const PropertiesFile* file, const char* key);
|
||||
|
||||
/**
|
||||
* @retval ERROR_NOT_FOUND @a key is absent - @a out_value is left untouched
|
||||
* @retval ERROR_BUFFER_OVERFLOW out_value_size is too small - @a out_value is left untouched
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t properties_file_get(const PropertiesFile* file, const char* key, char* out_value, size_t out_value_size);
|
||||
|
||||
/** Sets the value in the in-memory cache; only persisted to the backing file by
|
||||
* properties_file_close(). */
|
||||
void properties_file_set(PropertiesFile* file, const char* key, const char* value);
|
||||
|
||||
typedef void (*PropertiesFileVisitorFn)(const char* key, const char* value, void* context);
|
||||
|
||||
/** Invokes @a visitor for every key currently cached, in unspecified order. */
|
||||
void properties_file_for_each(const PropertiesFile* file, PropertiesFileVisitorFn visitor, void* context);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,10 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/semphr.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -25,20 +29,6 @@ enum SystemEventType {
|
||||
KERNEL_EVENT_TIME_CHANGED, // No data - fired whenever system time is set (NTP sync, RTC restore, manual change)
|
||||
};
|
||||
|
||||
/**
|
||||
* A system-wide event as delivered to a system_event_callback_t.
|
||||
* `data` points at the type-specific struct documented next to `type`'s enum value
|
||||
* in SystemEventType (or is NULL when none is documented).
|
||||
* It is only valid for the duration of the callback.
|
||||
*/
|
||||
struct SystemEvent {
|
||||
enum SystemEventType type;
|
||||
/** Microseconds since boot, from get_micros_since_boot(). */
|
||||
uint64_t timestamp;
|
||||
const void *data;
|
||||
size_t data_len;
|
||||
};
|
||||
|
||||
/** Data for KERNEL_EVENT_NETWORK_CONNECTED. */
|
||||
struct NetworkConnectedEvent {
|
||||
struct Device* device;
|
||||
@@ -71,9 +61,29 @@ struct ServiceStoppedEvent {
|
||||
const char* id;
|
||||
};
|
||||
|
||||
/** Size of the largest type-specific event struct documented in SystemEventType, i.e. the
|
||||
* embedded buffer size needed by SystemEvent/SystemEventSubscription to hold any event's
|
||||
* payload by value. */
|
||||
#define SYSTEM_EVENT_MAX_DATA_SIZE (sizeof(struct NetworkConnectedEvent))
|
||||
|
||||
/**
|
||||
* A system-wide event as delivered to a system_event_callback_t.
|
||||
* `data` (up to `data_len` bytes, `SYSTEM_EVENT_MAX_DATA_SIZE` max) is a by-value copy of the
|
||||
* type-specific struct documented next to `type`'s enum value in SystemEventType (or unused,
|
||||
* with `data_len` 0, when none is documented) - like SystemEventSubscription's `data`, but only
|
||||
* valid for the duration of the callback rather than for the subscription's lifetime.
|
||||
*/
|
||||
struct SystemEvent {
|
||||
enum SystemEventType type;
|
||||
/** Microseconds since boot, from get_micros_since_boot(). */
|
||||
uint64_t timestamp;
|
||||
uint8_t data[SYSTEM_EVENT_MAX_DATA_SIZE];
|
||||
size_t data_len;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param[in] event the event being delivered; only valid for the duration of the call
|
||||
* @param[in] context the context pointer passed to system_event_subscribe()
|
||||
* @param[in] context the context pointer passed to system_event_callback_add()
|
||||
*/
|
||||
typedef void (*system_event_callback_t)(struct SystemEvent* event, void* context);
|
||||
|
||||
@@ -82,7 +92,7 @@ typedef void (*system_event_callback_t)(struct SystemEvent* event, void* context
|
||||
* @warning Does not work in ISR context.
|
||||
* @warning @a callback is invoked synchronously, on the caller's task, from within
|
||||
* system_event_emit(). The internal subscription lock is not held during the call, so
|
||||
* @a callback may itself call system_event_subscribe(), system_event_unsubscribe() or
|
||||
* @a callback may itself call system_event_callback_add(), system_event_callback_remove() or
|
||||
* system_event_emit() without deadlocking - but a subscribe/unsubscribe made from within
|
||||
* a callback only takes effect for events emitted after the current system_event_emit()
|
||||
* call returns, since that call already snapshotted the subscriptions it will invoke.
|
||||
@@ -91,7 +101,7 @@ typedef void (*system_event_callback_t)(struct SystemEvent* event, void* context
|
||||
* @param[in] context an opaque pointer passed back to @a callback unmodified
|
||||
* @return ERROR_NONE on success
|
||||
*/
|
||||
error_t system_event_subscribe(
|
||||
error_t system_event_callback_add(
|
||||
enum SystemEventType type,
|
||||
system_event_callback_t callback,
|
||||
void *context
|
||||
@@ -100,11 +110,11 @@ error_t system_event_subscribe(
|
||||
/**
|
||||
* Remove a previously added subscription.
|
||||
* @warning Does not work in ISR context.
|
||||
* @param[in] type the event type passed to the matching system_event_subscribe() call
|
||||
* @param[in] callback the callback passed to the matching system_event_subscribe() call
|
||||
* @param[in] type the event type passed to the matching system_event_callback_add() call
|
||||
* @param[in] callback the callback passed to the matching system_event_callback_add() call
|
||||
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
|
||||
*/
|
||||
error_t system_event_unsubscribe(
|
||||
error_t system_event_callback_remove(
|
||||
enum SystemEventType type,
|
||||
system_event_callback_t callback
|
||||
);
|
||||
@@ -125,6 +135,127 @@ error_t system_event_emit(
|
||||
size_t data_len
|
||||
);
|
||||
|
||||
/** Size of the largest type-specific event struct documented in SystemEventType, i.e. the
|
||||
* embedded buffer size needed by SystemEventSubscription to hold any event's payload by value. */
|
||||
#define SYSTEM_EVENT_MAX_DATA_SIZE (sizeof(struct NetworkConnectedEvent))
|
||||
|
||||
/**
|
||||
* Poll subscription: caller-owned node, registered with system_event_subscribe()
|
||||
* and polled with system_event_await(). Unlike system_event_callback_t, `event` is a by-value
|
||||
* copy that remains valid for the subscription's lifetime (until the next matching event
|
||||
* overwrites it), not just for the duration of a callback.
|
||||
* @warning Must be zero-initialized before the first system_event_subscribe() call (e.g.
|
||||
* `SystemEventSubscription sub = {};` in C++, `SystemEventSubscription sub = {0};` in C, or
|
||||
* static/global storage) - system_event_subscribe() reads `internal.unsubscribe_in_progress`
|
||||
* before it writes it, to detect reuse of a node still being torn down by a concurrent
|
||||
* system_event_unsubscribe() call; on indeterminate (non-zeroed) storage that read is undefined
|
||||
* behavior. Not required again for a later system_event_subscribe() reusing the same node after
|
||||
* system_event_unsubscribe() - the fields it depends on are fully owned/maintained by this API
|
||||
* from the first successful registration onward.
|
||||
*/
|
||||
struct SystemEventSubscription {
|
||||
/** `event.type` is the event type to subscribe to; set by the caller before
|
||||
* system_event_subscribe(). The rest of `event` (timestamp/data/data_len) is populated by
|
||||
* each matching system_event_emit() - see the @warning above. */
|
||||
struct SystemEvent event;
|
||||
|
||||
/** Implementation-only bookkeeping; do not read or write directly. */
|
||||
struct {
|
||||
/** Own wakeup signal, not the subscribing task's shared default notification value - a
|
||||
* task with more than one poll subscription would otherwise have events for one
|
||||
* subscription wake (and consume the notification meant for) system_event_await()
|
||||
* calls on another. */
|
||||
SemaphoreHandle_t semaphore;
|
||||
|
||||
uint32_t sequence;
|
||||
uint32_t consumed_sequence;
|
||||
|
||||
/** Number of tasks currently blocked in system_event_await() on `semaphore` -
|
||||
* system_event_unsubscribe() waits for this to reach 0 before deleting it, since
|
||||
* FreeRTOS requires no task be blocked on a semaphore when it's deleted. */
|
||||
int waiter_count;
|
||||
/** Set by system_event_unsubscribe() before it gives `semaphore` and waits, so a task
|
||||
* already blocked in system_event_await() bails out (ERROR_INVALID_STATE) instead of
|
||||
* waiting out its full timeout. Reset once system_event_unsubscribe() finishes
|
||||
* draining old awaiters (see unsubscribe_in_progress) - not simply "on the next
|
||||
* system_event_subscribe()", so a fresh registration can never observe a stale `true`
|
||||
* left over from an unsubscribe that hasn't returned yet. */
|
||||
bool cancelled;
|
||||
/** True from the moment system_event_unsubscribe() unlinks `sub` until it has finished
|
||||
* draining old awaiters and deleted the old semaphore. system_event_subscribe() spins
|
||||
* until this clears before reusing `sub` - otherwise a new registration could reset
|
||||
* waiter_count/cancelled (both shared with the old registration, there being only one
|
||||
* `sub`) out from under the old system_event_unsubscribe() call still relying on them,
|
||||
* or hand out a new semaphore for that same call to then promptly delete instead of the
|
||||
* old one, while an old awaiter is still blocked on the real old semaphore. */
|
||||
bool unsubscribe_in_progress;
|
||||
|
||||
struct SystemEventSubscription* next;
|
||||
} internal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a poll subscription for events of @a sub->type.
|
||||
* @warning Does not work in ISR context.
|
||||
* @warning On its very first call for a given @a sub, @a sub must have been zero-initialized -
|
||||
* see SystemEventSubscription's @warning.
|
||||
* @warning If @a sub was just passed to system_event_unsubscribe() (e.g. reusing a node for a
|
||||
* new registration) and that call hasn't returned yet on another task, this call blocks
|
||||
* (briefly - not for the full duration of anyone's timeout) until it does, before registering -
|
||||
* see SystemEventSubscription::internal.unsubscribe_in_progress.
|
||||
* @param[in,out] sub subscription to register; caller sets @a sub->type beforehand, owns the
|
||||
* storage, and must keep it alive (and stationary) until unsubscribed
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_OUT_OF_MEMORY failed to allocate the subscription's wakeup semaphore; @a sub
|
||||
* was not registered
|
||||
* @retval ERROR_INVALID_STATE @a sub is already registered
|
||||
*/
|
||||
error_t system_event_subscribe(struct SystemEventSubscription* sub);
|
||||
|
||||
/**
|
||||
* Remove a previously registered poll subscription.
|
||||
* @warning Does not work in ISR context.
|
||||
* @warning Blocks (briefly - not for the full duration of anyone's timeout) until any task
|
||||
* currently blocked in system_event_await() on @a sub has woken up and left, so it's safe to
|
||||
* delete the subscription's semaphore before this call returns. A blocked awaiter is woken
|
||||
* (with ERROR_INVALID_STATE) as part of this call rather than left to time out on its own.
|
||||
* A concurrent system_event_subscribe() reusing the same @a sub waits out this same window
|
||||
* (see system_event_subscribe()'s @warning) rather than racing it.
|
||||
* @param[in] sub subscription to remove, as passed to system_event_subscribe()
|
||||
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
|
||||
*/
|
||||
error_t system_event_unsubscribe(struct SystemEventSubscription* sub);
|
||||
|
||||
/**
|
||||
* Blocks the calling task until a new event arrives for @a sub, or timeout elapses.
|
||||
* @warning Poll subscriptions coalesce to the latest event, they are not a queue: if
|
||||
* system_event_emit() is called more than once for @a sub->event.type between two
|
||||
* system_event_await() calls, only the most recent event's data/timestamp is visible via
|
||||
* system_event_get_data()/system_event_get_timestamp() afterward - intermediate events are
|
||||
* silently overwritten, never delivered. Use system_event_callback_add() instead if every
|
||||
* individual event matters.
|
||||
* @warning Cannot be called concurrently from different tasks. Each tasks must have its own subscription.
|
||||
* @param[in,out] sub subscription to wait on, as passed to system_event_subscribe()
|
||||
* @param[in] timeout max ticks to wait
|
||||
* @retval ERROR_NONE an event arrived
|
||||
* @retval ERROR_TIMEOUT @a timeout elapsed first
|
||||
* @retval ERROR_INVALID_STATE another task called system_event_unsubscribe() on @a sub while
|
||||
* this call was blocked
|
||||
*/
|
||||
error_t system_event_await(struct SystemEventSubscription* sub, TickType_t timeout);
|
||||
|
||||
/**
|
||||
* Copies @a sub's current event payload (the data from the most recent system_event_emit()
|
||||
* that reached it) into @a data.
|
||||
* @param[in] sub subscription to read the payload from, as passed to system_event_subscribe()
|
||||
* @param[out] data buffer to copy the payload into
|
||||
* @param[in] data_len size of @a data
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_BUFFER_OVERFLOW @a data_len is smaller than the stored payload - @a data is
|
||||
* left untouched
|
||||
*/
|
||||
error_t system_event_get_data(struct SystemEventSubscription* sub, uint8_t* data, size_t data_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user