From b85a50f9124bcaf4448823ef70af38deb526584f Mon Sep 17 00:00:00 2001 From: SG Date: Tue, 12 Oct 2021 21:48:34 +1000 Subject: [PATCH] [FL-1792] RPC service (#698) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Lib: added nanopb * Hal rfid: fixed confused arguments * Lib: update makefile, include nanopb * Lib: remove nanopb * Lib: add nanopb as submodule * Assets: remove protobuf * Assets: add protobuf message definitions as submodule * WIP: [FL-1792] Add Protobuf RPC * WIP: RPC add ping * Add Ping * Fix Ping, Add (WIP) storage * Update submodule * ble-glue: add ptotobuf to ble * WIP: Add storage list test * revert applications.mk * Add Storage List command * ble-glue: fix fast updating rx charachteristic * ble serial: split long ble packets * Add Storage Read/Write/Mkdir/Delete * Disable tests * Rename Element -> File * Add md5sum, fix test leak * Regenerate Protobuf * Fix review comments * ble-glue: sync f7 target Co-authored-by: Albert Kharisov Co-authored-by: gornekich Co-authored-by: あく --- .gitmodules | 6 + applications/applications.c | 7 +- applications/applications.mk | 5 + applications/rpc/rpc.c | 438 +++++++ applications/rpc/rpc.h | 16 + applications/rpc/rpc_i.h | 25 + applications/rpc/rpc_status.c | 25 + applications/rpc/rpc_storage.c | 401 +++++++ applications/storage/storage-cli.c | 2 +- applications/tests/rpc/rpc_test.c | 1027 +++++++++++++++++ applications/tests/test_index.c | 2 + assets/Makefile | 8 +- assets/assets.mk | 13 +- assets/compiled/flipper.pb.c | 16 + assets/compiled/flipper.pb.h | 153 +++ assets/compiled/status.pb.c | 15 + assets/compiled/status.pb.h | 60 + assets/compiled/storage.pb.c | 40 + assets/compiled/storage.pb.h | 212 ++++ assets/protobuf | 1 + firmware/targets/f6/ble-glue/gap.c | 13 +- firmware/targets/f6/ble-glue/serial_service.c | 34 +- firmware/targets/f6/ble-glue/serial_service.h | 5 + firmware/targets/f7/ble-glue/gap.c | 13 +- firmware/targets/f7/ble-glue/serial_service.c | 34 +- firmware/targets/f7/ble-glue/serial_service.h | 5 + firmware/targets/f7/furi-hal/furi-hal-rfid.c | 2 +- lib/lib.mk | 4 + lib/nanopb | 1 + 29 files changed, 2565 insertions(+), 18 deletions(-) create mode 100644 applications/rpc/rpc.c create mode 100644 applications/rpc/rpc.h create mode 100644 applications/rpc/rpc_i.h create mode 100644 applications/rpc/rpc_status.c create mode 100644 applications/rpc/rpc_storage.c create mode 100644 applications/tests/rpc/rpc_test.c create mode 100644 assets/compiled/flipper.pb.c create mode 100644 assets/compiled/flipper.pb.h create mode 100644 assets/compiled/status.pb.c create mode 100644 assets/compiled/status.pb.h create mode 100644 assets/compiled/storage.pb.c create mode 100644 assets/compiled/storage.pb.h create mode 160000 assets/protobuf create mode 160000 lib/nanopb diff --git a/.gitmodules b/.gitmodules index 5004f415..27f32c67 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,12 @@ [submodule "lib/littlefs"] path = lib/littlefs url = https://github.com/littlefs-project/littlefs.git +[submodule "lib/nanopb"] + path = lib/nanopb + url = https://github.com/nanopb/nanopb.git +[submodule "assets/protobuf"] + path = assets/protobuf + url = https://github.com/flipperdevices/flipperzero-protobuf.git [submodule "lib/libusb_stm32"] path = lib/libusb_stm32 url = https://github.com/flipperdevices/libusb_stm32.git diff --git a/applications/applications.c b/applications/applications.c index 4e19ef87..1e1469c4 100644 --- a/applications/applications.c +++ b/applications/applications.c @@ -2,6 +2,7 @@ #include // Services +extern int32_t rpc_srv(void* p); extern int32_t bt_srv(void* p); extern int32_t cli_srv(void* p); extern int32_t dialogs_srv(void* p); @@ -59,6 +60,10 @@ extern int32_t power_settings_app(void* p); const FlipperApplication FLIPPER_SERVICES[] = { /* Services */ +#ifdef SRV_RPC + {.app = rpc_srv, .name = "RPC", .stack_size = 1024 * 4, .icon = NULL}, +#endif + #ifdef SRV_BT {.app = bt_srv, .name = "BT", .stack_size = 1024, .icon = NULL}, #endif @@ -216,7 +221,7 @@ const FlipperApplication FLIPPER_DEBUG_APPS[] = { #endif #ifdef APP_UNIT_TESTS - {.app = flipper_test_app, .name = "Unit Tests", .stack_size = 1024, .icon = &A_Plugins_14}, + {.app = flipper_test_app, .name = "Unit Tests", .stack_size = 1024 * 8, .icon = &A_Plugins_14}, #endif #ifdef APP_IRDA_MONITOR diff --git a/applications/applications.mk b/applications/applications.mk index b5cf772c..5fb8a511 100644 --- a/applications/applications.mk +++ b/applications/applications.mk @@ -19,6 +19,7 @@ SRV_LOADER = 1 SRV_NOTIFICATION = 1 SRV_POWER = 1 SRV_POWER_OBSERVER = 1 +SRV_RPC = 1 SRV_STORAGE = 1 # Apps @@ -206,6 +207,10 @@ SRV_GUI = 1 SRV_CLI = 1 endif +SRV_RPC ?= 0 +ifeq ($(SRV_RPC), 1) +CFLAGS += -DSRV_RPC +endif SRV_LOADER ?= 0 ifeq ($(SRV_LOADER), 1) diff --git a/applications/rpc/rpc.c b/applications/rpc/rpc.c new file mode 100644 index 00000000..493e4f65 --- /dev/null +++ b/applications/rpc/rpc.c @@ -0,0 +1,438 @@ +#include "cmsis_os.h" +#include "cmsis_os2.h" +#include "flipper.pb.h" +#include "furi-hal-delay.h" +#include "furi/check.h" +#include "furi/log.h" +#include "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "portmacro.h" +#include "status.pb.h" +#include "storage.pb.h" +#include +#include +#include +#include +#include +#include "rpc_i.h" + +#define RPC_TAG "RPC" + +#define RPC_EVENT_NEW_DATA (1 << 0) +#define RPC_EVENT_DISCONNECT (1 << 1) +#define RPC_EVENTS_ALL (RPC_EVENT_DISCONNECT | RPC_EVENT_NEW_DATA) + +#define DEBUG_PRINT 0 + +DICT_DEF2(RpcHandlerDict, pb_size_t, M_DEFAULT_OPLIST, RpcHandler, M_POD_OPLIST) + +typedef struct { + RpcSystemAlloc alloc; + RpcSystemFree free; + void* context; +} RpcSystemCallbacks; + +static RpcSystemCallbacks rpc_systems[] = { + { + .alloc = rpc_system_status_alloc, + .free = NULL, + }, + { + .alloc = rpc_system_storage_alloc, + .free = rpc_system_storage_free, + }, +}; + +struct RpcSession { + RpcSendBytesCallback send_bytes_callback; + void* send_bytes_context; + osMutexId_t send_bytes_mutex; + Rpc* rpc; + bool terminate_session; + void** system_contexts; +}; + +struct Rpc { + bool busy; + osMutexId_t busy_mutex; + RpcSession session; + osEventFlagsId_t events; + StreamBufferHandle_t stream; + RpcHandlerDict_t handlers; + PB_Main* decoded_message; +}; + +static bool content_callback(pb_istream_t* stream, const pb_field_t* field, void** arg); + +static size_t rpc_sprint_msg_file( + char* str, + size_t str_size, + const char* prefix, + const PB_Storage_File* msg_file, + size_t msg_files_size) { + size_t cnt = 0; + + for(int i = 0; i < msg_files_size; ++i, ++msg_file) { + cnt += snprintf( + str + cnt, + str_size - cnt, + "%s[%c] size: %5ld", + prefix, + msg_file->type == PB_Storage_File_FileType_DIR ? 'd' : 'f', + msg_file->size); + + if(msg_file->name) { + cnt += snprintf(str + cnt, str_size - cnt, " \'%s\'", msg_file->name); + } + + if(msg_file->data && msg_file->data->size) { + cnt += snprintf( + str + cnt, + str_size - cnt, + " (%d):\'%.*s%s\'", + msg_file->data->size, + MIN(msg_file->data->size, 30), + msg_file->data->bytes, + msg_file->data->size > 30 ? "..." : ""); + } + + cnt += snprintf(str + cnt, str_size - cnt, "\r\n"); + } + + return cnt; +} + +#define ADD_STR(s, c, ...) snprintf(s + c, sizeof(s) - c, ##__VA_ARGS__); + +#define ADD_STR_ELEMENT(s, c, ...) rpc_sprint_msg_file(s + c, sizeof(s) - c, ##__VA_ARGS__); + +void rpc_print_message(const PB_Main* message) { + char str[500]; + size_t cnt = 0; + + cnt += snprintf( + str + cnt, + sizeof(str) - cnt, + "PB_Main: {\r\n\tresult: %d cmd_id: %ld (%s)\r\n", + message->command_status, + message->command_id, + message->has_next ? "has_next" : "last"); + switch(message->which_content) { + default: + /* not implemented yet */ + cnt += ADD_STR(str, cnt, "\tNOT_IMPLEMENTED (%d) {\r\n", message->which_content); + break; + case PB_Main_storage_md5sum_request_tag: { + cnt += ADD_STR(str, cnt, "\tmd5sum_request {\r\n"); + const char* path = message->content.storage_md5sum_request.path; + if(path) { + cnt += ADD_STR(str, cnt, "\t\tpath: %s\r\n", path); + } + break; + } + case PB_Main_storage_md5sum_response_tag: { + cnt += ADD_STR(str, cnt, "\tmd5sum_response {\r\n"); + const char* path = message->content.storage_md5sum_response.md5sum; + if(path) { + cnt += ADD_STR(str, cnt, "\t\tmd5sum: %s\r\n", path); + } + break; + } + case PB_Main_ping_request_tag: + cnt += ADD_STR(str, cnt, "\tping_request {\r\n"); + break; + case PB_Main_ping_response_tag: + cnt += ADD_STR(str, cnt, "\tping_response {\r\n"); + break; + case PB_Main_storage_mkdir_request_tag: + cnt += ADD_STR(str, cnt, "\tmkdir {\r\n"); + break; + case PB_Main_storage_delete_request_tag: { + cnt += ADD_STR(str, cnt, "\tdelete {\r\n"); + const char* path = message->content.storage_delete_request.path; + if(path) { + cnt += ADD_STR(str, cnt, "\t\tpath: %s\r\n", path); + } + break; + } + case PB_Main_empty_tag: + cnt += ADD_STR(str, cnt, "\tempty {\r\n"); + break; + case PB_Main_storage_list_request_tag: { + cnt += ADD_STR(str, cnt, "\tlist_request {\r\n"); + const char* path = message->content.storage_list_request.path; + if(path) { + cnt += ADD_STR(str, cnt, "\t\tpath: %s\r\n", path); + } + break; + } + case PB_Main_storage_read_request_tag: { + cnt += ADD_STR(str, cnt, "\tread_request {\r\n"); + const char* path = message->content.storage_read_request.path; + if(path) { + cnt += ADD_STR(str, cnt, "\t\tpath: %s\r\n", path); + } + break; + } + case PB_Main_storage_write_request_tag: { + cnt += ADD_STR(str, cnt, "\twrite_request {\r\n"); + const char* path = message->content.storage_write_request.path; + if(path) { + cnt += ADD_STR(str, cnt, "\t\tpath: %s\r\n", path); + } + if(message->content.storage_write_request.has_file) { + const PB_Storage_File* msg_file = &message->content.storage_write_request.file; + cnt += ADD_STR_ELEMENT(str, cnt, "\t\t\t", msg_file, 1); + } + break; + } + case PB_Main_storage_read_response_tag: + cnt += ADD_STR(str, cnt, "\tread_response {\r\n"); + if(message->content.storage_read_response.has_file) { + const PB_Storage_File* msg_file = &message->content.storage_read_response.file; + cnt += ADD_STR_ELEMENT(str, cnt, "\t\t\t", msg_file, 1); + } + break; + case PB_Main_storage_list_response_tag: { + const PB_Storage_File* msg_file = message->content.storage_list_response.file; + size_t msg_file_count = message->content.storage_list_response.file_count; + cnt += ADD_STR(str, cnt, "\tlist_response {\r\n"); + cnt += ADD_STR_ELEMENT(str, cnt, "\t\t", msg_file, msg_file_count); + } + } + cnt += ADD_STR(str, cnt, "\t}\r\n}\r\n"); + printf("%s", str); +} + +static Rpc* rpc_alloc(void) { + Rpc* rpc = furi_alloc(sizeof(Rpc)); + rpc->busy_mutex = osMutexNew(NULL); + rpc->busy = false; + rpc->events = osEventFlagsNew(NULL); + rpc->stream = xStreamBufferCreate(256, 1); + + rpc->decoded_message = furi_alloc(sizeof(PB_Main)); + rpc->decoded_message->cb_content.funcs.decode = content_callback; + rpc->decoded_message->cb_content.arg = rpc; + + RpcHandlerDict_init(rpc->handlers); + + return rpc; +} + +RpcSession* rpc_open_session(Rpc* rpc) { + furi_assert(rpc); + bool result = false; + furi_check(osMutexAcquire(rpc->busy_mutex, osWaitForever) == osOK); + if(rpc->busy) { + result = false; + } else { + rpc->busy = true; + result = true; + } + furi_check(osMutexRelease(rpc->busy_mutex) == osOK); + + if(result) { + RpcSession* session = &rpc->session; + session->send_bytes_mutex = osMutexNew(NULL); + session->rpc = rpc; + session->terminate_session = false; + session->system_contexts = furi_alloc(COUNT_OF(rpc_systems) * sizeof(void*)); + for(int i = 0; i < COUNT_OF(rpc_systems); ++i) { + session->system_contexts[i] = rpc_systems[i].alloc(rpc); + } + FURI_LOG_D(RPC_TAG, "Session started\r\n"); + } + + return result ? &rpc->session : NULL; /* support 1 open session for now */ +} + +void rpc_close_session(RpcSession* session) { + furi_assert(session); + furi_assert(session->rpc); + furi_assert(session->rpc->busy); + + osMutexDelete(session->send_bytes_mutex); + rpc_set_send_bytes_callback(session, NULL, NULL); + osEventFlagsSet(session->rpc->events, RPC_EVENT_DISCONNECT); +} + +void rpc_set_send_bytes_callback(RpcSession* session, RpcSendBytesCallback callback, void* context) { + furi_assert(session); + furi_assert(session->rpc); + furi_assert(session->rpc->busy); + + osMutexAcquire(session->send_bytes_mutex, osWaitForever); + session->send_bytes_callback = callback; + session->send_bytes_context = context; + osMutexRelease(session->send_bytes_mutex); +} + +size_t + rpc_feed_bytes(RpcSession* session, uint8_t* encoded_bytes, size_t size, TickType_t timeout) { + furi_assert(session); + Rpc* rpc = session->rpc; + furi_assert(rpc->busy); + + size_t bytes_sent = xStreamBufferSend(rpc->stream, encoded_bytes, size, timeout); + osEventFlagsSet(rpc->events, RPC_EVENT_NEW_DATA); + + return bytes_sent; +} + +bool rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) { + Rpc* rpc = istream->state; + uint32_t flags = 0; + size_t bytes_received = 0; + + while(1) { + bytes_received += + xStreamBufferReceive(rpc->stream, buf + bytes_received, count - bytes_received, 0); + if(count == bytes_received) { + break; + } else { + flags = osEventFlagsWait(rpc->events, RPC_EVENTS_ALL, 0, osWaitForever); + if(flags & RPC_EVENT_DISCONNECT) { + if(xStreamBufferIsEmpty(rpc->stream)) { + rpc->session.terminate_session = true; + break; + } else { + /* Save disconnect flag and continue reading buffer */ + osEventFlagsSet(rpc->events, RPC_EVENT_DISCONNECT); + } + } + } + } + + return (count == bytes_received); +} + +void rpc_encode_and_send(Rpc* rpc, PB_Main* main_message) { + furi_assert(rpc); + furi_assert(main_message); + RpcSession* session = &rpc->session; + pb_ostream_t ostream = PB_OSTREAM_SIZING; + +#if DEBUG_PRINT + FURI_LOG_I(RPC_TAG, "OUTPUT:"); + rpc_print_message(main_message); +#endif + + bool result = pb_encode_ex(&ostream, &PB_Main_msg, main_message, PB_ENCODE_DELIMITED); + furi_check(result && ostream.bytes_written); + + uint8_t* buffer = furi_alloc(ostream.bytes_written); + ostream = pb_ostream_from_buffer(buffer, ostream.bytes_written); + + pb_encode_ex(&ostream, &PB_Main_msg, main_message, PB_ENCODE_DELIMITED); + + { + osMutexAcquire(session->send_bytes_mutex, osWaitForever); + +#if DEBUG_PRINT + printf("\r\nREPONSE DEC(%d): {", ostream.bytes_written); + for(int i = 0; i < ostream.bytes_written; ++i) { + printf("%d, ", buffer[i]); + } + printf("}\r\n"); + + printf("REPONSE HEX(%d): {", ostream.bytes_written); + for(int i = 0; i < ostream.bytes_written; ++i) { + printf("%02X", buffer[i]); + } + printf("}\r\n\r\n"); +#endif // DEBUG_PRINT + + if(session->send_bytes_callback) { + session->send_bytes_callback( + session->send_bytes_context, buffer, ostream.bytes_written); + } + osMutexRelease(session->send_bytes_mutex); + } + free(buffer); +} + +static bool content_callback(pb_istream_t* stream, const pb_field_t* field, void** arg) { + furi_assert(stream); + Rpc* rpc = *arg; + + RpcHandler* handler = RpcHandlerDict_get(rpc->handlers, field->tag); + + if(handler && handler->decode_submessage) { + handler->decode_submessage(stream, field, arg); + } + + return true; +} + +int32_t rpc_srv(void* p) { + Rpc* rpc = rpc_alloc(); + furi_record_create("rpc", rpc); + + while(1) { + pb_istream_t istream = { + .callback = rpc_pb_stream_read, + .state = rpc, + .errmsg = NULL, + .bytes_left = 0x7FFFFFFF, + }; + + if(pb_decode_ex(&istream, &PB_Main_msg, rpc->decoded_message, PB_DECODE_DELIMITED)) { +#if DEBUG_PRINT + FURI_LOG_I(RPC_TAG, "INPUT:"); + rpc_print_message(rpc->decoded_message); +#endif + RpcHandler* handler = + RpcHandlerDict_get(rpc->handlers, rpc->decoded_message->which_content); + + if(handler && handler->message_handler) { + handler->message_handler(rpc->decoded_message, handler->context); + } else if(!handler) { + FURI_LOG_E( + RPC_TAG, + "Unhandled message, tag: %d\r\n", + rpc->decoded_message->which_content); + } + pb_release(&PB_Main_msg, rpc->decoded_message); + } else { + pb_release(&PB_Main_msg, rpc->decoded_message); + RpcSession* session = &rpc->session; + if(session->terminate_session) { + session->terminate_session = false; + osEventFlagsClear(rpc->events, RPC_EVENTS_ALL); + FURI_LOG_D(RPC_TAG, "Session terminated\r\n"); + for(int i = 0; i < COUNT_OF(rpc_systems); ++i) { + if(rpc_systems[i].free) { + rpc_systems[i].free(session->system_contexts[i]); + } + } + free(session->system_contexts); + RpcHandlerDict_clean(rpc->handlers); + rpc->busy = false; + } else { + xStreamBufferReset(rpc->stream); + FURI_LOG_E( + RPC_TAG, "Decode failed, error: \'%.128s\'\r\n", PB_GET_ERROR(&istream)); + } + } + } + return 0; +} + +void rpc_add_handler(Rpc* rpc, pb_size_t message_tag, RpcHandler* handler) { + furi_assert(RpcHandlerDict_get(rpc->handlers, message_tag) == NULL); + + RpcHandlerDict_set_at(rpc->handlers, message_tag, *handler); +} + +void rpc_encode_and_send_empty(Rpc* rpc, uint32_t command_id, PB_CommandStatus status) { + PB_Main message = { + .command_id = command_id, + .command_status = status, + .has_next = false, + .which_content = PB_Main_empty_tag, + }; + rpc_encode_and_send(rpc, &message); + pb_release(&PB_Main_msg, &message); +} diff --git a/applications/rpc/rpc.h b/applications/rpc/rpc.h new file mode 100644 index 00000000..91557606 --- /dev/null +++ b/applications/rpc/rpc.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include +#include "cmsis_os.h" + +typedef struct Rpc Rpc; +typedef struct RpcSession RpcSession; + +typedef void (*RpcSendBytesCallback)(void* context, uint8_t* bytes, size_t bytes_len); + +RpcSession* rpc_open_session(Rpc* rpc); +void rpc_close_session(RpcSession* session); +/* WARN: can't call RPC API within RpcSendBytesCallback */ +void rpc_set_send_bytes_callback(RpcSession* session, RpcSendBytesCallback callback, void* context); +size_t + rpc_feed_bytes(RpcSession* session, uint8_t* encoded_bytes, size_t size, TickType_t timeout); diff --git a/applications/rpc/rpc_i.h b/applications/rpc/rpc_i.h new file mode 100644 index 00000000..e84021a5 --- /dev/null +++ b/applications/rpc/rpc_i.h @@ -0,0 +1,25 @@ +#pragma once +#include "rpc.h" +#include "pb.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "flipper.pb.h" + +typedef void* (*RpcSystemAlloc)(Rpc*); +typedef void (*RpcSystemFree)(void*); +typedef void (*PBMessageHandler)(const PB_Main* msg_request, void* context); + +typedef struct { + bool (*decode_submessage)(pb_istream_t* stream, const pb_field_t* field, void** arg); + PBMessageHandler message_handler; + void* context; +} RpcHandler; + +void rpc_encode_and_send(Rpc* rpc, PB_Main* main_message); +void rpc_encode_and_send_empty(Rpc* rpc, uint32_t command_id, PB_CommandStatus status); +void rpc_add_handler(Rpc* rpc, pb_size_t message_tag, RpcHandler* handler); + +void* rpc_system_status_alloc(Rpc* rpc); +void* rpc_system_storage_alloc(Rpc* rpc); +void rpc_system_storage_free(void* ctx); +void rpc_print_message(const PB_Main* message); diff --git a/applications/rpc/rpc_status.c b/applications/rpc/rpc_status.c new file mode 100644 index 00000000..524675d9 --- /dev/null +++ b/applications/rpc/rpc_status.c @@ -0,0 +1,25 @@ +#include "flipper.pb.h" +#include "rpc_i.h" +#include "status.pb.h" + +void rpc_system_status_ping_process(const PB_Main* msg_request, void* context) { + PB_Main msg_response = PB_Main_init_default; + msg_response.has_next = false; + msg_response.command_status = PB_CommandStatus_OK; + msg_response.command_id = msg_request->command_id; + msg_response.which_content = PB_Main_ping_response_tag; + + rpc_encode_and_send(context, &msg_response); +} + +void* rpc_system_status_alloc(Rpc* rpc) { + RpcHandler rpc_handler = { + .message_handler = rpc_system_status_ping_process, + .decode_submessage = NULL, + .context = rpc, + }; + + rpc_add_handler(rpc, PB_Main_ping_request_tag, &rpc_handler); + + return NULL; +} diff --git a/applications/rpc/rpc_storage.c b/applications/rpc/rpc_storage.c new file mode 100644 index 00000000..22bb2e1c --- /dev/null +++ b/applications/rpc/rpc_storage.c @@ -0,0 +1,401 @@ +#include "flipper.pb.h" +#include "furi/common_defines.h" +#include "furi/memmgr.h" +#include "furi/record.h" +#include "pb_decode.h" +#include "rpc/rpc.h" +#include "rpc_i.h" +#include "storage.pb.h" +#include "storage/filesystem-api-defines.h" +#include "storage/storage.h" +#include +#include + +#define RPC_TAG "RPC_STORAGE" +#define MAX_NAME_LENGTH 255 +#define MAX_DATA_SIZE 512 + +typedef enum { + RpcStorageStateIdle = 0, + RpcStorageStateWriting, +} RpcStorageState; + +typedef struct { + Rpc* rpc; + Storage* api; + File* file; + RpcStorageState state; + uint32_t current_command_id; +} RpcStorageSystem; + +void rpc_print_message(const PB_Main* message); + +static void rpc_system_storage_reset_state(RpcStorageSystem* rpc_storage, bool send_error) { + furi_assert(rpc_storage); + + if(rpc_storage->state != RpcStorageStateIdle) { + if(send_error) { + rpc_encode_and_send_empty( + rpc_storage->rpc, + rpc_storage->current_command_id, + PB_CommandStatus_ERROR_CONTINUOUS_COMMAND_INTERRUPTED); + } + + if(rpc_storage->state == RpcStorageStateWriting) { + storage_file_close(rpc_storage->file); + storage_file_free(rpc_storage->file); + furi_record_close("storage"); + } + + rpc_storage->state = RpcStorageStateIdle; + } +} + +static PB_CommandStatus rpc_system_storage_get_error(FS_Error fs_error) { + PB_CommandStatus pb_error; + switch(fs_error) { + case FSE_OK: + pb_error = PB_CommandStatus_OK; + break; + case FSE_INVALID_NAME: + pb_error = PB_CommandStatus_ERROR_STORAGE_INVALID_NAME; + break; + case FSE_INVALID_PARAMETER: + pb_error = PB_CommandStatus_ERROR_STORAGE_INVALID_PARAMETER; + break; + case FSE_INTERNAL: + pb_error = PB_CommandStatus_ERROR_STORAGE_INTERNAL; + break; + case FSE_ALREADY_OPEN: + pb_error = PB_CommandStatus_ERROR_STORAGE_ALREADY_OPEN; + break; + case FSE_DENIED: + pb_error = PB_CommandStatus_ERROR_STORAGE_DENIED; + break; + case FSE_EXIST: + pb_error = PB_CommandStatus_ERROR_STORAGE_EXIST; + break; + case FSE_NOT_EXIST: + pb_error = PB_CommandStatus_ERROR_STORAGE_NOT_EXIST; + break; + case FSE_NOT_READY: + pb_error = PB_CommandStatus_ERROR_STORAGE_NOT_READY; + break; + case FSE_NOT_IMPLEMENTED: + pb_error = PB_CommandStatus_ERROR_STORAGE_NOT_IMPLEMENTED; + break; + default: + pb_error = PB_CommandStatus_ERROR; + break; + } + + return pb_error; +} + +static PB_CommandStatus rpc_system_storage_get_file_error(File* file) { + return rpc_system_storage_get_error(storage_file_get_error(file)); +} + +static void rpc_system_storage_list_process(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(context); + furi_assert(request->which_content == PB_Main_storage_list_request_tag); + + RpcStorageSystem* rpc_storage = context; + rpc_system_storage_reset_state(rpc_storage, true); + + Storage* fs_api = furi_record_open("storage"); + File* dir = storage_file_alloc(fs_api); + + PB_Main response = { + .command_id = request->command_id, + .has_next = false, + .which_content = PB_Main_storage_list_request_tag, + .command_status = PB_CommandStatus_OK, + }; + PB_Storage_ListResponse* list = &response.content.storage_list_response; + response.which_content = PB_Main_storage_list_response_tag; + + bool finish = false; + int i = 0; + + if(!storage_dir_open(dir, request->content.storage_list_request.path)) { + response.command_status = rpc_system_storage_get_file_error(dir); + response.which_content = PB_Main_empty_tag; + finish = true; + } + + while(!finish) { + FileInfo fileinfo; + char* name = furi_alloc(MAX_NAME_LENGTH + 1); + if(storage_dir_read(dir, &fileinfo, name, MAX_NAME_LENGTH)) { + if(i == COUNT_OF(list->file)) { + list->file_count = i; + response.has_next = true; + rpc_encode_and_send(rpc_storage->rpc, &response); + pb_release(&PB_Main_msg, &response); + i = 0; + } + list->file[i].type = (fileinfo.flags & FSF_DIRECTORY) ? PB_Storage_File_FileType_DIR : + PB_Storage_File_FileType_FILE; + list->file[i].size = fileinfo.size; + list->file[i].data = NULL; + list->file[i].name = name; + ++i; + } else { + list->file_count = i; + finish = true; + free(name); + } + } + + response.has_next = false; + rpc_encode_and_send(rpc_storage->rpc, &response); + pb_release(&PB_Main_msg, &response); + + storage_dir_close(dir); + storage_file_free(dir); + + furi_record_close("storage"); +} + +static void rpc_system_storage_read_process(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(request->which_content == PB_Main_storage_read_request_tag); + + RpcStorageSystem* rpc_storage = context; + rpc_system_storage_reset_state(rpc_storage, true); + + /* use same message memory to send reponse */ + PB_Main* response = furi_alloc(sizeof(PB_Main)); + response->command_id = request->command_id; + response->which_content = PB_Main_storage_read_response_tag; + response->command_status = PB_CommandStatus_OK; + const char* path = request->content.storage_read_request.path; + Storage* fs_api = furi_record_open("storage"); + File* file = storage_file_alloc(fs_api); + bool result = false; + + if(storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { + size_t size_left = storage_file_size(file); + response->content.storage_read_response.has_file = true; + response->content.storage_read_response.file.data = + furi_alloc(PB_BYTES_ARRAY_T_ALLOCSIZE(MIN(size_left, MAX_DATA_SIZE))); + do { + uint8_t* buffer = response->content.storage_read_response.file.data->bytes; + uint16_t* read_size_msg = &response->content.storage_read_response.file.data->size; + + size_t read_size = MIN(size_left, MAX_DATA_SIZE); + *read_size_msg = storage_file_read(file, buffer, read_size); + size_left -= read_size; + result = (*read_size_msg == read_size); + + if(result) { + response->has_next = (size_left > 0); + rpc_encode_and_send(rpc_storage->rpc, response); + // no pb_release(...); + } + } while((size_left != 0) && result); + + if(!result) { + rpc_encode_and_send_empty( + rpc_storage->rpc, request->command_id, rpc_system_storage_get_file_error(file)); + } + } else { + rpc_encode_and_send_empty( + rpc_storage->rpc, request->command_id, rpc_system_storage_get_file_error(file)); + } + + pb_release(&PB_Main_msg, response); + free(response); + storage_file_close(file); + storage_file_free(file); + + furi_record_close("storage"); +} + +static void rpc_system_storage_write_process(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(request->which_content == PB_Main_storage_write_request_tag); + + RpcStorageSystem* rpc_storage = context; + bool result = true; + + if((request->command_id != rpc_storage->current_command_id) && + (rpc_storage->state == RpcStorageStateWriting)) { + rpc_system_storage_reset_state(rpc_storage, true); + } + + if(rpc_storage->state != RpcStorageStateWriting) { + rpc_storage->api = furi_record_open("storage"); + rpc_storage->file = storage_file_alloc(rpc_storage->api); + rpc_storage->current_command_id = request->command_id; + rpc_storage->state = RpcStorageStateWriting; + const char* path = request->content.storage_write_request.path; + result = storage_file_open(rpc_storage->file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS); + } + + File* file = rpc_storage->file; + + if(result) { + uint8_t* buffer = request->content.storage_write_request.file.data->bytes; + size_t buffer_size = request->content.storage_write_request.file.data->size; + + uint16_t written_size = storage_file_write(file, buffer, buffer_size); + result = (written_size == buffer_size); + + if(result && !request->has_next) { + rpc_encode_and_send_empty( + rpc_storage->rpc, rpc_storage->current_command_id, PB_CommandStatus_OK); + rpc_system_storage_reset_state(rpc_storage, false); + } + } + + if(!result) { + rpc_encode_and_send_empty( + rpc_storage->rpc, + rpc_storage->current_command_id, + rpc_system_storage_get_file_error(file)); + rpc_system_storage_reset_state(rpc_storage, false); + } +} + +static void rpc_system_storage_delete_process(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(request->which_content == PB_Main_storage_delete_request_tag); + furi_assert(context); + RpcStorageSystem* rpc_storage = context; + PB_CommandStatus status; + rpc_system_storage_reset_state(rpc_storage, true); + + Storage* fs_api = furi_record_open("storage"); + char* path = request->content.storage_mkdir_request.path; + if(path) { + FS_Error error = storage_common_remove(fs_api, path); + status = rpc_system_storage_get_error(error); + } else { + status = PB_CommandStatus_ERROR_INVALID_PARAMETERS; + } + rpc_encode_and_send_empty(rpc_storage->rpc, request->command_id, status); +} + +static void rpc_system_storage_mkdir_process(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(request->which_content == PB_Main_storage_mkdir_request_tag); + furi_assert(context); + RpcStorageSystem* rpc_storage = context; + PB_CommandStatus status; + rpc_system_storage_reset_state(rpc_storage, true); + + Storage* fs_api = furi_record_open("storage"); + char* path = request->content.storage_mkdir_request.path; + if(path) { + FS_Error error = storage_common_mkdir(fs_api, path); + status = rpc_system_storage_get_error(error); + } else { + status = PB_CommandStatus_ERROR_INVALID_PARAMETERS; + } + rpc_encode_and_send_empty(rpc_storage->rpc, request->command_id, status); +} + +static void rpc_system_storage_md5sum_process(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(request->which_content == PB_Main_storage_md5sum_request_tag); + furi_assert(context); + RpcStorageSystem* rpc_storage = context; + rpc_system_storage_reset_state(rpc_storage, true); + + const char* filename = request->content.storage_md5sum_request.path; + if(!filename) { + rpc_encode_and_send_empty( + rpc_storage->rpc, request->command_id, PB_CommandStatus_ERROR_INVALID_PARAMETERS); + return; + } + + Storage* fs_api = furi_record_open("storage"); + File* file = storage_file_alloc(fs_api); + + if(storage_file_open(file, filename, FSAM_READ, FSOM_OPEN_EXISTING)) { + const uint16_t read_size = 512; + const uint8_t hash_size = 16; + uint8_t* data = malloc(read_size); + uint8_t* hash = malloc(sizeof(uint8_t) * hash_size); + md5_context* md5_ctx = malloc(sizeof(md5_context)); + + md5_starts(md5_ctx); + while(true) { + uint16_t readed_size = storage_file_read(file, data, read_size); + if(readed_size == 0) break; + md5_update(md5_ctx, data, readed_size); + } + md5_finish(md5_ctx, hash); + free(md5_ctx); + + PB_Main response = { + .command_id = request->command_id, + .command_status = PB_CommandStatus_OK, + .which_content = PB_Main_storage_md5sum_response_tag, + .has_next = false, + }; + + char* md5sum = response.content.storage_md5sum_response.md5sum; + size_t md5sum_size = sizeof(response.content.storage_md5sum_response.md5sum); + furi_assert(hash_size <= ((md5sum_size - 1) / 2)); + for(uint8_t i = 0; i < hash_size; i++) { + md5sum += sprintf(md5sum, "%02x", hash[i]); + } + + free(hash); + free(data); + storage_file_close(file); + rpc_encode_and_send(rpc_storage->rpc, &response); + } else { + rpc_encode_and_send_empty( + rpc_storage->rpc, request->command_id, rpc_system_storage_get_file_error(file)); + } + + storage_file_free(file); + + furi_record_close("storage"); +} + +void* rpc_system_storage_alloc(Rpc* rpc) { + furi_assert(rpc); + + RpcStorageSystem* rpc_storage = furi_alloc(sizeof(RpcStorageSystem)); + rpc_storage->api = furi_record_open("storage"); + rpc_storage->rpc = rpc; + rpc_storage->state = RpcStorageStateIdle; + + RpcHandler rpc_handler = { + .message_handler = NULL, + .decode_submessage = NULL, + .context = rpc_storage, + }; + + rpc_handler.message_handler = rpc_system_storage_list_process; + rpc_add_handler(rpc, PB_Main_storage_list_request_tag, &rpc_handler); + + rpc_handler.message_handler = rpc_system_storage_read_process; + rpc_add_handler(rpc, PB_Main_storage_read_request_tag, &rpc_handler); + + rpc_handler.message_handler = rpc_system_storage_write_process; + rpc_add_handler(rpc, PB_Main_storage_write_request_tag, &rpc_handler); + + rpc_handler.message_handler = rpc_system_storage_delete_process; + rpc_add_handler(rpc, PB_Main_storage_delete_request_tag, &rpc_handler); + + rpc_handler.message_handler = rpc_system_storage_mkdir_process; + rpc_add_handler(rpc, PB_Main_storage_mkdir_request_tag, &rpc_handler); + + rpc_handler.message_handler = rpc_system_storage_md5sum_process; + rpc_add_handler(rpc, PB_Main_storage_md5sum_request_tag, &rpc_handler); + + return rpc_storage; +} + +void rpc_system_storage_free(void* ctx) { + RpcStorageSystem* rpc_storage = ctx; + rpc_system_storage_reset_state(rpc_storage, false); + free(rpc_storage); +} diff --git a/applications/storage/storage-cli.c b/applications/storage/storage-cli.c index f5520e0e..14425993 100644 --- a/applications/storage/storage-cli.c +++ b/applications/storage/storage-cli.c @@ -526,4 +526,4 @@ void storage_cli(Cli* cli, string_t args, void* context) { string_clear(path); string_clear(cmd); -} \ No newline at end of file +} diff --git a/applications/tests/rpc/rpc_test.c b/applications/tests/rpc/rpc_test.c new file mode 100644 index 00000000..84fef1de --- /dev/null +++ b/applications/tests/rpc/rpc_test.c @@ -0,0 +1,1027 @@ +#include "flipper.pb.h" +#include "furi-hal-delay.h" +#include "furi/check.h" +#include "furi/record.h" +#include "pb_decode.h" +#include "rpc/rpc_i.h" +#include "storage.pb.h" +#include "storage/storage.h" +#include +#include "../minunit.h" +#include +#include +#include +#include +#include +#include + +LIST_DEF(MsgList, PB_Main, M_POD_OPLIST) +#define M_OPL_MsgList_t() LIST_OPLIST(MsgList) + +/* MinUnit test framework doesn't allow passing context into tests, + * so we have to use global variables + */ +static Rpc* rpc = NULL; +static RpcSession* session = NULL; +static StreamBufferHandle_t output_stream = NULL; +static uint32_t command_id = 0; + +#define TEST_RPC_TAG "TEST_RPC" +#define MAX_RECEIVE_OUTPUT_TIMEOUT 3000 +#define MAX_NAME_LENGTH 255 +#define MAX_DATA_SIZE 512 // have to be exact as in rpc_storage.c +#define TEST_DIR TEST_DIR_NAME "/" +#define TEST_DIR_NAME "/ext/unit_tests_tmp" +#define MD5SUM_SIZE 16 + +#define PING_REQUEST 0 +#define PING_RESPONSE 1 +#define WRITE_REQUEST 0 +#define READ_RESPONSE 1 + +#define DEBUG_PRINT 0 + +#define BYTES(x) (x), sizeof(x) + +static void output_bytes_callback(void* ctx, uint8_t* got_bytes, size_t got_size); +static void clean_directory(Storage* fs_api, const char* clean_dir); +static void + test_rpc_add_empty_to_list(MsgList_t msg_list, PB_CommandStatus status, uint32_t command_id); +static void test_rpc_encode_and_feed(MsgList_t msg_list); +static void test_rpc_encode_and_feed_one(PB_Main* request); +static void test_rpc_compare_messages(PB_Main* result, PB_Main* expected); +static void test_rpc_decode_and_compare(MsgList_t expected_msg_list); +static void test_rpc_free_msg_list(MsgList_t msg_list); + +static void test_rpc_storage_setup(void) { + furi_assert(!rpc); + furi_assert(!session); + furi_assert(!output_stream); + + rpc = furi_record_open("rpc"); + for(int i = 0; !session && (i < 10000); ++i) { + session = rpc_open_session(rpc); + delay(1); + } + furi_assert(session); + + Storage* fs_api = furi_record_open("storage"); + clean_directory(fs_api, TEST_DIR_NAME); + furi_record_close("storage"); + + output_stream = xStreamBufferCreate(1000, 1); + mu_assert(session, "failed to start session"); + rpc_set_send_bytes_callback(session, output_bytes_callback, output_stream); +} + +static void test_rpc_storage_teardown(void) { + Storage* fs_api = furi_record_open("storage"); + clean_directory(fs_api, TEST_DIR_NAME); + furi_record_close("storage"); + + rpc_close_session(session); + furi_record_close("rpc"); + vStreamBufferDelete(output_stream); + ++command_id; + output_stream = NULL; + rpc = NULL; + session = NULL; +} + +static void clean_directory(Storage* fs_api, const char* clean_dir) { + furi_assert(fs_api); + furi_assert(clean_dir); + + File* dir = storage_file_alloc(fs_api); + if(storage_dir_open(dir, clean_dir)) { + FileInfo fileinfo; + char* name = furi_alloc(MAX_NAME_LENGTH + 1); + while(storage_dir_read(dir, &fileinfo, name, MAX_NAME_LENGTH)) { + char* fullname = furi_alloc(strlen(clean_dir) + strlen(name) + 1 + 1); + sprintf(fullname, "%s/%s", clean_dir, name); + if(fileinfo.flags & FSF_DIRECTORY) { + clean_directory(fs_api, fullname); + } + storage_common_remove(fs_api, fullname); + free(fullname); + } + free(name); + } else { + FS_Error error = storage_common_mkdir(fs_api, clean_dir); + furi_assert(error == FSE_OK); + } + + storage_dir_close(dir); + storage_file_free(dir); +} + +static void test_rpc_print_message_list(MsgList_t msg_list) { +#if DEBUG_PRINT + MsgList_reverse(msg_list); + for + M_EACH(msg, msg_list, MsgList_t) { + rpc_print_message(msg); + } + MsgList_reverse(msg_list); +#endif +} + +static PB_CommandStatus test_rpc_storage_get_file_error(File* file) { + FS_Error fs_error = storage_file_get_error(file); + PB_CommandStatus pb_error; + switch(fs_error) { + case FSE_OK: + pb_error = PB_CommandStatus_OK; + break; + case FSE_INVALID_NAME: + pb_error = PB_CommandStatus_ERROR_STORAGE_INVALID_NAME; + break; + case FSE_INVALID_PARAMETER: + pb_error = PB_CommandStatus_ERROR_STORAGE_INVALID_PARAMETER; + break; + case FSE_INTERNAL: + pb_error = PB_CommandStatus_ERROR_STORAGE_INTERNAL; + break; + case FSE_ALREADY_OPEN: + pb_error = PB_CommandStatus_ERROR_STORAGE_ALREADY_OPEN; + break; + case FSE_DENIED: + pb_error = PB_CommandStatus_ERROR_STORAGE_DENIED; + break; + case FSE_EXIST: + pb_error = PB_CommandStatus_ERROR_STORAGE_EXIST; + break; + case FSE_NOT_EXIST: + pb_error = PB_CommandStatus_ERROR_STORAGE_NOT_EXIST; + break; + case FSE_NOT_READY: + pb_error = PB_CommandStatus_ERROR_STORAGE_NOT_READY; + break; + case FSE_NOT_IMPLEMENTED: + pb_error = PB_CommandStatus_ERROR_STORAGE_NOT_IMPLEMENTED; + break; + default: + pb_error = PB_CommandStatus_ERROR; + break; + } + + return pb_error; +} + +static void output_bytes_callback(void* ctx, uint8_t* got_bytes, size_t got_size) { + StreamBufferHandle_t stream_buffer = ctx; + + size_t bytes_sent = xStreamBufferSend(stream_buffer, got_bytes, got_size, osWaitForever); + furi_assert(bytes_sent == got_size); +} + +static void test_rpc_add_ping_to_list(MsgList_t msg_list, bool request, uint32_t command_id) { + PB_Main* response = MsgList_push_new(msg_list); + response->command_id = command_id; + response->command_status = PB_CommandStatus_OK; + response->cb_content.funcs.encode = NULL; + response->has_next = false; + response->which_content = (request == PING_REQUEST) ? PB_Main_ping_request_tag : + PB_Main_ping_response_tag; +} + +static void test_rpc_create_simple_message( + PB_Main* message, + uint16_t tag, + const char* str, + uint32_t command_id) { + furi_assert(message); + furi_assert(str); + + char* str_copy = furi_alloc(strlen(str) + 1); + strcpy(str_copy, str); + message->command_id = command_id; + message->command_status = PB_CommandStatus_OK; + message->cb_content.funcs.encode = NULL; + message->which_content = tag; + message->has_next = false; + switch(tag) { + case PB_Main_storage_list_request_tag: + message->content.storage_list_request.path = str_copy; + break; + case PB_Main_storage_mkdir_request_tag: + message->content.storage_mkdir_request.path = str_copy; + break; + case PB_Main_storage_read_request_tag: + message->content.storage_read_request.path = str_copy; + break; + case PB_Main_storage_delete_request_tag: + message->content.storage_delete_request.path = str_copy; + break; + case PB_Main_storage_md5sum_request_tag: + message->content.storage_md5sum_request.path = str_copy; + break; + case PB_Main_storage_md5sum_response_tag: { + char* md5sum = message->content.storage_md5sum_response.md5sum; + size_t md5sum_size = sizeof(message->content.storage_md5sum_response.md5sum); + furi_assert((strlen(str) + 1) <= md5sum_size); + memcpy(md5sum, str_copy, md5sum_size); + free(str_copy); + break; + } + default: + furi_assert(0); + break; + } +} + +static void test_rpc_add_read_or_write_to_list( + MsgList_t msg_list, + bool write, + const char* path, + const uint8_t* pattern, + size_t pattern_size, + size_t pattern_repeats, + uint32_t command_id) { + furi_assert(pattern_repeats > 0); + + do { + PB_Main* request = MsgList_push_new(msg_list); + PB_Storage_File* msg_file = NULL; + + request->command_id = command_id; + request->command_status = PB_CommandStatus_OK; + + if(write == WRITE_REQUEST) { + size_t path_size = strlen(path) + 1; + request->content.storage_write_request.path = furi_alloc(path_size); + strncpy(request->content.storage_write_request.path, path, path_size); + request->which_content = PB_Main_storage_write_request_tag; + request->content.storage_write_request.has_file = true; + msg_file = &request->content.storage_write_request.file; + } else { + request->which_content = PB_Main_storage_read_response_tag; + request->content.storage_read_response.has_file = true; + msg_file = &request->content.storage_read_response.file; + } + + msg_file->data = furi_alloc(PB_BYTES_ARRAY_T_ALLOCSIZE(pattern_size)); + msg_file->data->size = pattern_size; + + memcpy(msg_file->data->bytes, pattern, pattern_size); + + --pattern_repeats; + request->has_next = (pattern_repeats > 0); + } while(pattern_repeats); +} + +static void test_rpc_encode_and_feed_one(PB_Main* request) { + furi_assert(request); + + pb_ostream_t ostream = PB_OSTREAM_SIZING; + + bool result = pb_encode_ex(&ostream, &PB_Main_msg, request, PB_ENCODE_DELIMITED); + furi_check(result && ostream.bytes_written); + + uint8_t* buffer = furi_alloc(ostream.bytes_written); + ostream = pb_ostream_from_buffer(buffer, ostream.bytes_written); + + pb_encode_ex(&ostream, &PB_Main_msg, request, PB_ENCODE_DELIMITED); + + size_t bytes_left = ostream.bytes_written; + uint8_t* buffer_ptr = buffer; + do { + size_t bytes_sent = rpc_feed_bytes(session, buffer_ptr, bytes_left, 1000); + mu_check(bytes_sent > 0); + + bytes_left -= bytes_sent; + buffer_ptr += bytes_sent; + } while(bytes_left); + + free(buffer); + pb_release(&PB_Main_msg, request); +} + +static void test_rpc_encode_and_feed(MsgList_t msg_list) { + MsgList_reverse(msg_list); + for + M_EACH(request, msg_list, MsgList_t) { + test_rpc_encode_and_feed_one(request); + } + MsgList_reverse(msg_list); +} + +static void + test_rpc_compare_file(PB_Storage_File* result_msg_file, PB_Storage_File* expected_msg_file) { + mu_check(!result_msg_file->name == !expected_msg_file->name); + if(result_msg_file->name) { + mu_check(!strcmp(result_msg_file->name, expected_msg_file->name)); + } + mu_check(result_msg_file->size == expected_msg_file->size); + mu_check(result_msg_file->type == expected_msg_file->type); + + mu_check(!result_msg_file->data == !expected_msg_file->data); + mu_check(result_msg_file->data->size == expected_msg_file->data->size); + for(int i = 0; i < result_msg_file->data->size; ++i) { + mu_check(result_msg_file->data->bytes[i] == expected_msg_file->data->bytes[i]); + } +} + +static void test_rpc_compare_messages(PB_Main* result, PB_Main* expected) { + mu_check(result->command_id == expected->command_id); + mu_check(result->command_status == expected->command_status); + mu_check(result->has_next == expected->has_next); + mu_check(result->which_content == expected->which_content); + if(result->command_status != PB_CommandStatus_OK) { + mu_check(result->which_content == PB_Main_empty_tag); + } + + switch(result->which_content) { + case PB_Main_empty_tag: + case PB_Main_ping_response_tag: + /* nothing to check */ + break; + case PB_Main_ping_request_tag: + case PB_Main_storage_list_request_tag: + case PB_Main_storage_read_request_tag: + case PB_Main_storage_write_request_tag: + case PB_Main_storage_delete_request_tag: + case PB_Main_storage_mkdir_request_tag: + case PB_Main_storage_md5sum_request_tag: + /* rpc doesn't send it */ + mu_check(0); + break; + case PB_Main_storage_read_response_tag: { + bool result_has_msg_file = result->content.storage_read_response.has_file; + bool expected_has_msg_file = expected->content.storage_read_response.has_file; + mu_check(result_has_msg_file == expected_has_msg_file); + + if(result_has_msg_file) { + PB_Storage_File* result_msg_file = &result->content.storage_read_response.file; + PB_Storage_File* expected_msg_file = &expected->content.storage_read_response.file; + test_rpc_compare_file(result_msg_file, expected_msg_file); + } else { + mu_check(0); + } + } break; + case PB_Main_storage_list_response_tag: { + size_t expected_msg_files = expected->content.storage_list_response.file_count; + size_t result_msg_files = result->content.storage_list_response.file_count; + mu_check(result_msg_files == expected_msg_files); + for(int i = 0; i < expected_msg_files; ++i) { + PB_Storage_File* result_msg_file = &result->content.storage_list_response.file[i]; + PB_Storage_File* expected_msg_file = &expected->content.storage_list_response.file[i]; + test_rpc_compare_file(result_msg_file, expected_msg_file); + } + break; + } + case PB_Main_storage_md5sum_response_tag: { + char* result_md5sum = result->content.storage_md5sum_response.md5sum; + char* expected_md5sum = expected->content.storage_md5sum_response.md5sum; + mu_check(!strcmp(result_md5sum, expected_md5sum)); + break; + } + default: + furi_assert(0); + break; + } +} + +static bool test_rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) { + StreamBufferHandle_t stream_buffer = istream->state; + size_t bytes_received = 0; + + bytes_received = xStreamBufferReceive(stream_buffer, buf, count, MAX_RECEIVE_OUTPUT_TIMEOUT); + return (count == bytes_received); +} + +static void test_rpc_storage_list_create_expected_list( + MsgList_t msg_list, + const char* path, + uint32_t command_id) { + Storage* fs_api = furi_record_open("storage"); + File* dir = storage_file_alloc(fs_api); + + PB_Main response = { + .command_id = command_id, + .has_next = false, + .which_content = PB_Main_storage_list_request_tag, + /* other fields (e.g. msg_files ptrs) explicitly initialized by 0 */ + }; + PB_Storage_ListResponse* list = &response.content.storage_list_response; + response.which_content = PB_Main_storage_list_response_tag; + + bool finish = false; + int i = 0; + + if(storage_dir_open(dir, path)) { + response.command_status = PB_CommandStatus_OK; + } else { + response.command_status = test_rpc_storage_get_file_error(dir); + response.which_content = PB_Main_empty_tag; + finish = true; + } + + while(!finish) { + FileInfo fileinfo; + char* name = furi_alloc(MAX_NAME_LENGTH + 1); + if(storage_dir_read(dir, &fileinfo, name, MAX_NAME_LENGTH)) { + if(i == COUNT_OF(list->file)) { + list->file_count = i; + response.has_next = true; + MsgList_push_back(msg_list, response); + i = 0; + } + list->file[i].type = (fileinfo.flags & FSF_DIRECTORY) ? PB_Storage_File_FileType_DIR : + PB_Storage_File_FileType_FILE; + list->file[i].size = fileinfo.size; + list->file[i].data = NULL; + /* memory free inside rpc_encode_and_send() -> pb_release() */ + list->file[i].name = name; + ++i; + } else { + finish = true; + free(name); + } + } + + list->file_count = i; + response.has_next = false; + MsgList_push_back(msg_list, response); + + storage_dir_close(dir); + storage_file_free(dir); + + furi_record_close("storage"); +} + +static void test_rpc_decode_and_compare(MsgList_t expected_msg_list) { + furi_assert(!MsgList_empty_p(expected_msg_list)); + + pb_istream_t istream = { + .callback = test_rpc_pb_stream_read, + .state = output_stream, + .errmsg = NULL, + .bytes_left = 0x7FFFFFFF, + }; + /* other fields explicitly initialized by 0 */ + PB_Main result = {.cb_content.funcs.decode = NULL}; + + /* mlib adds msg_files into start of list, so reverse it */ + MsgList_reverse(expected_msg_list); + for + M_EACH(expected_msg, expected_msg_list, MsgList_t) { + if(!pb_decode_ex(&istream, &PB_Main_msg, &result, PB_DECODE_DELIMITED)) { + mu_assert( + 0, + "not all expected messages decoded (maybe increase MAX_RECEIVE_OUTPUT_TIMEOUT)"); + break; + } + + test_rpc_compare_messages(&result, expected_msg); + pb_release(&PB_Main_msg, &result); + } + MsgList_reverse(expected_msg_list); +} + +static void test_rpc_free_msg_list(MsgList_t msg_list) { + for + M_EACH(it, msg_list, MsgList_t) { + pb_release(&PB_Main_msg, it); + } + MsgList_clear(msg_list); +} + +static void test_rpc_storage_list_run(const char* path, uint32_t command_id) { + PB_Main request; + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + test_rpc_create_simple_message(&request, PB_Main_storage_list_request_tag, path, command_id); + test_rpc_storage_list_create_expected_list(expected_msg_list, path, command_id); + test_rpc_encode_and_feed_one(&request); + test_rpc_decode_and_compare(expected_msg_list); + + pb_release(&PB_Main_msg, &request); + test_rpc_free_msg_list(expected_msg_list); +} + +MU_TEST(test_storage_list) { + test_rpc_storage_list_run("/ext/nfc", ++command_id); + + test_rpc_storage_list_run("/int", ++command_id); + test_rpc_storage_list_run("/ext", ++command_id); + test_rpc_storage_list_run("/ext/irda", ++command_id); + test_rpc_storage_list_run("/ext/ibutton", ++command_id); + test_rpc_storage_list_run("/ext/lfrfid", ++command_id); + test_rpc_storage_list_run("error_path", ++command_id); +} + +static void + test_rpc_add_empty_to_list(MsgList_t msg_list, PB_CommandStatus status, uint32_t command_id) { + PB_Main* response = MsgList_push_new(msg_list); + response->command_id = command_id; + response->command_status = status; + response->cb_content.funcs.encode = NULL; + response->has_next = false; + response->which_content = PB_Main_empty_tag; +} + +static void test_rpc_add_read_to_list_by_reading_real_file( + MsgList_t msg_list, + const char* path, + uint32_t command_id) { + furi_assert(MsgList_empty_p(msg_list)); + Storage* fs_api = furi_record_open("storage"); + File* file = storage_file_alloc(fs_api); + + bool result = false; + + if(storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { + size_t size_left = storage_file_size(file); + + do { + PB_Main* response = MsgList_push_new(msg_list); + response->command_id = command_id; + response->command_status = PB_CommandStatus_OK; + response->has_next = false; + response->which_content = PB_Main_storage_read_response_tag; + response->content.storage_read_response.has_file = true; + + response->content.storage_read_response.file.data = + furi_alloc(PB_BYTES_ARRAY_T_ALLOCSIZE(MIN(size_left, MAX_DATA_SIZE))); + uint8_t* buffer = response->content.storage_read_response.file.data->bytes; + uint16_t* read_size_msg = &response->content.storage_read_response.file.data->size; + size_t read_size = MIN(size_left, MAX_DATA_SIZE); + *read_size_msg = storage_file_read(file, buffer, read_size); + size_left -= read_size; + result = (*read_size_msg == read_size); + + if(result) { + response->has_next = (size_left > 0); + } + } while((size_left != 0) && result); + + if(!result) { + test_rpc_add_empty_to_list( + msg_list, test_rpc_storage_get_file_error(file), command_id); + } + } else { + test_rpc_add_empty_to_list(msg_list, test_rpc_storage_get_file_error(file), command_id); + } + + storage_file_close(file); + storage_file_free(file); + + furi_record_close("storage"); +} + +static void test_storage_read_run(const char* path, uint32_t command_id) { + PB_Main request; + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + test_rpc_add_read_to_list_by_reading_real_file(expected_msg_list, path, command_id); + test_rpc_create_simple_message(&request, PB_Main_storage_read_request_tag, path, command_id); + test_rpc_encode_and_feed_one(&request); + test_rpc_decode_and_compare(expected_msg_list); + + pb_release(&PB_Main_msg, &request); + test_rpc_free_msg_list(expected_msg_list); +} + +static void test_create_dir(const char* path) { + Storage* fs_api = furi_record_open("storage"); + FS_Error error = storage_common_mkdir(fs_api, path); + furi_assert((error == FSE_OK) || (error == FSE_EXIST)); + furi_record_close("storage"); +} + +static void test_create_file(const char* path, size_t size) { + Storage* fs_api = furi_record_open("storage"); + File* file = storage_file_alloc(fs_api); + + if(storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { + uint8_t buf[128] = {0}; + for(int i = 0; i < sizeof(buf); ++i) { + buf[i] = '0' + (i % 10); + } + while(size) { + size_t written = storage_file_write(file, buf, MIN(size, sizeof(buf))); + furi_assert(written); + size -= written; + } + } + + storage_file_close(file); + storage_file_free(file); + + furi_record_close("storage"); +} + +MU_TEST(test_storage_read) { + test_create_file(TEST_DIR "empty.txt", 0); + test_create_file(TEST_DIR "file1.txt", 1); + test_create_file(TEST_DIR "file2.txt", MAX_DATA_SIZE); + test_create_file(TEST_DIR "file3.txt", MAX_DATA_SIZE + 1); + test_create_file(TEST_DIR "file4.txt", (MAX_DATA_SIZE * 2) + 1); + + test_storage_read_run(TEST_DIR "empty.txt", ++command_id); + test_storage_read_run(TEST_DIR "file1.txt", ++command_id); + test_storage_read_run(TEST_DIR "file2.txt", ++command_id); + test_storage_read_run(TEST_DIR "file3.txt", ++command_id); + test_storage_read_run(TEST_DIR "file4.txt", ++command_id); +} + +static void test_storage_write_run( + const char* path, + size_t write_size, + size_t write_count, + uint32_t command_id, + PB_CommandStatus status) { + MsgList_t input_msg_list; + MsgList_init(input_msg_list); + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + uint8_t* buf = furi_alloc(write_size); + for(int i = 0; i < write_size; ++i) { + buf[i] = '0' + (i % 10); + } + + test_rpc_add_read_or_write_to_list( + input_msg_list, WRITE_REQUEST, path, buf, write_size, write_count, command_id); + test_rpc_add_empty_to_list(expected_msg_list, status, command_id); + test_rpc_encode_and_feed(input_msg_list); + test_rpc_decode_and_compare(expected_msg_list); + + test_rpc_free_msg_list(input_msg_list); + test_rpc_free_msg_list(expected_msg_list); + + free(buf); +} + +static void test_storage_write_read_run( + const char* path, + const uint8_t* pattern, + size_t pattern_size, + size_t pattern_repeats, + uint32_t* command_id) { + MsgList_t input_msg_list; + MsgList_init(input_msg_list); + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + test_rpc_add_read_or_write_to_list( + input_msg_list, WRITE_REQUEST, path, pattern, pattern_size, pattern_repeats, ++*command_id); + test_rpc_add_empty_to_list(expected_msg_list, PB_CommandStatus_OK, *command_id); + + test_rpc_create_simple_message( + MsgList_push_raw(input_msg_list), PB_Main_storage_read_request_tag, path, ++*command_id); + test_rpc_add_read_or_write_to_list( + expected_msg_list, + READ_RESPONSE, + path, + pattern, + pattern_size, + pattern_repeats, + *command_id); + + test_rpc_print_message_list(input_msg_list); + test_rpc_print_message_list(expected_msg_list); + + test_rpc_encode_and_feed(input_msg_list); + test_rpc_decode_and_compare(expected_msg_list); + + test_rpc_free_msg_list(input_msg_list); + test_rpc_free_msg_list(expected_msg_list); +} + +MU_TEST(test_storage_write_read) { + uint8_t pattern1[] = "abcdefgh"; + test_storage_write_read_run(TEST_DIR "test1.txt", pattern1, sizeof(pattern1), 1, &command_id); + test_storage_write_read_run(TEST_DIR "test2.txt", pattern1, 1, 1, &command_id); + test_storage_write_read_run(TEST_DIR "test3.txt", pattern1, 0, 1, &command_id); +} + +MU_TEST(test_storage_write) { + test_storage_write_run( + TEST_DIR "afaefo/aefaef/aef/aef/test1.txt", + 1, + 1, + ++command_id, + PB_CommandStatus_ERROR_STORAGE_NOT_EXIST); + test_storage_write_run(TEST_DIR "test1.txt", 100, 1, ++command_id, PB_CommandStatus_OK); + test_storage_write_run(TEST_DIR "test2.txt", 100, 3, ++command_id, PB_CommandStatus_OK); + test_storage_write_run(TEST_DIR "test1.txt", 100, 3, ++command_id, PB_CommandStatus_OK); + test_storage_write_run(TEST_DIR "test2.txt", 100, 3, ++command_id, PB_CommandStatus_OK); + test_storage_write_run( + TEST_DIR "afaefo/aefaef/aef/aef/test1.txt", + 1, + 1, + ++command_id, + PB_CommandStatus_ERROR_STORAGE_NOT_EXIST); + test_storage_write_run(TEST_DIR "test2.txt", 1, 50, ++command_id, PB_CommandStatus_OK); + test_storage_write_run(TEST_DIR "test2.txt", 4096, 1, ++command_id, PB_CommandStatus_OK); +} + +MU_TEST(test_storage_interrupt_continuous_same_system) { + MsgList_t input_msg_list; + MsgList_init(input_msg_list); + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + uint8_t pattern[16] = {0}; + + test_rpc_add_read_or_write_to_list( + input_msg_list, + WRITE_REQUEST, + TEST_DIR "test1.txt", + pattern, + sizeof(pattern), + 3, + command_id); + + /* replace last packet (has_next == false) with another command */ + PB_Main message_to_remove; + MsgList_pop_back(&message_to_remove, input_msg_list); + pb_release(&PB_Main_msg, &message_to_remove); + test_rpc_create_simple_message( + MsgList_push_new(input_msg_list), + PB_Main_storage_mkdir_request_tag, + TEST_DIR "dir1", + command_id + 1); + test_rpc_add_read_or_write_to_list( + input_msg_list, + WRITE_REQUEST, + TEST_DIR "test2.txt", + pattern, + sizeof(pattern), + 3, + command_id); + + test_rpc_add_empty_to_list( + expected_msg_list, PB_CommandStatus_ERROR_CONTINUOUS_COMMAND_INTERRUPTED, command_id); + test_rpc_add_empty_to_list(expected_msg_list, PB_CommandStatus_OK, command_id + 1); + + test_rpc_encode_and_feed(input_msg_list); + test_rpc_decode_and_compare(expected_msg_list); + + test_rpc_free_msg_list(input_msg_list); + test_rpc_free_msg_list(expected_msg_list); +} + +MU_TEST(test_storage_interrupt_continuous_another_system) { + MsgList_t input_msg_list; + MsgList_init(input_msg_list); + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + uint8_t pattern[16] = {0}; + + test_rpc_add_read_or_write_to_list( + input_msg_list, + WRITE_REQUEST, + TEST_DIR "test1.txt", + pattern, + sizeof(pattern), + 3, + command_id); + + PB_Main message = { + .command_id = command_id + 1, + .command_status = PB_CommandStatus_OK, + .cb_content.funcs.encode = NULL, + .has_next = false, + .which_content = PB_Main_ping_request_tag, + }; + + MsgList_it_t it; + MsgList_it(it, input_msg_list); + MsgList_next(it); + MsgList_insert(input_msg_list, it, message); + + test_rpc_add_read_or_write_to_list( + input_msg_list, + WRITE_REQUEST, + TEST_DIR "test2.txt", + pattern, + sizeof(pattern), + 3, + command_id + 2); + + test_rpc_add_ping_to_list(expected_msg_list, PING_RESPONSE, command_id + 1); + test_rpc_add_empty_to_list(expected_msg_list, PB_CommandStatus_OK, command_id); + test_rpc_add_empty_to_list(expected_msg_list, PB_CommandStatus_OK, command_id + 2); + + test_rpc_encode_and_feed(input_msg_list); + test_rpc_decode_and_compare(expected_msg_list); + + test_rpc_free_msg_list(input_msg_list); + test_rpc_free_msg_list(expected_msg_list); +} + +static void test_storage_delete_run(const char* path, size_t command_id, PB_CommandStatus status) { + PB_Main request; + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + test_rpc_create_simple_message(&request, PB_Main_storage_delete_request_tag, path, command_id); + test_rpc_add_empty_to_list(expected_msg_list, status, command_id); + + test_rpc_encode_and_feed_one(&request); + test_rpc_decode_and_compare(expected_msg_list); + + pb_release(&PB_Main_msg, &request); + test_rpc_free_msg_list(expected_msg_list); +} + +MU_TEST(test_storage_delete) { + test_create_file(TEST_DIR "empty.txt", 0); + test_storage_delete_run(TEST_DIR "empty.txt", ++command_id, PB_CommandStatus_OK); + test_storage_delete_run( + TEST_DIR "empty.txt", ++command_id, PB_CommandStatus_ERROR_STORAGE_NOT_EXIST); + + test_create_dir(TEST_DIR "dir1"); + test_storage_delete_run(TEST_DIR "dir1", ++command_id, PB_CommandStatus_OK); + test_storage_delete_run( + TEST_DIR "dir1", ++command_id, PB_CommandStatus_ERROR_STORAGE_NOT_EXIST); +} + +static void test_storage_mkdir_run(const char* path, size_t command_id, PB_CommandStatus status) { + PB_Main request; + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + test_rpc_create_simple_message(&request, PB_Main_storage_mkdir_request_tag, path, command_id); + test_rpc_add_empty_to_list(expected_msg_list, status, command_id); + + test_rpc_encode_and_feed_one(&request); + test_rpc_decode_and_compare(expected_msg_list); + + pb_release(&PB_Main_msg, &request); + test_rpc_free_msg_list(expected_msg_list); +} + +MU_TEST(test_storage_mkdir) { + test_storage_mkdir_run(TEST_DIR "dir1", ++command_id, PB_CommandStatus_OK); + test_storage_mkdir_run(TEST_DIR "dir1", ++command_id, PB_CommandStatus_ERROR_STORAGE_EXIST); + test_create_dir(TEST_DIR "dir2"); + test_storage_mkdir_run(TEST_DIR "dir2", ++command_id, PB_CommandStatus_ERROR_STORAGE_EXIST); + + Storage* fs_api = furi_record_open("storage"); + FS_Error error = storage_common_remove(fs_api, TEST_DIR "dir1"); + furi_assert(error == FSE_OK); + furi_record_close("storage"); + + test_storage_mkdir_run(TEST_DIR "dir1", ++command_id, PB_CommandStatus_OK); +} + +static void test_storage_calculate_md5sum(const char* path, char* md5sum) { + Storage* api = furi_record_open("storage"); + File* file = storage_file_alloc(api); + + if(storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { + const uint16_t once_read_size = 512; + const uint8_t hash_size = MD5SUM_SIZE; + uint8_t* data = malloc(once_read_size); + uint8_t* hash = malloc(sizeof(uint8_t) * hash_size); + md5_context* md5_ctx = malloc(sizeof(md5_context)); + + md5_starts(md5_ctx); + while(true) { + uint16_t read_size = storage_file_read(file, data, once_read_size); + if(read_size == 0) break; + md5_update(md5_ctx, data, read_size); + } + md5_finish(md5_ctx, hash); + free(md5_ctx); + + for(uint8_t i = 0; i < hash_size; i++) { + md5sum += sprintf(md5sum, "%02x", hash[i]); + } + + free(hash); + free(data); + } else { + furi_assert(0); + } + + storage_file_close(file); + storage_file_free(file); + + furi_record_close("storage"); +} + +static void test_storage_md5sum_run( + const char* path, + uint32_t command_id, + const char* md5sum, + PB_CommandStatus status) { + PB_Main request; + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + test_rpc_create_simple_message(&request, PB_Main_storage_md5sum_request_tag, path, command_id); + if(status == PB_CommandStatus_OK) { + PB_Main* response = MsgList_push_new(expected_msg_list); + test_rpc_create_simple_message( + response, PB_Main_storage_md5sum_response_tag, md5sum, command_id); + response->command_status = status; + } else { + test_rpc_add_empty_to_list(expected_msg_list, status, command_id); + } + + test_rpc_encode_and_feed_one(&request); + test_rpc_decode_and_compare(expected_msg_list); + + pb_release(&PB_Main_msg, &request); + test_rpc_free_msg_list(expected_msg_list); +} + +MU_TEST(test_storage_md5sum) { + char md5sum1[MD5SUM_SIZE * 2 + 1] = {0}; + char md5sum2[MD5SUM_SIZE * 2 + 1] = {0}; + char md5sum3[MD5SUM_SIZE * 2 + 1] = {0}; + + test_storage_md5sum_run( + TEST_DIR "test1.txt", ++command_id, "", PB_CommandStatus_ERROR_STORAGE_NOT_EXIST); + + test_create_file(TEST_DIR "file1.txt", 0); + test_create_file(TEST_DIR "file2.txt", 1); + test_create_file(TEST_DIR "file3.txt", 512); + test_storage_calculate_md5sum(TEST_DIR "file1.txt", md5sum1); + test_storage_calculate_md5sum(TEST_DIR "file2.txt", md5sum2); + test_storage_calculate_md5sum(TEST_DIR "file3.txt", md5sum3); + + test_storage_md5sum_run(TEST_DIR "file1.txt", ++command_id, md5sum1, PB_CommandStatus_OK); + test_storage_md5sum_run(TEST_DIR "file1.txt", ++command_id, md5sum1, PB_CommandStatus_OK); + + test_storage_md5sum_run(TEST_DIR "file2.txt", ++command_id, md5sum2, PB_CommandStatus_OK); + test_storage_md5sum_run(TEST_DIR "file2.txt", ++command_id, md5sum2, PB_CommandStatus_OK); + + test_storage_md5sum_run(TEST_DIR "file3.txt", ++command_id, md5sum3, PB_CommandStatus_OK); + test_storage_md5sum_run(TEST_DIR "file3.txt", ++command_id, md5sum3, PB_CommandStatus_OK); + + test_storage_md5sum_run(TEST_DIR "file2.txt", ++command_id, md5sum2, PB_CommandStatus_OK); + test_storage_md5sum_run(TEST_DIR "file3.txt", ++command_id, md5sum3, PB_CommandStatus_OK); + test_storage_md5sum_run(TEST_DIR "file1.txt", ++command_id, md5sum1, PB_CommandStatus_OK); + test_storage_md5sum_run(TEST_DIR "file2.txt", ++command_id, md5sum2, PB_CommandStatus_OK); +} + +MU_TEST(test_ping) { + MsgList_t input_msg_list; + MsgList_init(input_msg_list); + MsgList_t expected_msg_list; + MsgList_init(expected_msg_list); + + test_rpc_add_ping_to_list(input_msg_list, PING_REQUEST, 0); + test_rpc_add_ping_to_list(input_msg_list, PING_REQUEST, 1); + test_rpc_add_ping_to_list(input_msg_list, PING_REQUEST, 0); + test_rpc_add_ping_to_list(input_msg_list, PING_REQUEST, 500); + test_rpc_add_ping_to_list(input_msg_list, PING_REQUEST, (uint32_t)-1); + test_rpc_add_ping_to_list(input_msg_list, PING_REQUEST, 700); + test_rpc_add_ping_to_list(input_msg_list, PING_REQUEST, 1); + + test_rpc_add_ping_to_list(expected_msg_list, PING_RESPONSE, 0); + test_rpc_add_ping_to_list(expected_msg_list, PING_RESPONSE, 1); + test_rpc_add_ping_to_list(expected_msg_list, PING_RESPONSE, 0); + test_rpc_add_ping_to_list(expected_msg_list, PING_RESPONSE, 500); + test_rpc_add_ping_to_list(expected_msg_list, PING_RESPONSE, (uint32_t)-1); + test_rpc_add_ping_to_list(expected_msg_list, PING_RESPONSE, 700); + test_rpc_add_ping_to_list(expected_msg_list, PING_RESPONSE, 1); + + test_rpc_encode_and_feed(input_msg_list); + test_rpc_decode_and_compare(expected_msg_list); + + test_rpc_free_msg_list(input_msg_list); + test_rpc_free_msg_list(expected_msg_list); +} + +// TODO: 1) test for rubbish data +// 2) test for unexpected end of packet +// 3) test for one push of several packets +// 4) test for fill buffer till end (great varint) and close connection + +MU_TEST_SUITE(test_rpc_status) { + MU_SUITE_CONFIGURE(&test_rpc_storage_setup, &test_rpc_storage_teardown); + + MU_RUN_TEST(test_ping); +} + +MU_TEST_SUITE(test_rpc_storage) { + MU_SUITE_CONFIGURE(&test_rpc_storage_setup, &test_rpc_storage_teardown); + + MU_RUN_TEST(test_storage_list); + MU_RUN_TEST(test_storage_read); + MU_RUN_TEST(test_storage_write_read); + MU_RUN_TEST(test_storage_write); + MU_RUN_TEST(test_storage_delete); + MU_RUN_TEST(test_storage_mkdir); + MU_RUN_TEST(test_storage_md5sum); + MU_RUN_TEST(test_storage_interrupt_continuous_same_system); + MU_RUN_TEST(test_storage_interrupt_continuous_another_system); +} + +int run_minunit_test_rpc() { + MU_RUN_SUITE(test_rpc_storage); + MU_RUN_SUITE(test_rpc_status); + MU_REPORT(); + + return MU_EXIT_CODE; +} diff --git a/applications/tests/test_index.c b/applications/tests/test_index.c index c0f8919a..7bb775f3 100644 --- a/applications/tests/test_index.c +++ b/applications/tests/test_index.c @@ -6,6 +6,7 @@ int run_minunit(); int run_minunit_test_irda_decoder_encoder(); +int run_minunit_test_rpc(); int32_t flipper_test_app(void* p) { uint32_t test_result = 0; @@ -16,6 +17,7 @@ int32_t flipper_test_app(void* p) { // test_result |= run_minunit(); // disabled as it fails randomly test_result |= run_minunit_test_irda_decoder_encoder(); + test_result |= run_minunit_test_rpc(); if(test_result == 0) { // test passed diff --git a/assets/Makefile b/assets/Makefile index 1a379af5..efa07a1f 100644 --- a/assets/Makefile +++ b/assets/Makefile @@ -2,10 +2,16 @@ PROJECT_ROOT = $(abspath $(dir $(abspath $(firstword $(MAKEFILE_LIST))))..) include $(PROJECT_ROOT)/assets/assets.mk +all_assets: $(ASSETS) $(PROTOBUF) + $(ASSETS): $(ASSETS_SOURCES) $(ASSETS_COMPILLER) - @echo "\tASSETS\t" $@ + @echo "\tASSETS\t\t" $@ @$(ASSETS_COMPILLER) icons "$(ASSETS_SOURCE_DIR)" "$(ASSETS_COMPILED_DIR)" +$(PROTOBUF) &: $(PROTOBUF_SOURCES) $(PROTOBUF_COMPILER) + @echo "\tPROTOBUF\t" $(PROTOBUF_FILENAMES) + @$(PROJECT_ROOT)/lib/nanopb/generator/nanopb_generator.py -q -I$(PROTOBUF_SOURCE_DIR) -D$(PROTOBUF_COMPILED_DIR) $(PROTOBUF_SOURCES) + clean: @echo "\tCLEAN\t" @$(RM) $(ASSETS) diff --git a/assets/assets.mk b/assets/assets.mk index 7bb44a37..8430702e 100644 --- a/assets/assets.mk +++ b/assets/assets.mk @@ -6,5 +6,14 @@ ASSETS_SOURCE_DIR := $(ASSETS_DIR)/icons ASSETS_SOURCES += $(shell find $(ASSETS_SOURCE_DIR) -type f -iname '*.png' -or -iname 'frame_rate') ASSETS += $(ASSETS_COMPILED_DIR)/assets_icons.c -CFLAGS += -I$(ASSETS_COMPILED_DIR) -C_SOURCES += $(ASSETS_COMPILED_DIR)/assets_icons.c +PROTOBUF_SOURCE_DIR := $(ASSETS_DIR)/protobuf +PROTOBUF_COMPILER := $(PROJECT_ROOT)/lib/nanopb/generator/nanopb_generator.py +PROTOBUF_COMPILED_DIR := $(ASSETS_COMPILED_DIR) +PROTOBUF_SOURCES := $(shell find $(PROTOBUF_SOURCE_DIR) -type f -iname '*.proto') +#PROTOBUF_FILENAMES := $(notdir $(PROTOBUF)) +PROTOBUF_FILENAMES := $(notdir $(addsuffix .pb.c,$(basename $(PROTOBUF_SOURCES)))) +PROTOBUF := $(addprefix $(PROTOBUF_COMPILED_DIR)/,$(PROTOBUF_FILENAMES)) +PROTOBUF_CFLAGS += -DPB_ENABLE_MALLOC -DPB_WITHOUT_64BIT + +CFLAGS += -I$(ASSETS_COMPILED_DIR) $(PROTOBUF_CFLAGS) +C_SOURCES += $(wildcard $(ASSETS_COMPILED_DIR)/*.c) diff --git a/assets/compiled/flipper.pb.c b/assets/compiled/flipper.pb.c new file mode 100644 index 00000000..d81c78ec --- /dev/null +++ b/assets/compiled/flipper.pb.c @@ -0,0 +1,16 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.5 */ + +#include "flipper.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(PB_Empty, PB_Empty, AUTO) + + +PB_BIND(PB_Main, PB_Main, AUTO) + + + + diff --git a/assets/compiled/flipper.pb.h b/assets/compiled/flipper.pb.h new file mode 100644 index 00000000..f78393e6 --- /dev/null +++ b/assets/compiled/flipper.pb.h @@ -0,0 +1,153 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.5 */ + +#ifndef PB_PB_FLIPPER_PB_H_INCLUDED +#define PB_PB_FLIPPER_PB_H_INCLUDED +#include +#include "storage.pb.h" +#include "status.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +typedef enum _PB_CommandStatus { + PB_CommandStatus_OK = 0, + PB_CommandStatus_ERROR = 1, /* *< Unknown error */ + PB_CommandStatus_ERROR_DECODE = 2, /* *< Command can't be decoded successfully - command_id in response may be wrong! */ + PB_CommandStatus_ERROR_NOT_IMPLEMENTED = 3, /* *< Command succesfully decoded, but not implemented (deprecated or not yet implemented) */ + PB_CommandStatus_ERROR_BUSY = 4, /* *< Somebody took global lock, so not all commands are available */ + PB_CommandStatus_ERROR_CONTINUOUS_COMMAND_INTERRUPTED = 14, /* *< Not received has_next == 0 */ + PB_CommandStatus_ERROR_INVALID_PARAMETERS = 15, /* *< not provided (or provided invalid) crucial parameters to perform rpc */ + PB_CommandStatus_ERROR_STORAGE_NOT_READY = 5, /* *< FS not ready */ + PB_CommandStatus_ERROR_STORAGE_EXIST = 6, /* *< File/Dir alrady exist */ + PB_CommandStatus_ERROR_STORAGE_NOT_EXIST = 7, /* *< File/Dir does not exist */ + PB_CommandStatus_ERROR_STORAGE_INVALID_PARAMETER = 8, /* *< Invalid API parameter */ + PB_CommandStatus_ERROR_STORAGE_DENIED = 9, /* *< Access denied */ + PB_CommandStatus_ERROR_STORAGE_INVALID_NAME = 10, /* *< Invalid name/path */ + PB_CommandStatus_ERROR_STORAGE_INTERNAL = 11, /* *< Internal error */ + PB_CommandStatus_ERROR_STORAGE_NOT_IMPLEMENTED = 12, /* *< Functon not implemented */ + PB_CommandStatus_ERROR_STORAGE_ALREADY_OPEN = 13 /* *< File/Dir already opened */ +} PB_CommandStatus; + +/* Struct definitions */ +/* There are Server commands (e.g. Storage_write), which have no body message + in response. But 'oneof' obligate to have at least 1 encoded message + in scope. For this needs Empty message is implemented. */ +typedef struct _PB_Empty { + char dummy_field; +} PB_Empty; + +typedef struct _PB_Main { + uint32_t command_id; + PB_CommandStatus command_status; + bool has_next; + pb_callback_t cb_content; + pb_size_t which_content; + union { + PB_Empty empty; + PB_Status_PingRequest ping_request; + PB_Status_PingResponse ping_response; + PB_Storage_ListRequest storage_list_request; + PB_Storage_ListResponse storage_list_response; + PB_Storage_ReadRequest storage_read_request; + PB_Storage_ReadResponse storage_read_response; + PB_Storage_WriteRequest storage_write_request; + PB_Storage_DeleteRequest storage_delete_request; + PB_Storage_MkdirRequest storage_mkdir_request; + PB_Storage_Md5sumRequest storage_md5sum_request; + PB_Storage_Md5sumResponse storage_md5sum_response; + } content; +} PB_Main; + + +/* Helper constants for enums */ +#define _PB_CommandStatus_MIN PB_CommandStatus_OK +#define _PB_CommandStatus_MAX PB_CommandStatus_ERROR_INVALID_PARAMETERS +#define _PB_CommandStatus_ARRAYSIZE ((PB_CommandStatus)(PB_CommandStatus_ERROR_INVALID_PARAMETERS+1)) + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define PB_Empty_init_default {0} +#define PB_Main_init_default {0, _PB_CommandStatus_MIN, 0, {{NULL}, NULL}, 0, {PB_Empty_init_default}} +#define PB_Empty_init_zero {0} +#define PB_Main_init_zero {0, _PB_CommandStatus_MIN, 0, {{NULL}, NULL}, 0, {PB_Empty_init_zero}} + +/* Field tags (for use in manual encoding/decoding) */ +#define PB_Main_command_id_tag 1 +#define PB_Main_command_status_tag 2 +#define PB_Main_has_next_tag 3 +#define PB_Main_empty_tag 4 +#define PB_Main_ping_request_tag 5 +#define PB_Main_ping_response_tag 6 +#define PB_Main_storage_list_request_tag 7 +#define PB_Main_storage_list_response_tag 8 +#define PB_Main_storage_read_request_tag 9 +#define PB_Main_storage_read_response_tag 10 +#define PB_Main_storage_write_request_tag 11 +#define PB_Main_storage_delete_request_tag 12 +#define PB_Main_storage_mkdir_request_tag 13 +#define PB_Main_storage_md5sum_request_tag 14 +#define PB_Main_storage_md5sum_response_tag 15 + +/* Struct field encoding specification for nanopb */ +#define PB_Empty_FIELDLIST(X, a) \ + +#define PB_Empty_CALLBACK NULL +#define PB_Empty_DEFAULT NULL + +#define PB_Main_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, command_id, 1) \ +X(a, STATIC, SINGULAR, UENUM, command_status, 2) \ +X(a, STATIC, SINGULAR, BOOL, has_next, 3) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,empty,content.empty), 4) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,ping_request,content.ping_request), 5) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,ping_response,content.ping_response), 6) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,storage_list_request,content.storage_list_request), 7) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,storage_list_response,content.storage_list_response), 8) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,storage_read_request,content.storage_read_request), 9) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,storage_read_response,content.storage_read_response), 10) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,storage_write_request,content.storage_write_request), 11) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,storage_delete_request,content.storage_delete_request), 12) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,storage_mkdir_request,content.storage_mkdir_request), 13) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,storage_md5sum_request,content.storage_md5sum_request), 14) \ +X(a, STATIC, ONEOF, MSG_W_CB, (content,storage_md5sum_response,content.storage_md5sum_response), 15) +#define PB_Main_CALLBACK NULL +#define PB_Main_DEFAULT NULL +#define PB_Main_content_empty_MSGTYPE PB_Empty +#define PB_Main_content_ping_request_MSGTYPE PB_Status_PingRequest +#define PB_Main_content_ping_response_MSGTYPE PB_Status_PingResponse +#define PB_Main_content_storage_list_request_MSGTYPE PB_Storage_ListRequest +#define PB_Main_content_storage_list_response_MSGTYPE PB_Storage_ListResponse +#define PB_Main_content_storage_read_request_MSGTYPE PB_Storage_ReadRequest +#define PB_Main_content_storage_read_response_MSGTYPE PB_Storage_ReadResponse +#define PB_Main_content_storage_write_request_MSGTYPE PB_Storage_WriteRequest +#define PB_Main_content_storage_delete_request_MSGTYPE PB_Storage_DeleteRequest +#define PB_Main_content_storage_mkdir_request_MSGTYPE PB_Storage_MkdirRequest +#define PB_Main_content_storage_md5sum_request_MSGTYPE PB_Storage_Md5sumRequest +#define PB_Main_content_storage_md5sum_response_MSGTYPE PB_Storage_Md5sumResponse + +extern const pb_msgdesc_t PB_Empty_msg; +extern const pb_msgdesc_t PB_Main_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define PB_Empty_fields &PB_Empty_msg +#define PB_Main_fields &PB_Main_msg + +/* Maximum encoded size of messages (where known) */ +#define PB_Empty_size 0 +#if defined(PB_Storage_ListRequest_size) && defined(PB_Storage_ListResponse_size) && defined(PB_Storage_ReadRequest_size) && defined(PB_Storage_ReadResponse_size) && defined(PB_Storage_WriteRequest_size) && defined(PB_Storage_DeleteRequest_size) && defined(PB_Storage_MkdirRequest_size) && defined(PB_Storage_Md5sumRequest_size) +#define PB_Main_size (10 + sizeof(union PB_Main_content_size_union)) +union PB_Main_content_size_union {char f7[(6 + PB_Storage_ListRequest_size)]; char f8[(6 + PB_Storage_ListResponse_size)]; char f9[(6 + PB_Storage_ReadRequest_size)]; char f10[(6 + PB_Storage_ReadResponse_size)]; char f11[(6 + PB_Storage_WriteRequest_size)]; char f12[(6 + PB_Storage_DeleteRequest_size)]; char f13[(6 + PB_Storage_MkdirRequest_size)]; char f14[(6 + PB_Storage_Md5sumRequest_size)]; char f0[36];}; +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/assets/compiled/status.pb.c b/assets/compiled/status.pb.c new file mode 100644 index 00000000..6010eb55 --- /dev/null +++ b/assets/compiled/status.pb.c @@ -0,0 +1,15 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.5 */ + +#include "status.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(PB_Status_PingRequest, PB_Status_PingRequest, AUTO) + + +PB_BIND(PB_Status_PingResponse, PB_Status_PingResponse, AUTO) + + + diff --git a/assets/compiled/status.pb.h b/assets/compiled/status.pb.h new file mode 100644 index 00000000..acc6902a --- /dev/null +++ b/assets/compiled/status.pb.h @@ -0,0 +1,60 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.5 */ + +#ifndef PB_PB_STATUS_STATUS_PB_H_INCLUDED +#define PB_PB_STATUS_STATUS_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef struct _PB_Status_PingRequest { + char dummy_field; +} PB_Status_PingRequest; + +typedef struct _PB_Status_PingResponse { + char dummy_field; +} PB_Status_PingResponse; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define PB_Status_PingRequest_init_default {0} +#define PB_Status_PingResponse_init_default {0} +#define PB_Status_PingRequest_init_zero {0} +#define PB_Status_PingResponse_init_zero {0} + +/* Field tags (for use in manual encoding/decoding) */ + +/* Struct field encoding specification for nanopb */ +#define PB_Status_PingRequest_FIELDLIST(X, a) \ + +#define PB_Status_PingRequest_CALLBACK NULL +#define PB_Status_PingRequest_DEFAULT NULL + +#define PB_Status_PingResponse_FIELDLIST(X, a) \ + +#define PB_Status_PingResponse_CALLBACK NULL +#define PB_Status_PingResponse_DEFAULT NULL + +extern const pb_msgdesc_t PB_Status_PingRequest_msg; +extern const pb_msgdesc_t PB_Status_PingResponse_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define PB_Status_PingRequest_fields &PB_Status_PingRequest_msg +#define PB_Status_PingResponse_fields &PB_Status_PingResponse_msg + +/* Maximum encoded size of messages (where known) */ +#define PB_Status_PingRequest_size 0 +#define PB_Status_PingResponse_size 0 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/assets/compiled/storage.pb.c b/assets/compiled/storage.pb.c new file mode 100644 index 00000000..a74477e1 --- /dev/null +++ b/assets/compiled/storage.pb.c @@ -0,0 +1,40 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.5 */ + +#include "storage.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(PB_Storage_File, PB_Storage_File, AUTO) + + +PB_BIND(PB_Storage_ListRequest, PB_Storage_ListRequest, AUTO) + + +PB_BIND(PB_Storage_ListResponse, PB_Storage_ListResponse, AUTO) + + +PB_BIND(PB_Storage_ReadRequest, PB_Storage_ReadRequest, AUTO) + + +PB_BIND(PB_Storage_ReadResponse, PB_Storage_ReadResponse, AUTO) + + +PB_BIND(PB_Storage_WriteRequest, PB_Storage_WriteRequest, AUTO) + + +PB_BIND(PB_Storage_DeleteRequest, PB_Storage_DeleteRequest, AUTO) + + +PB_BIND(PB_Storage_MkdirRequest, PB_Storage_MkdirRequest, AUTO) + + +PB_BIND(PB_Storage_Md5sumRequest, PB_Storage_Md5sumRequest, AUTO) + + +PB_BIND(PB_Storage_Md5sumResponse, PB_Storage_Md5sumResponse, AUTO) + + + + diff --git a/assets/compiled/storage.pb.h b/assets/compiled/storage.pb.h new file mode 100644 index 00000000..cdb47372 --- /dev/null +++ b/assets/compiled/storage.pb.h @@ -0,0 +1,212 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.5 */ + +#ifndef PB_PB_STORAGE_STORAGE_PB_H_INCLUDED +#define PB_PB_STORAGE_STORAGE_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +typedef enum _PB_Storage_File_FileType { + PB_Storage_File_FileType_FILE = 0, + PB_Storage_File_FileType_DIR = 1 +} PB_Storage_File_FileType; + +/* Struct definitions */ +typedef struct _PB_Storage_DeleteRequest { + char *path; +} PB_Storage_DeleteRequest; + +typedef struct _PB_Storage_ListRequest { + char *path; +} PB_Storage_ListRequest; + +typedef struct _PB_Storage_Md5sumRequest { + char *path; +} PB_Storage_Md5sumRequest; + +typedef struct _PB_Storage_MkdirRequest { + char *path; +} PB_Storage_MkdirRequest; + +typedef struct _PB_Storage_ReadRequest { + char *path; +} PB_Storage_ReadRequest; + +typedef struct _PB_Storage_File { + PB_Storage_File_FileType type; + char *name; + uint32_t size; + pb_bytes_array_t *data; +} PB_Storage_File; + +typedef struct _PB_Storage_Md5sumResponse { + char md5sum[33]; +} PB_Storage_Md5sumResponse; + +typedef struct _PB_Storage_ListResponse { + pb_size_t file_count; + PB_Storage_File file[8]; +} PB_Storage_ListResponse; + +typedef struct _PB_Storage_ReadResponse { + bool has_file; + PB_Storage_File file; +} PB_Storage_ReadResponse; + +typedef struct _PB_Storage_WriteRequest { + char *path; + bool has_file; + PB_Storage_File file; +} PB_Storage_WriteRequest; + + +/* Helper constants for enums */ +#define _PB_Storage_File_FileType_MIN PB_Storage_File_FileType_FILE +#define _PB_Storage_File_FileType_MAX PB_Storage_File_FileType_DIR +#define _PB_Storage_File_FileType_ARRAYSIZE ((PB_Storage_File_FileType)(PB_Storage_File_FileType_DIR+1)) + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define PB_Storage_File_init_default {_PB_Storage_File_FileType_MIN, NULL, 0, NULL} +#define PB_Storage_ListRequest_init_default {NULL} +#define PB_Storage_ListResponse_init_default {0, {PB_Storage_File_init_default, PB_Storage_File_init_default, PB_Storage_File_init_default, PB_Storage_File_init_default, PB_Storage_File_init_default, PB_Storage_File_init_default, PB_Storage_File_init_default, PB_Storage_File_init_default}} +#define PB_Storage_ReadRequest_init_default {NULL} +#define PB_Storage_ReadResponse_init_default {false, PB_Storage_File_init_default} +#define PB_Storage_WriteRequest_init_default {NULL, false, PB_Storage_File_init_default} +#define PB_Storage_DeleteRequest_init_default {NULL} +#define PB_Storage_MkdirRequest_init_default {NULL} +#define PB_Storage_Md5sumRequest_init_default {NULL} +#define PB_Storage_Md5sumResponse_init_default {""} +#define PB_Storage_File_init_zero {_PB_Storage_File_FileType_MIN, NULL, 0, NULL} +#define PB_Storage_ListRequest_init_zero {NULL} +#define PB_Storage_ListResponse_init_zero {0, {PB_Storage_File_init_zero, PB_Storage_File_init_zero, PB_Storage_File_init_zero, PB_Storage_File_init_zero, PB_Storage_File_init_zero, PB_Storage_File_init_zero, PB_Storage_File_init_zero, PB_Storage_File_init_zero}} +#define PB_Storage_ReadRequest_init_zero {NULL} +#define PB_Storage_ReadResponse_init_zero {false, PB_Storage_File_init_zero} +#define PB_Storage_WriteRequest_init_zero {NULL, false, PB_Storage_File_init_zero} +#define PB_Storage_DeleteRequest_init_zero {NULL} +#define PB_Storage_MkdirRequest_init_zero {NULL} +#define PB_Storage_Md5sumRequest_init_zero {NULL} +#define PB_Storage_Md5sumResponse_init_zero {""} + +/* Field tags (for use in manual encoding/decoding) */ +#define PB_Storage_DeleteRequest_path_tag 1 +#define PB_Storage_ListRequest_path_tag 1 +#define PB_Storage_Md5sumRequest_path_tag 1 +#define PB_Storage_MkdirRequest_path_tag 1 +#define PB_Storage_ReadRequest_path_tag 1 +#define PB_Storage_File_type_tag 1 +#define PB_Storage_File_name_tag 2 +#define PB_Storage_File_size_tag 3 +#define PB_Storage_File_data_tag 4 +#define PB_Storage_Md5sumResponse_md5sum_tag 1 +#define PB_Storage_ListResponse_file_tag 1 +#define PB_Storage_ReadResponse_file_tag 1 +#define PB_Storage_WriteRequest_path_tag 1 +#define PB_Storage_WriteRequest_file_tag 2 + +/* Struct field encoding specification for nanopb */ +#define PB_Storage_File_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, type, 1) \ +X(a, POINTER, SINGULAR, STRING, name, 2) \ +X(a, STATIC, SINGULAR, UINT32, size, 3) \ +X(a, POINTER, SINGULAR, BYTES, data, 4) +#define PB_Storage_File_CALLBACK NULL +#define PB_Storage_File_DEFAULT NULL + +#define PB_Storage_ListRequest_FIELDLIST(X, a) \ +X(a, POINTER, SINGULAR, STRING, path, 1) +#define PB_Storage_ListRequest_CALLBACK NULL +#define PB_Storage_ListRequest_DEFAULT NULL + +#define PB_Storage_ListResponse_FIELDLIST(X, a) \ +X(a, STATIC, REPEATED, MESSAGE, file, 1) +#define PB_Storage_ListResponse_CALLBACK NULL +#define PB_Storage_ListResponse_DEFAULT NULL +#define PB_Storage_ListResponse_file_MSGTYPE PB_Storage_File + +#define PB_Storage_ReadRequest_FIELDLIST(X, a) \ +X(a, POINTER, SINGULAR, STRING, path, 1) +#define PB_Storage_ReadRequest_CALLBACK NULL +#define PB_Storage_ReadRequest_DEFAULT NULL + +#define PB_Storage_ReadResponse_FIELDLIST(X, a) \ +X(a, STATIC, OPTIONAL, MESSAGE, file, 1) +#define PB_Storage_ReadResponse_CALLBACK NULL +#define PB_Storage_ReadResponse_DEFAULT NULL +#define PB_Storage_ReadResponse_file_MSGTYPE PB_Storage_File + +#define PB_Storage_WriteRequest_FIELDLIST(X, a) \ +X(a, POINTER, SINGULAR, STRING, path, 1) \ +X(a, STATIC, OPTIONAL, MESSAGE, file, 2) +#define PB_Storage_WriteRequest_CALLBACK NULL +#define PB_Storage_WriteRequest_DEFAULT NULL +#define PB_Storage_WriteRequest_file_MSGTYPE PB_Storage_File + +#define PB_Storage_DeleteRequest_FIELDLIST(X, a) \ +X(a, POINTER, SINGULAR, STRING, path, 1) +#define PB_Storage_DeleteRequest_CALLBACK NULL +#define PB_Storage_DeleteRequest_DEFAULT NULL + +#define PB_Storage_MkdirRequest_FIELDLIST(X, a) \ +X(a, POINTER, SINGULAR, STRING, path, 1) +#define PB_Storage_MkdirRequest_CALLBACK NULL +#define PB_Storage_MkdirRequest_DEFAULT NULL + +#define PB_Storage_Md5sumRequest_FIELDLIST(X, a) \ +X(a, POINTER, SINGULAR, STRING, path, 1) +#define PB_Storage_Md5sumRequest_CALLBACK NULL +#define PB_Storage_Md5sumRequest_DEFAULT NULL + +#define PB_Storage_Md5sumResponse_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, md5sum, 1) +#define PB_Storage_Md5sumResponse_CALLBACK NULL +#define PB_Storage_Md5sumResponse_DEFAULT NULL + +extern const pb_msgdesc_t PB_Storage_File_msg; +extern const pb_msgdesc_t PB_Storage_ListRequest_msg; +extern const pb_msgdesc_t PB_Storage_ListResponse_msg; +extern const pb_msgdesc_t PB_Storage_ReadRequest_msg; +extern const pb_msgdesc_t PB_Storage_ReadResponse_msg; +extern const pb_msgdesc_t PB_Storage_WriteRequest_msg; +extern const pb_msgdesc_t PB_Storage_DeleteRequest_msg; +extern const pb_msgdesc_t PB_Storage_MkdirRequest_msg; +extern const pb_msgdesc_t PB_Storage_Md5sumRequest_msg; +extern const pb_msgdesc_t PB_Storage_Md5sumResponse_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define PB_Storage_File_fields &PB_Storage_File_msg +#define PB_Storage_ListRequest_fields &PB_Storage_ListRequest_msg +#define PB_Storage_ListResponse_fields &PB_Storage_ListResponse_msg +#define PB_Storage_ReadRequest_fields &PB_Storage_ReadRequest_msg +#define PB_Storage_ReadResponse_fields &PB_Storage_ReadResponse_msg +#define PB_Storage_WriteRequest_fields &PB_Storage_WriteRequest_msg +#define PB_Storage_DeleteRequest_fields &PB_Storage_DeleteRequest_msg +#define PB_Storage_MkdirRequest_fields &PB_Storage_MkdirRequest_msg +#define PB_Storage_Md5sumRequest_fields &PB_Storage_Md5sumRequest_msg +#define PB_Storage_Md5sumResponse_fields &PB_Storage_Md5sumResponse_msg + +/* Maximum encoded size of messages (where known) */ +/* PB_Storage_File_size depends on runtime parameters */ +/* PB_Storage_ListRequest_size depends on runtime parameters */ +/* PB_Storage_ListResponse_size depends on runtime parameters */ +/* PB_Storage_ReadRequest_size depends on runtime parameters */ +/* PB_Storage_ReadResponse_size depends on runtime parameters */ +/* PB_Storage_WriteRequest_size depends on runtime parameters */ +/* PB_Storage_DeleteRequest_size depends on runtime parameters */ +/* PB_Storage_MkdirRequest_size depends on runtime parameters */ +/* PB_Storage_Md5sumRequest_size depends on runtime parameters */ +#define PB_Storage_Md5sumResponse_size 34 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/assets/protobuf b/assets/protobuf new file mode 160000 index 00000000..41599b8e --- /dev/null +++ b/assets/protobuf @@ -0,0 +1 @@ +Subproject commit 41599b8e6a6b33a229e8f5fa58de1a2cfcc8184a diff --git a/firmware/targets/f6/ble-glue/gap.c b/firmware/targets/f6/ble-glue/gap.c index 74e1d35a..73c8c0a7 100644 --- a/firmware/targets/f6/ble-glue/gap.c +++ b/firmware/targets/f6/ble-glue/gap.c @@ -10,6 +10,7 @@ #include "serial_service.h" #include +#include #include #define GAP_TAG "BLE" @@ -34,6 +35,8 @@ typedef struct { osMutexId_t state_mutex; uint8_t mac_address[BD_ADDR_SIZE_LOCAL]; Bt* bt; + Rpc* rpc; + RpcSession* rpc_session; osTimerId advertise_timer; osThreadAttr_t thread_attr; osThreadId_t thread_id; @@ -81,7 +84,8 @@ SVCCTL_UserEvtFlowStatus_t SVCCTL_App_Notification( void *pckt ) if (disconnection_complete_event->Connection_Handle == gap->gap_svc.connection_handle) { gap->gap_svc.connection_handle = 0; gap->state = GapStateIdle; - FURI_LOG_I(GAP_TAG, "Disconnect from client"); + FURI_LOG_I(GAP_TAG, "Disconnect from client. Close RPC session"); + rpc_close_session(gap->rpc_session); } if(gap->enable_adv) { // Restart advertising @@ -116,7 +120,9 @@ SVCCTL_UserEvtFlowStatus_t SVCCTL_App_Notification( void *pckt ) case EVT_LE_CONN_COMPLETE: furi_hal_power_insomnia_enter(); hci_le_connection_complete_event_rp0* connection_complete_event = (hci_le_connection_complete_event_rp0 *) meta_evt->data; - FURI_LOG_I(GAP_TAG, "Connection complete for connection handle 0x%x", connection_complete_event->Connection_Handle); + FURI_LOG_I(GAP_TAG, "Connection complete for connection handle 0x%x. Start RPC session", connection_complete_event->Connection_Handle); + gap->rpc_session = rpc_open_session(gap->rpc); + serial_svc_set_rpc_session(gap->rpc_session); // Stop advertising as connection completed osTimerStop(gap->advertise_timer); @@ -377,8 +383,9 @@ bool gap_init() { gap = furi_alloc(sizeof(Gap)); srand(DWT->CYCCNT); - // Open Bt record + // Open records gap->bt = furi_record_open("bt"); + gap->rpc = furi_record_open("rpc"); // Create advertising timer gap->advertise_timer = osTimerNew(gap_advetise_timer_callback, osTimerOnce, NULL, NULL); // Initialization of GATT & GAP layer diff --git a/firmware/targets/f6/ble-glue/serial_service.c b/firmware/targets/f6/ble-glue/serial_service.c index e5ab9d2d..a6f8e626 100644 --- a/firmware/targets/f6/ble-glue/serial_service.c +++ b/firmware/targets/f6/ble-glue/serial_service.c @@ -12,6 +12,8 @@ typedef struct { uint16_t svc_handle; uint16_t rx_char_handle; uint16_t tx_char_handle; + RpcSession* rpc_session; + osSemaphoreId_t rpc_sem; } SerialSvc; static SerialSvc* serial_svc; @@ -20,6 +22,21 @@ static const uint8_t service_uuid[] = {0x00, 0x00, 0xfe, 0x60, 0xcc, 0x7a, 0x48, static const uint8_t char_rx_uuid[] = {0x00, 0x00, 0xfe, 0x61, 0x8e, 0x22, 0x45, 0x41, 0x9d, 0x4c, 0x21, 0xed, 0xae, 0x82, 0xed, 0x19}; static const uint8_t char_tx_uuid[] = {0x00, 0x00, 0xfe, 0x62, 0x8e, 0x22, 0x45, 0x41, 0x9d, 0x4c, 0x21, 0xed, 0xae, 0x82, 0xed, 0x19}; +void serial_svc_rpc_send_bytes_callback(void* context, uint8_t* bytes, size_t bytes_len) { + size_t bytes_sent = 0; + while(bytes_sent < bytes_len) { + size_t bytes_remain = bytes_len - bytes_sent; + if(bytes_remain > SERIAL_SVC_DATA_LEN_MAX) { + serial_svc_update_rx(&bytes[bytes_sent], SERIAL_SVC_DATA_LEN_MAX); + bytes_sent += SERIAL_SVC_DATA_LEN_MAX; + } else { + serial_svc_update_rx(&bytes[bytes_sent], bytes_remain); + bytes_sent += bytes_remain; + } + osSemaphoreAcquire(serial_svc->rpc_sem, osWaitForever); + } +} + static SVCCTL_EvtAckStatus_t serial_svc_event_handler(void *event) { SVCCTL_EvtAckStatus_t ret = SVCCTL_EvtNotAck; hci_event_pckt* event_pckt = (hci_event_pckt *)(((hci_uart_pckt*)event)->data); @@ -34,10 +51,12 @@ static SVCCTL_EvtAckStatus_t serial_svc_event_handler(void *event) { FURI_LOG_D(SERIAL_SERVICE_TAG, "TX descriptor event"); } else if(attribute_modified->Attr_Handle == serial_svc->tx_char_handle + 1) { FURI_LOG_D(SERIAL_SERVICE_TAG, "Received %d bytes", attribute_modified->Attr_Data_Length); - serial_svc_update_rx(attribute_modified->Attr_Data, attribute_modified->Attr_Data_Length); + rpc_feed_bytes(serial_svc->rpc_session, attribute_modified->Attr_Data, attribute_modified->Attr_Data_Length, 1000); + // serial_svc_update_rx(attribute_modified->Attr_Data, attribute_modified->Attr_Data_Length); ret = SVCCTL_EvtAckFlowEnable; } } else if(blecore_evt->ecode == ACI_GATT_SERVER_CONFIRMATION_VSEVT_CODE) { + osSemaphoreRelease(serial_svc->rpc_sem); FURI_LOG_D(SERIAL_SERVICE_TAG, "Ack received", blecore_evt->ecode); ret = SVCCTL_EvtAckFlowEnable; } @@ -45,9 +64,10 @@ static SVCCTL_EvtAckStatus_t serial_svc_event_handler(void *event) { return ret; } -void serial_svc_start() { +void serial_svc_start(Rpc* rpc) { tBleStatus status; serial_svc = furi_alloc(sizeof(SerialSvc)); + serial_svc->rpc_sem = osSemaphoreNew(1, 0, NULL); // Register event handler SVCCTL_RegisterSvcHandler(serial_svc_event_handler); @@ -106,12 +126,20 @@ void serial_svc_stop() { } } +void serial_svc_set_rpc_session(RpcSession* rpc_session) { + furi_assert(rpc_session); + // Set session + serial_svc->rpc_session = rpc_session; + // Set callback + rpc_set_send_bytes_callback(serial_svc->rpc_session, serial_svc_rpc_send_bytes_callback, NULL); +} + bool serial_svc_update_rx(uint8_t* data, uint8_t data_len) { if(data_len > SERIAL_SVC_DATA_LEN_MAX) { return false; } - + FURI_LOG_D(SERIAL_SERVICE_TAG, "Updating char %d len", data_len); tBleStatus result = aci_gatt_update_char_value(serial_svc->svc_handle, serial_svc->rx_char_handle, 0, diff --git a/firmware/targets/f6/ble-glue/serial_service.h b/firmware/targets/f6/ble-glue/serial_service.h index 9d3d6217..bdb924f8 100644 --- a/firmware/targets/f6/ble-glue/serial_service.h +++ b/firmware/targets/f6/ble-glue/serial_service.h @@ -3,12 +3,17 @@ #include #include +#include + + #ifdef __cplusplus extern "C" { #endif void serial_svc_start(); +void serial_svc_set_rpc_session(RpcSession* rpc_session); + void serial_svc_stop(); bool serial_svc_update_rx(uint8_t* data, uint8_t data_len); diff --git a/firmware/targets/f7/ble-glue/gap.c b/firmware/targets/f7/ble-glue/gap.c index 74e1d35a..73c8c0a7 100644 --- a/firmware/targets/f7/ble-glue/gap.c +++ b/firmware/targets/f7/ble-glue/gap.c @@ -10,6 +10,7 @@ #include "serial_service.h" #include +#include #include #define GAP_TAG "BLE" @@ -34,6 +35,8 @@ typedef struct { osMutexId_t state_mutex; uint8_t mac_address[BD_ADDR_SIZE_LOCAL]; Bt* bt; + Rpc* rpc; + RpcSession* rpc_session; osTimerId advertise_timer; osThreadAttr_t thread_attr; osThreadId_t thread_id; @@ -81,7 +84,8 @@ SVCCTL_UserEvtFlowStatus_t SVCCTL_App_Notification( void *pckt ) if (disconnection_complete_event->Connection_Handle == gap->gap_svc.connection_handle) { gap->gap_svc.connection_handle = 0; gap->state = GapStateIdle; - FURI_LOG_I(GAP_TAG, "Disconnect from client"); + FURI_LOG_I(GAP_TAG, "Disconnect from client. Close RPC session"); + rpc_close_session(gap->rpc_session); } if(gap->enable_adv) { // Restart advertising @@ -116,7 +120,9 @@ SVCCTL_UserEvtFlowStatus_t SVCCTL_App_Notification( void *pckt ) case EVT_LE_CONN_COMPLETE: furi_hal_power_insomnia_enter(); hci_le_connection_complete_event_rp0* connection_complete_event = (hci_le_connection_complete_event_rp0 *) meta_evt->data; - FURI_LOG_I(GAP_TAG, "Connection complete for connection handle 0x%x", connection_complete_event->Connection_Handle); + FURI_LOG_I(GAP_TAG, "Connection complete for connection handle 0x%x. Start RPC session", connection_complete_event->Connection_Handle); + gap->rpc_session = rpc_open_session(gap->rpc); + serial_svc_set_rpc_session(gap->rpc_session); // Stop advertising as connection completed osTimerStop(gap->advertise_timer); @@ -377,8 +383,9 @@ bool gap_init() { gap = furi_alloc(sizeof(Gap)); srand(DWT->CYCCNT); - // Open Bt record + // Open records gap->bt = furi_record_open("bt"); + gap->rpc = furi_record_open("rpc"); // Create advertising timer gap->advertise_timer = osTimerNew(gap_advetise_timer_callback, osTimerOnce, NULL, NULL); // Initialization of GATT & GAP layer diff --git a/firmware/targets/f7/ble-glue/serial_service.c b/firmware/targets/f7/ble-glue/serial_service.c index e5ab9d2d..a6f8e626 100644 --- a/firmware/targets/f7/ble-glue/serial_service.c +++ b/firmware/targets/f7/ble-glue/serial_service.c @@ -12,6 +12,8 @@ typedef struct { uint16_t svc_handle; uint16_t rx_char_handle; uint16_t tx_char_handle; + RpcSession* rpc_session; + osSemaphoreId_t rpc_sem; } SerialSvc; static SerialSvc* serial_svc; @@ -20,6 +22,21 @@ static const uint8_t service_uuid[] = {0x00, 0x00, 0xfe, 0x60, 0xcc, 0x7a, 0x48, static const uint8_t char_rx_uuid[] = {0x00, 0x00, 0xfe, 0x61, 0x8e, 0x22, 0x45, 0x41, 0x9d, 0x4c, 0x21, 0xed, 0xae, 0x82, 0xed, 0x19}; static const uint8_t char_tx_uuid[] = {0x00, 0x00, 0xfe, 0x62, 0x8e, 0x22, 0x45, 0x41, 0x9d, 0x4c, 0x21, 0xed, 0xae, 0x82, 0xed, 0x19}; +void serial_svc_rpc_send_bytes_callback(void* context, uint8_t* bytes, size_t bytes_len) { + size_t bytes_sent = 0; + while(bytes_sent < bytes_len) { + size_t bytes_remain = bytes_len - bytes_sent; + if(bytes_remain > SERIAL_SVC_DATA_LEN_MAX) { + serial_svc_update_rx(&bytes[bytes_sent], SERIAL_SVC_DATA_LEN_MAX); + bytes_sent += SERIAL_SVC_DATA_LEN_MAX; + } else { + serial_svc_update_rx(&bytes[bytes_sent], bytes_remain); + bytes_sent += bytes_remain; + } + osSemaphoreAcquire(serial_svc->rpc_sem, osWaitForever); + } +} + static SVCCTL_EvtAckStatus_t serial_svc_event_handler(void *event) { SVCCTL_EvtAckStatus_t ret = SVCCTL_EvtNotAck; hci_event_pckt* event_pckt = (hci_event_pckt *)(((hci_uart_pckt*)event)->data); @@ -34,10 +51,12 @@ static SVCCTL_EvtAckStatus_t serial_svc_event_handler(void *event) { FURI_LOG_D(SERIAL_SERVICE_TAG, "TX descriptor event"); } else if(attribute_modified->Attr_Handle == serial_svc->tx_char_handle + 1) { FURI_LOG_D(SERIAL_SERVICE_TAG, "Received %d bytes", attribute_modified->Attr_Data_Length); - serial_svc_update_rx(attribute_modified->Attr_Data, attribute_modified->Attr_Data_Length); + rpc_feed_bytes(serial_svc->rpc_session, attribute_modified->Attr_Data, attribute_modified->Attr_Data_Length, 1000); + // serial_svc_update_rx(attribute_modified->Attr_Data, attribute_modified->Attr_Data_Length); ret = SVCCTL_EvtAckFlowEnable; } } else if(blecore_evt->ecode == ACI_GATT_SERVER_CONFIRMATION_VSEVT_CODE) { + osSemaphoreRelease(serial_svc->rpc_sem); FURI_LOG_D(SERIAL_SERVICE_TAG, "Ack received", blecore_evt->ecode); ret = SVCCTL_EvtAckFlowEnable; } @@ -45,9 +64,10 @@ static SVCCTL_EvtAckStatus_t serial_svc_event_handler(void *event) { return ret; } -void serial_svc_start() { +void serial_svc_start(Rpc* rpc) { tBleStatus status; serial_svc = furi_alloc(sizeof(SerialSvc)); + serial_svc->rpc_sem = osSemaphoreNew(1, 0, NULL); // Register event handler SVCCTL_RegisterSvcHandler(serial_svc_event_handler); @@ -106,12 +126,20 @@ void serial_svc_stop() { } } +void serial_svc_set_rpc_session(RpcSession* rpc_session) { + furi_assert(rpc_session); + // Set session + serial_svc->rpc_session = rpc_session; + // Set callback + rpc_set_send_bytes_callback(serial_svc->rpc_session, serial_svc_rpc_send_bytes_callback, NULL); +} + bool serial_svc_update_rx(uint8_t* data, uint8_t data_len) { if(data_len > SERIAL_SVC_DATA_LEN_MAX) { return false; } - + FURI_LOG_D(SERIAL_SERVICE_TAG, "Updating char %d len", data_len); tBleStatus result = aci_gatt_update_char_value(serial_svc->svc_handle, serial_svc->rx_char_handle, 0, diff --git a/firmware/targets/f7/ble-glue/serial_service.h b/firmware/targets/f7/ble-glue/serial_service.h index 9d3d6217..bdb924f8 100644 --- a/firmware/targets/f7/ble-glue/serial_service.h +++ b/firmware/targets/f7/ble-glue/serial_service.h @@ -3,12 +3,17 @@ #include #include +#include + + #ifdef __cplusplus extern "C" { #endif void serial_svc_start(); +void serial_svc_set_rpc_session(RpcSession* rpc_session); + void serial_svc_stop(); bool serial_svc_update_rx(uint8_t* data, uint8_t data_len); diff --git a/firmware/targets/f7/furi-hal/furi-hal-rfid.c b/firmware/targets/f7/furi-hal/furi-hal-rfid.c index d0772231..59d24333 100644 --- a/firmware/targets/f7/furi-hal/furi-hal-rfid.c +++ b/firmware/targets/f7/furi-hal/furi-hal-rfid.c @@ -42,7 +42,7 @@ void furi_hal_rfid_pins_emulate() { hal_gpio_write(&gpio_rfid_carrier_out, false); hal_gpio_init_ex( - &gpio_rfid_carrier, GpioModeAltFunctionPushPull, GpioSpeedLow, GpioPullUp, GpioAltFn2TIM2); + &gpio_rfid_carrier, GpioModeAltFunctionPushPull, GpioPullNo, GpioSpeedLow, GpioAltFn2TIM2); } void furi_hal_rfid_pins_read() { diff --git a/lib/lib.mk b/lib/lib.mk index ce16722b..4ebb484a 100644 --- a/lib/lib.mk +++ b/lib/lib.mk @@ -113,3 +113,7 @@ CPP_SOURCES += $(wildcard $(LIB_DIR)/toolbox/*.cpp) # USB Stack CFLAGS += -I$(LIB_DIR)/libusb_stm32/inc C_SOURCES += $(wildcard $(LIB_DIR)/libusb_stm32/src/*.c) + +# protobuf +CFLAGS += -I$(LIB_DIR)/nanopb +C_SOURCES += $(wildcard $(LIB_DIR)/nanopb/*.c) diff --git a/lib/nanopb b/lib/nanopb new file mode 160000 index 00000000..c9124132 --- /dev/null +++ b/lib/nanopb @@ -0,0 +1 @@ +Subproject commit c9124132a604047d0ef97a09c0e99cd9bed2c818