Kaynağa Gözat

[FL-1946] RPC App launch (#758)

* RPC: Add App start, lock status
 - Add RPC commands Application start, lock status acquiring.
 - Write tests for RPC App system.
 - Replace Unit Tests application with CLI command. This is for CI needs and for tests that run application.
* Fix NDEBUG build
* Update PB submodule
* Fix RPC print (ENABLE DEBUG PRINT!)
* snprintf -> string_t
* Fix Hard Fault (early mutex free)
* printf -> string_t, format, enable tests
* Update submodule: protobuf
* Applications: rollback unit test naming scheme.

Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
Albert Kharisov 4 yıl önce
ebeveyn
işleme
1db29eaea8

+ 10 - 5
applications/applications.c

@@ -20,7 +20,7 @@ extern int32_t desktop_srv(void* p);
 extern int32_t accessor_app(void* p);
 extern int32_t archive_app(void* p);
 extern int32_t blink_test_app(void* p);
-extern int32_t flipper_test_app(void* p);
+extern int32_t delay_test_app(void* p);
 extern int32_t gpio_app(void* p);
 extern int32_t ibutton_app(void* p);
 extern int32_t irda_app(void* p);
@@ -49,6 +49,7 @@ extern void nfc_cli_init();
 extern void storage_cli_init();
 extern void subghz_cli_init();
 extern void power_cli_init();
+extern void unit_tests_cli_init();
 
 // Settings
 extern int32_t notification_settings_app(void* p);
@@ -183,6 +184,10 @@ const FlipperOnStartHook FLIPPER_ON_SYSTEM_START[] = {
 #ifdef SRV_STORAGE
     storage_cli_init,
 #endif
+
+#ifdef APP_UNIT_TESTS
+    unit_tests_cli_init,
+#endif
 };
 
 const size_t FLIPPER_ON_SYSTEM_START_COUNT =
@@ -220,10 +225,6 @@ const FlipperApplication FLIPPER_DEBUG_APPS[] = {
     {.app = usb_test_app, .name = "USB Test", .stack_size = 1024, .icon = &A_Plugins_14},
 #endif
 
-#ifdef APP_UNIT_TESTS
-    {.app = flipper_test_app, .name = "Unit Tests", .stack_size = 1024 * 8, .icon = &A_Plugins_14},
-#endif
-
 #ifdef APP_IRDA_MONITOR
     {.app = irda_monitor_app, .name = "Irda Monitor", .stack_size = 1024, .icon = &A_Plugins_14},
 #endif
@@ -239,6 +240,10 @@ const FlipperApplication FLIPPER_DEBUG_APPS[] = {
 #ifdef SRV_BT
     {.app = bt_debug_app, .name = "Bluetooth Debug", .stack_size = 1024, .icon = NULL},
 #endif
+
+#ifdef APP_UNIT_TESTS
+    {.app = delay_test_app, .name = "Delay Test App", .stack_size = 1024, .icon = &A_Plugins_14},
+#endif
 };
 
 const size_t FLIPPER_DEBUG_APPS_COUNT = sizeof(FLIPPER_DEBUG_APPS) / sizeof(FlipperApplication);

+ 1 - 2
applications/applications.mk

@@ -42,7 +42,6 @@ APP_BLINK	= 1
 APP_IRDA_MONITOR = 1
 APP_KEYPAD_TEST = 1
 APP_SD_TEST	= 1
-APP_UNIT_TESTS = 0
 APP_VIBRO_DEMO = 1
 APP_USB_TEST = 1
 endif
@@ -60,7 +59,7 @@ SRV_GUI		= 1
 endif
 
 
-APP_UNIT_TESTS	?= 0
+APP_UNIT_TESTS ?= 0
 ifeq ($(APP_UNIT_TESTS), 1)
 CFLAGS		+= -DAPP_UNIT_TESTS
 endif

+ 13 - 6
applications/loader/loader.c

@@ -1,3 +1,4 @@
+#include "loader/loader.h"
 #include "loader_i.h"
 
 #define LOADER_THREAD_FLAG_SHOW_MENU (1 << 0)
@@ -56,7 +57,7 @@ static void loader_cli_callback(Cli* cli, string_t args, void* _ctx) {
     furi_thread_start(loader_instance->thread);
 }
 
-bool loader_start(Loader* instance, const char* name, const char* args) {
+LoaderStatus loader_start(Loader* instance, const char* name, const char* args) {
     furi_assert(name);
 
     const FlipperApplication* flipper_app = NULL;
@@ -79,14 +80,15 @@ bool loader_start(Loader* instance, const char* name, const char* args) {
 
     if(!flipper_app) {
         FURI_LOG_E(LOADER_LOG_TAG, "Can't find application with name %s", name);
-        return false;
+        return LoaderStatusErrorUnknownApp;
     }
 
-    loader_lock(instance);
+    bool locked = loader_lock(instance);
 
-    if(furi_thread_get_state(instance->thread) != FuriThreadStateStopped) {
+    if(!locked || (furi_thread_get_state(instance->thread) != FuriThreadStateStopped)) {
         FURI_LOG_E(LOADER_LOG_TAG, "Can't start app. %s is running", instance->current_app->name);
-        return false;
+        /* no need to call loader_unlock() - it is called as soon as application stops */
+        return LoaderStatusErrorAppStarted;
     }
 
     instance->current_app = flipper_app;
@@ -106,7 +108,8 @@ bool loader_start(Loader* instance, const char* name, const char* args) {
     furi_thread_set_context(instance->thread, thread_args);
     furi_thread_set_callback(instance->thread, flipper_app->app);
 
-    return furi_thread_start(instance->thread);
+    bool thread_started = furi_thread_start(instance->thread);
+    return thread_started ? LoaderStatusOk : LoaderStatusErrorInternal;
 }
 
 bool loader_lock(Loader* instance) {
@@ -127,6 +130,10 @@ void loader_unlock(Loader* instance) {
     furi_check(osMutexRelease(instance->mutex) == osOK);
 }
 
+bool loader_is_locked(Loader* instance) {
+    return (instance->lock_semaphore > 0);
+}
+
 static void loader_thread_state_callback(FuriThreadState thread_state, void* context) {
     furi_assert(context);
 

+ 11 - 1
applications/loader/loader.h

@@ -4,12 +4,19 @@
 
 typedef struct Loader Loader;
 
+typedef enum {
+    LoaderStatusOk,
+    LoaderStatusErrorAppStarted,
+    LoaderStatusErrorUnknownApp,
+    LoaderStatusErrorInternal,
+} LoaderStatus;
+
 /** Start application
  * @param name - application name
  * @param args - application arguments
  * @retval true on success
  */
-bool loader_start(Loader* instance, const char* name, const char* args);
+LoaderStatus loader_start(Loader* instance, const char* name, const char* args);
 
 /** Lock application start
  * @retval true on success
@@ -19,5 +26,8 @@ bool loader_lock(Loader* instance);
 /** Unlock application start */
 void loader_unlock(Loader* instance);
 
+/** Get loader lock status */
+bool loader_is_locked(Loader* instance);
+
 /** Show primary loader */
 void loader_show_menu();

+ 83 - 53
applications/rpc/rpc.c

@@ -4,6 +4,7 @@
 #include "furi-hal-delay.h"
 #include "furi/check.h"
 #include "furi/log.h"
+#include <m-string.h>
 #include "pb.h"
 #include "pb_decode.h"
 #include "pb_encode.h"
@@ -42,6 +43,10 @@ static RpcSystemCallbacks rpc_systems[] = {
         .alloc = rpc_system_storage_alloc,
         .free = rpc_system_storage_free,
     },
+    {
+        .alloc = rpc_system_app_alloc,
+        .free = NULL,
+    },
 };
 
 struct RpcSession {
@@ -65,31 +70,28 @@ struct Rpc {
 
 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,
+static size_t rpc_sprintf_msg_file(
+    string_t str,
     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,
+        string_cat_printf(
+            str,
             "%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);
+            string_cat_printf(str, " \'%s\'", msg_file->name);
         }
 
         if(msg_file->data && msg_file->data->size) {
-            cnt += snprintf(
-                str + cnt,
-                str_size - cnt,
+            string_cat_printf(
+                str,
                 " (%d):\'%.*s%s\'",
                 msg_file->data->size,
                 MIN(msg_file->data->size, 30),
@@ -97,23 +99,18 @@ static size_t rpc_sprint_msg_file(
                 msg_file->data->size > 30 ? "..." : "");
         }
 
-        cnt += snprintf(str + cnt, str_size - cnt, "\r\n");
+        string_cat_printf(str, "\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;
+    string_t str;
+    string_init(str);
 
-    cnt += snprintf(
-        str + cnt,
-        sizeof(str) - cnt,
+    string_cat_printf(
+        str,
         "PB_Main: {\r\n\tresult: %d cmd_id: %ld (%s)\r\n",
         message->command_status,
         message->command_id,
@@ -121,88 +118,112 @@ void rpc_print_message(const PB_Main* message) {
     switch(message->which_content) {
     default:
         /* not implemented yet */
-        cnt += ADD_STR(str, cnt, "\tNOT_IMPLEMENTED (%d) {\r\n", message->which_content);
+        string_cat_printf(str, "\tNOT_IMPLEMENTED (%d) {\r\n", message->which_content);
+        break;
+    case PB_Main_app_start_tag: {
+        string_cat_printf(str, "\tapp_start {\r\n");
+        const char* name = message->content.app_start.name;
+        const char* args = message->content.app_start.args;
+        if(name) {
+            string_cat_printf(str, "\t\tname: %s\r\n", name);
+        }
+        if(args) {
+            string_cat_printf(str, "\t\targs: %s\r\n", args);
+        }
+        break;
+    }
+    case PB_Main_app_lock_status_request_tag: {
+        string_cat_printf(str, "\tapp_lock_status_request {\r\n");
         break;
+    }
+    case PB_Main_app_lock_status_response_tag: {
+        string_cat_printf(str, "\tapp_lock_status_response {\r\n");
+        bool lock_status = message->content.app_lock_status_response.locked;
+        string_cat_printf(str, "\t\tlocked: %s\r\n", lock_status ? "true" : "false");
+        break;
+    }
     case PB_Main_storage_md5sum_request_tag: {
-        cnt += ADD_STR(str, cnt, "\tmd5sum_request {\r\n");
+        string_cat_printf(str, "\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);
+            string_cat_printf(str, "\t\tpath: %s\r\n", path);
         }
         break;
     }
     case PB_Main_storage_md5sum_response_tag: {
-        cnt += ADD_STR(str, cnt, "\tmd5sum_response {\r\n");
+        string_cat_printf(str, "\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);
+            string_cat_printf(str, "\t\tmd5sum: %s\r\n", path);
         }
         break;
     }
     case PB_Main_ping_request_tag:
-        cnt += ADD_STR(str, cnt, "\tping_request {\r\n");
+        string_cat_printf(str, "\tping_request {\r\n");
         break;
     case PB_Main_ping_response_tag:
-        cnt += ADD_STR(str, cnt, "\tping_response {\r\n");
+        string_cat_printf(str, "\tping_response {\r\n");
         break;
     case PB_Main_storage_mkdir_request_tag:
-        cnt += ADD_STR(str, cnt, "\tmkdir {\r\n");
+        string_cat_printf(str, "\tmkdir {\r\n");
         break;
     case PB_Main_storage_delete_request_tag: {
-        cnt += ADD_STR(str, cnt, "\tdelete {\r\n");
+        string_cat_printf(str, "\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);
+            string_cat_printf(str, "\t\tpath: %s\r\n", path);
         }
         break;
     }
     case PB_Main_empty_tag:
-        cnt += ADD_STR(str, cnt, "\tempty {\r\n");
+        string_cat_printf(str, "\tempty {\r\n");
         break;
     case PB_Main_storage_list_request_tag: {
-        cnt += ADD_STR(str, cnt, "\tlist_request {\r\n");
+        string_cat_printf(str, "\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);
+            string_cat_printf(str, "\t\tpath: %s\r\n", path);
         }
         break;
     }
     case PB_Main_storage_read_request_tag: {
-        cnt += ADD_STR(str, cnt, "\tread_request {\r\n");
+        string_cat_printf(str, "\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);
+            string_cat_printf(str, "\t\tpath: %s\r\n", path);
         }
         break;
     }
     case PB_Main_storage_write_request_tag: {
-        cnt += ADD_STR(str, cnt, "\twrite_request {\r\n");
+        string_cat_printf(str, "\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);
+            string_cat_printf(str, "\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);
+            rpc_sprintf_msg_file(str, "\t\t\t", msg_file, 1);
         }
         break;
     }
     case PB_Main_storage_read_response_tag:
-        cnt += ADD_STR(str, cnt, "\tread_response {\r\n");
+        string_cat_printf(str, "\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);
+            rpc_sprintf_msg_file(str, "\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);
+        string_cat_printf(str, "\tlist_response {\r\n");
+        rpc_sprintf_msg_file(str, "\t\t", msg_file, msg_file_count);
     }
     }
-    cnt += ADD_STR(str, cnt, "\t}\r\n}\r\n");
-    printf("%s", str);
+    string_cat_printf(str, "\t}\r\n}\r\n");
+    printf("%s", string_get_cstr(str));
+
+    string_clear(str);
 }
 
 static Rpc* rpc_alloc(void) {
@@ -253,7 +274,6 @@ void rpc_close_session(RpcSession* 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);
 }
@@ -328,22 +348,31 @@ void rpc_encode_and_send(Rpc* rpc, PB_Main* main_message) {
     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);
+        string_t str;
+        string_init(str);
+        string_reserve(str, 100 + ostream.bytes_written * 5);
+
+        string_cat_printf(str, "\r\nREPONSE DEC(%d): {", ostream.bytes_written);
         for(int i = 0; i < ostream.bytes_written; ++i) {
-            printf("%d, ", buffer[i]);
+            string_cat_printf(str, "%d, ", buffer[i]);
         }
-        printf("}\r\n");
+        string_cat_printf(str, "}\r\n");
+
+        printf("%s", string_get_cstr(str));
+        string_clean(str);
+        string_reserve(str, 100 + ostream.bytes_written * 3);
 
-        printf("REPONSE HEX(%d): {", ostream.bytes_written);
+        string_cat_printf(str, "REPONSE HEX(%d): {", ostream.bytes_written);
         for(int i = 0; i < ostream.bytes_written; ++i) {
-            printf("%02X", buffer[i]);
+            string_cat_printf(str, "%02X", buffer[i]);
         }
-        printf("}\r\n\r\n");
+        string_cat_printf(str, "}\r\n\r\n");
+
+        printf("%s", string_get_cstr(str));
 #endif // DEBUG_PRINT
 
+        osMutexAcquire(session->send_bytes_mutex, osWaitForever);
         if(session->send_bytes_callback) {
             session->send_bytes_callback(
                 session->send_bytes_context, buffer, ostream.bytes_written);
@@ -408,6 +437,7 @@ int32_t rpc_srv(void* p) {
                     }
                 }
                 free(session->system_contexts);
+                osMutexDelete(session->send_bytes_mutex);
                 RpcHandlerDict_clean(rpc->handlers);
                 rpc->busy = false;
             } else {

+ 78 - 0
applications/rpc/rpc_app.c

@@ -0,0 +1,78 @@
+#include "flipper.pb.h"
+#include "furi/record.h"
+#include "status.pb.h"
+#include "rpc_i.h"
+#include <furi.h>
+#include <loader/loader.h>
+
+void rpc_system_app_start_process(const PB_Main* request, void* context) {
+    Rpc* rpc = context;
+    furi_assert(rpc);
+    furi_assert(request);
+    furi_assert(request->which_content == PB_Main_app_start_tag);
+    PB_CommandStatus result = PB_CommandStatus_ERROR_APP_CANT_START;
+
+    Loader* loader = furi_record_open("loader");
+    const char* app_name = request->content.app_start.name;
+    if(app_name) {
+        const char* app_args = request->content.app_start.args;
+        LoaderStatus status = loader_start(loader, app_name, app_args);
+        if(status == LoaderStatusErrorAppStarted) {
+            result = PB_CommandStatus_ERROR_APP_SYSTEM_LOCKED;
+        } else if(status == LoaderStatusErrorInternal) {
+            result = PB_CommandStatus_ERROR_APP_CANT_START;
+        } else if(status == LoaderStatusErrorUnknownApp) {
+            result = PB_CommandStatus_ERROR_INVALID_PARAMETERS;
+        } else if(status == LoaderStatusOk) {
+            result = PB_CommandStatus_OK;
+        } else {
+            furi_assert(0);
+        }
+    } else {
+        result = PB_CommandStatus_ERROR_INVALID_PARAMETERS;
+    }
+
+    furi_record_close("loader");
+
+    rpc_encode_and_send_empty(rpc, request->command_id, result);
+}
+
+void rpc_system_app_lock_status_process(const PB_Main* request, void* context) {
+    Rpc* rpc = context;
+    furi_assert(rpc);
+    furi_assert(request);
+    furi_assert(request->which_content == PB_Main_app_lock_status_request_tag);
+
+    Loader* loader = furi_record_open("loader");
+
+    PB_Main response = {
+        .has_next = false,
+        .command_status = PB_CommandStatus_OK,
+        .command_id = request->command_id,
+        .which_content = PB_Main_app_lock_status_response_tag,
+    };
+
+    response.content.app_lock_status_response.locked = loader_is_locked(loader);
+
+    furi_record_close("loader");
+
+    rpc_encode_and_send(rpc, &response);
+}
+
+void* rpc_system_app_alloc(Rpc* rpc) {
+    furi_assert(rpc);
+
+    RpcHandler rpc_handler = {
+        .message_handler = NULL,
+        .decode_submessage = NULL,
+        .context = rpc,
+    };
+
+    rpc_handler.message_handler = rpc_system_app_start_process;
+    rpc_add_handler(rpc, PB_Main_app_start_tag, &rpc_handler);
+
+    rpc_handler.message_handler = rpc_system_app_lock_status_process;
+    rpc_add_handler(rpc, PB_Main_app_lock_status_request_tag, &rpc_handler);
+
+    return NULL;
+}

+ 2 - 0
applications/rpc/rpc_i.h

@@ -22,4 +22,6 @@ 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_system_app_alloc(Rpc* rpc);
+
 void rpc_print_message(const PB_Main* message);

+ 1 - 0
applications/rpc/rpc_storage.c

@@ -340,6 +340,7 @@ static void rpc_system_storage_md5sum_process(const PB_Main* request, void* cont
 
         char* md5sum = response.content.storage_md5sum_response.md5sum;
         size_t md5sum_size = sizeof(response.content.storage_md5sum_response.md5sum);
+        (void)md5sum_size;
         furi_assert(hash_size <= ((md5sum_size - 1) / 2));
         for(uint8_t i = 0; i < hash_size; i++) {
             md5sum += sprintf(md5sum, "%02x", hash[i]);

+ 1 - 1
applications/subghz/helpers/subghz_frequency_analyzer_worker.c

@@ -41,7 +41,7 @@ static uint32_t subghz_frequency_analyzer_worker_expRunningAverageAdaptive(
 static int32_t subghz_frequency_analyzer_worker_thread(void* context) {
     SubGhzFrequencyAnalyzerWorker* instance = context;
 
-    FrequencyRSSI frequency_rssi;
+    FrequencyRSSI frequency_rssi = {.frequency = 0, .rssi = 0};
     float rssi;
     uint32_t frequency;
     uint32_t frequency_start;

+ 137 - 1
applications/tests/rpc/rpc_test.c

@@ -14,6 +14,8 @@
 #include <pb_encode.h>
 #include <m-list.h>
 #include <lib/toolbox/md5.h>
+#include <cli/cli.h>
+#include <loader/loader.h>
 
 LIST_DEF(MsgList, PB_Main, M_POD_OPLIST)
 #define M_OPL_MsgList_t() LIST_OPLIST(MsgList)
@@ -108,6 +110,7 @@ static void clean_directory(Storage* fs_api, const char* clean_dir) {
         free(name);
     } else {
         FS_Error error = storage_common_mkdir(fs_api, clean_dir);
+        (void)error;
         furi_assert(error == FSE_OK);
     }
 
@@ -172,6 +175,7 @@ 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);
+    (void)bytes_sent;
     furi_assert(bytes_sent == got_size);
 }
 
@@ -346,6 +350,12 @@ static void test_rpc_compare_messages(PB_Main* result, PB_Main* expected) {
         /* rpc doesn't send it */
         mu_check(0);
         break;
+    case PB_Main_app_lock_status_response_tag: {
+        bool result_locked = result->content.app_lock_status_response.locked;
+        bool expected_locked = expected->content.app_lock_status_response.locked;
+        mu_check(result_locked == expected_locked);
+        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;
@@ -588,6 +598,7 @@ static void test_storage_read_run(const char* path, uint32_t command_id) {
 static void test_create_dir(const char* path) {
     Storage* fs_api = furi_record_open("storage");
     FS_Error error = storage_common_mkdir(fs_api, path);
+    (void)error;
     furi_assert((error == FSE_OK) || (error == FSE_EXIST));
     furi_record_close("storage");
 }
@@ -717,7 +728,7 @@ MU_TEST(test_storage_write) {
         ++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);
+    test_storage_write_run(TEST_DIR "test2.txt", 512, 3, ++command_id, PB_CommandStatus_OK);
 }
 
 MU_TEST(test_storage_interrupt_continuous_same_system) {
@@ -866,6 +877,7 @@ MU_TEST(test_storage_mkdir) {
 
     Storage* fs_api = furi_record_open("storage");
     FS_Error error = storage_common_remove(fs_api, TEST_DIR "dir1");
+    (void)error;
     furi_assert(error == FSE_OK);
     furi_record_close("storage");
 
@@ -1018,10 +1030,134 @@ MU_TEST_SUITE(test_rpc_storage) {
     MU_RUN_TEST(test_storage_interrupt_continuous_another_system);
 }
 
+static void test_app_create_request(
+    PB_Main* request,
+    const char* app_name,
+    const char* app_args,
+    uint32_t command_id) {
+    request->command_id = command_id;
+    request->command_status = PB_CommandStatus_OK;
+    request->cb_content.funcs.encode = NULL;
+    request->which_content = PB_Main_app_start_tag;
+    request->has_next = false;
+
+    if(app_name) {
+        char* msg_app_name = furi_alloc(strlen(app_name) + 1);
+        strcpy(msg_app_name, app_name);
+        request->content.app_start.name = msg_app_name;
+    } else {
+        request->content.app_start.name = NULL;
+    }
+
+    if(app_args) {
+        char* msg_app_args = furi_alloc(strlen(app_args) + 1);
+        strcpy(msg_app_args, app_args);
+        request->content.app_start.args = msg_app_args;
+    } else {
+        request->content.app_start.args = NULL;
+    }
+}
+
+static void test_app_start_run(
+    const char* app_name,
+    const char* app_args,
+    PB_CommandStatus status,
+    uint32_t command_id) {
+    PB_Main request;
+    MsgList_t expected_msg_list;
+    MsgList_init(expected_msg_list);
+
+    test_app_create_request(&request, app_name, app_args, 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);
+}
+
+static void test_app_get_status_lock_run(bool locked_expected, uint32_t command_id) {
+    PB_Main request = {
+        .command_id = command_id,
+        .command_status = PB_CommandStatus_OK,
+        .which_content = PB_Main_app_lock_status_request_tag,
+        .has_next = false,
+    };
+
+    MsgList_t expected_msg_list;
+    MsgList_init(expected_msg_list);
+    PB_Main* response = MsgList_push_new(expected_msg_list);
+    response->command_id = command_id;
+    response->command_status = PB_CommandStatus_OK;
+    response->which_content = PB_Main_app_lock_status_response_tag;
+    response->has_next = false;
+    response->content.app_lock_status_response.locked = locked_expected;
+
+    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_app_start_and_lock_status) {
+    test_app_get_status_lock_run(false, ++command_id);
+    test_app_start_run(NULL, "/ext/file", PB_CommandStatus_ERROR_INVALID_PARAMETERS, ++command_id);
+    test_app_start_run(NULL, NULL, PB_CommandStatus_ERROR_INVALID_PARAMETERS, ++command_id);
+    test_app_get_status_lock_run(false, ++command_id);
+    test_app_start_run(
+        "skynet_destroy_world_app", NULL, PB_CommandStatus_ERROR_INVALID_PARAMETERS, ++command_id);
+    test_app_get_status_lock_run(false, ++command_id);
+
+    test_app_start_run("Delay Test App", "0", PB_CommandStatus_OK, ++command_id);
+    delay(100);
+    test_app_get_status_lock_run(false, ++command_id);
+
+    test_app_start_run("Delay Test App", "200", PB_CommandStatus_OK, ++command_id);
+    test_app_get_status_lock_run(true, ++command_id);
+    delay(100);
+    test_app_get_status_lock_run(true, ++command_id);
+    test_app_start_run(
+        "Delay Test App", "0", PB_CommandStatus_ERROR_APP_SYSTEM_LOCKED, ++command_id);
+    delay(200);
+    test_app_get_status_lock_run(false, ++command_id);
+
+    test_app_start_run("Delay Test App", "500", PB_CommandStatus_OK, ++command_id);
+    delay(100);
+    test_app_get_status_lock_run(true, ++command_id);
+    test_app_start_run("Infrared", "0", PB_CommandStatus_ERROR_APP_SYSTEM_LOCKED, ++command_id);
+    delay(100);
+    test_app_get_status_lock_run(true, ++command_id);
+    test_app_start_run(
+        "2_girls_1_app", "0", PB_CommandStatus_ERROR_INVALID_PARAMETERS, ++command_id);
+    delay(100);
+    test_app_get_status_lock_run(true, ++command_id);
+    delay(500);
+    test_app_get_status_lock_run(false, ++command_id);
+}
+
+MU_TEST_SUITE(test_rpc_app) {
+    MU_SUITE_CONFIGURE(&test_rpc_storage_setup, &test_rpc_storage_teardown);
+
+    MU_RUN_TEST(test_app_start_and_lock_status);
+}
+
 int run_minunit_test_rpc() {
     MU_RUN_SUITE(test_rpc_storage);
     MU_RUN_SUITE(test_rpc_status);
+    MU_RUN_SUITE(test_rpc_app);
     MU_REPORT();
 
     return MU_EXIT_CODE;
 }
+
+int32_t delay_test_app(void* p) {
+    int timeout = atoi((const char*)p);
+
+    if(timeout > 0) {
+        delay(timeout);
+    }
+
+    return 0;
+}

+ 33 - 15
applications/tests/test_index.c

@@ -1,33 +1,51 @@
+#include "m-string.h"
 #include <stdio.h>
 #include <furi.h>
 #include <furi-hal.h>
 #include "minunit_vars.h"
 #include <notification/notification-messages.h>
+#include <cli/cli.h>
+#include <loader/loader.h>
 
 int run_minunit();
 int run_minunit_test_irda_decoder_encoder();
 int run_minunit_test_rpc();
 
-int32_t flipper_test_app(void* p) {
+void unit_tests_cli(Cli* cli, string_t args, void* context) {
     uint32_t test_result = 0;
+    minunit_run = 0;
+    minunit_assert = 0;
+    minunit_fail = 0;
+    minunit_status = 0;
 
-    NotificationApp* notification = furi_record_open("notification");
-
-    notification_message_block(notification, &sequence_set_only_blue_255);
+    Loader* loader = furi_record_open("loader");
+    furi_record_close("loader");
 
-    //    test_result |= run_minunit();     // disabled as it fails randomly
-    test_result |= run_minunit_test_irda_decoder_encoder();
-    test_result |= run_minunit_test_rpc();
+    NotificationApp* notification = furi_record_open("notification");
+    furi_record_close("notification");
 
-    if(test_result == 0) {
-        // test passed
-        notification_message(notification, &sequence_success);
+    if(loader_is_locked(loader)) {
+        FURI_LOG_E("UNIT_TESTS", "RPC: stop all applications to run tests");
+        notification_message(notification, &sequence_blink_magenta_100);
     } else {
-        // test failed
-        notification_message(notification, &sequence_error);
+        notification_message_block(notification, &sequence_set_only_blue_255);
+
+        test_result |= run_minunit();
+        test_result |= run_minunit_test_irda_decoder_encoder();
+        test_result |= run_minunit_test_rpc();
+
+        if(test_result == 0) {
+            notification_message(notification, &sequence_success);
+            FURI_LOG_I("UNIT_TESTS", "PASSED");
+        } else {
+            notification_message(notification, &sequence_error);
+            FURI_LOG_E("UNIT_TESTS", "FAILED");
+        }
     }
+}
 
-    furi_record_close("notification");
-
-    return 0;
+void unit_tests_cli_init() {
+    Cli* cli = furi_record_open("cli");
+    cli_add_command(cli, "unit_tests", CliCommandFlagParallelSafe, unit_tests_cli, NULL);
+    furi_record_close("cli");
 }

+ 18 - 0
assets/compiled/application.pb.c

@@ -0,0 +1,18 @@
+/* Automatically generated nanopb constant definitions */
+/* Generated by nanopb-0.4.5 */
+
+#include "application.pb.h"
+#if PB_PROTO_HEADER_VERSION != 40
+#error Regenerate this file with the current version of nanopb generator.
+#endif
+
+PB_BIND(PB_App_Start, PB_App_Start, AUTO)
+
+
+PB_BIND(PB_App_LockStatusRequest, PB_App_LockStatusRequest, AUTO)
+
+
+PB_BIND(PB_App_LockStatusResponse, PB_App_LockStatusResponse, AUTO)
+
+
+

+ 79 - 0
assets/compiled/application.pb.h

@@ -0,0 +1,79 @@
+/* Automatically generated nanopb header */
+/* Generated by nanopb-0.4.5 */
+
+#ifndef PB_PB_APP_APPLICATION_PB_H_INCLUDED
+#define PB_PB_APP_APPLICATION_PB_H_INCLUDED
+#include <pb.h>
+
+#if PB_PROTO_HEADER_VERSION != 40
+#error Regenerate this file with the current version of nanopb generator.
+#endif
+
+/* Struct definitions */
+typedef struct _PB_App_LockStatusRequest { 
+    char dummy_field;
+} PB_App_LockStatusRequest;
+
+typedef struct _PB_App_Start { 
+    char *name; 
+    char *args; 
+} PB_App_Start;
+
+typedef struct _PB_App_LockStatusResponse { 
+    bool locked; 
+} PB_App_LockStatusResponse;
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Initializer values for message structs */
+#define PB_App_Start_init_default                {NULL, NULL}
+#define PB_App_LockStatusRequest_init_default    {0}
+#define PB_App_LockStatusResponse_init_default   {0}
+#define PB_App_Start_init_zero                   {NULL, NULL}
+#define PB_App_LockStatusRequest_init_zero       {0}
+#define PB_App_LockStatusResponse_init_zero      {0}
+
+/* Field tags (for use in manual encoding/decoding) */
+#define PB_App_Start_name_tag                    1
+#define PB_App_Start_args_tag                    2
+#define PB_App_LockStatusResponse_locked_tag     1
+
+/* Struct field encoding specification for nanopb */
+#define PB_App_Start_FIELDLIST(X, a) \
+X(a, POINTER,  SINGULAR, STRING,   name,              1) \
+X(a, POINTER,  SINGULAR, STRING,   args,              2)
+#define PB_App_Start_CALLBACK NULL
+#define PB_App_Start_DEFAULT NULL
+
+#define PB_App_LockStatusRequest_FIELDLIST(X, a) \
+
+#define PB_App_LockStatusRequest_CALLBACK NULL
+#define PB_App_LockStatusRequest_DEFAULT NULL
+
+#define PB_App_LockStatusResponse_FIELDLIST(X, a) \
+X(a, STATIC,   SINGULAR, BOOL,     locked,            1)
+#define PB_App_LockStatusResponse_CALLBACK NULL
+#define PB_App_LockStatusResponse_DEFAULT NULL
+
+extern const pb_msgdesc_t PB_App_Start_msg;
+extern const pb_msgdesc_t PB_App_LockStatusRequest_msg;
+extern const pb_msgdesc_t PB_App_LockStatusResponse_msg;
+
+/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
+#define PB_App_Start_fields &PB_App_Start_msg
+#define PB_App_LockStatusRequest_fields &PB_App_LockStatusRequest_msg
+#define PB_App_LockStatusResponse_fields &PB_App_LockStatusResponse_msg
+
+/* Maximum encoded size of messages (where known) */
+/* PB_App_Start_size depends on runtime parameters */
+#define PB_App_LockStatusRequest_size            0
+#define PB_App_LockStatusResponse_size           2
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif

+ 21 - 6
assets/compiled/flipper.pb.h

@@ -6,6 +6,7 @@
 #include <pb.h>
 #include "storage.pb.h"
 #include "status.pb.h"
+#include "application.pb.h"
 
 #if PB_PROTO_HEADER_VERSION != 40
 #error Regenerate this file with the current version of nanopb generator.
@@ -28,7 +29,9 @@ typedef enum _PB_CommandStatus {
     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_ERROR_STORAGE_ALREADY_OPEN = 13, /* *< File/Dir already opened */
+    PB_CommandStatus_ERROR_APP_CANT_START = 16, /* *< Can't start app - either wrong name, or internal error */
+    PB_CommandStatus_ERROR_APP_SYSTEM_LOCKED = 17 /* *<  Another app is running */
 } PB_CommandStatus;
 
 /* Struct definitions */
@@ -58,14 +61,17 @@ typedef struct _PB_Main {
         PB_Storage_MkdirRequest storage_mkdir_request;
         PB_Storage_Md5sumRequest storage_md5sum_request;
         PB_Storage_Md5sumResponse storage_md5sum_response;
+        PB_App_Start app_start;
+        PB_App_LockStatusRequest app_lock_status_request;
+        PB_App_LockStatusResponse app_lock_status_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))
+#define _PB_CommandStatus_MAX PB_CommandStatus_ERROR_APP_SYSTEM_LOCKED
+#define _PB_CommandStatus_ARRAYSIZE ((PB_CommandStatus)(PB_CommandStatus_ERROR_APP_SYSTEM_LOCKED+1))
 
 
 #ifdef __cplusplus
@@ -94,6 +100,9 @@ extern "C" {
 #define PB_Main_storage_mkdir_request_tag        13
 #define PB_Main_storage_md5sum_request_tag       14
 #define PB_Main_storage_md5sum_response_tag      15
+#define PB_Main_app_start_tag                    16
+#define PB_Main_app_lock_status_request_tag      17
+#define PB_Main_app_lock_status_response_tag     18
 
 /* Struct field encoding specification for nanopb */
 #define PB_Empty_FIELDLIST(X, a) \
@@ -116,7 +125,10 @@ X(a, STATIC,   ONEOF,    MSG_W_CB, (content,storage_write_request,content.storag
 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)
+X(a, STATIC,   ONEOF,    MSG_W_CB, (content,storage_md5sum_response,content.storage_md5sum_response),  15) \
+X(a, STATIC,   ONEOF,    MSG_W_CB, (content,app_start,content.app_start),  16) \
+X(a, STATIC,   ONEOF,    MSG_W_CB, (content,app_lock_status_request,content.app_lock_status_request),  17) \
+X(a, STATIC,   ONEOF,    MSG_W_CB, (content,app_lock_status_response,content.app_lock_status_response),  18)
 #define PB_Main_CALLBACK NULL
 #define PB_Main_DEFAULT NULL
 #define PB_Main_content_empty_MSGTYPE PB_Empty
@@ -131,6 +143,9 @@ X(a, STATIC,   ONEOF,    MSG_W_CB, (content,storage_md5sum_response,content.stor
 #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
+#define PB_Main_content_app_start_MSGTYPE PB_App_Start
+#define PB_Main_content_app_lock_status_request_MSGTYPE PB_App_LockStatusRequest
+#define PB_Main_content_app_lock_status_response_MSGTYPE PB_App_LockStatusResponse
 
 extern const pb_msgdesc_t PB_Empty_msg;
 extern const pb_msgdesc_t PB_Main_msg;
@@ -141,9 +156,9 @@ extern const pb_msgdesc_t 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)
+#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) && defined(PB_App_Start_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];};
+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 f16[(7 + PB_App_Start_size)]; char f0[36];};
 #endif
 
 #ifdef __cplusplus

+ 1 - 1
assets/protobuf

@@ -1 +1 @@
-Subproject commit 41599b8e6a6b33a229e8f5fa58de1a2cfcc8184a
+Subproject commit 8e6db414beed5aff0902f2cca2f4146a0dffb7a1

+ 1 - 1
lib/subghz/protocols/subghz_protocol_came_twee.c

@@ -56,7 +56,7 @@ void subghz_protocol_came_twee_free(SubGhzProtocolCameTwee* instance) {
 LevelDuration subghz_protocol_came_twee_add_duration_to_upload(
     SubGhzProtocolCameTwee* instance,
     ManchesterEncoderResult result) {
-    LevelDuration data;
+    LevelDuration data = {.duration = 0, .level = 0};
     switch(result) {
     case ManchesterEncoderResultShortLow:
         data.duration = instance->common.te_short;