 95737958ad
			
		
	
	
		95737958ad
		
			
		
	
	
	
	
		
			
			* ApiSymbols: add furi_record_destroy * FuriHal: cleanup serial API, add logging configuration in RTC * FuriHal: hide private part in _i header. Toolbox: cleanup value index. SystemSettings: logging device and baudrate. * FuriHal: RTC logging method documentation * Synchronize API Symbols * Furi: mark HEAP_PRINT_DEBUG as broken * FuriHal: furi_hal_serial, add custom IRQ func * Fix PR review issues * Implement basic external module detection and echo * Update api symbols for f18 * Minimally working implementation (can create directory via rpc) * Make expansion protocol parser a header-only library * Rename a function * Improve thread syncronisation * Implement multi-packet transmissions * Improve test application * Clean up expansion worker code * Send heartbeat when host is ready * Update API symbols * Add draft documentation * Expansion worker: proper timeout and error handling * Expansion worker: correct TX, do not disable expansion callback * Expansion protocol: pc side test script * PC side expansion test: trying to change baudrate * Working comms between 2 flippers * Cleaner exit from expansion worker thread * Better checks * Add debug logs * Remove unneeded delays * Use USART as default expansion port * Refactor furi_hal_serial_control, fix crash * Improve furi_hal abstraction, wait for stable rx pin * Remove rogue include * Set proper exit reason on RPC error * Remove rogue comment * Remove RX stability check as potentially problematic * Improve expansion_test application * Remove rogue define * Give up on TODO * Implement expansion protocol checksum support * Update ExpansionModules.md * RPC: reverse input * Assets: sync protobuf * Fix typos * FuriHal: UART add reception DMA (#3220) * FuriHal: add DMA serial rx mode * usb_uart_bridge: switch to working with DMA * Sync api symbol versions * FuriHal: update serial docs and api * FuriHal: Selial added similar API for simple reception mode as with DMA * FuriHal: Update API target H18 * API: ver API H7 * FuriHal: Serial error processing * FuriHal: fix furi_hal_serial set baudrate * Sync api symbols * FuriHal: cleanup serial isr and various flag handling procedures * FuriHal: cleanup and simplify serial API * Debug: update UART Echo serial related flags * FuriHal: update serial API symbols naming * Make expansion_test compile * Remove unneeded file * Make PVS-studio happy * Optimise stack usage * Optimise heap usage, improve api signature * Fix typo * Clean up code * Update expansion_protocol.h * Fix unit tests * Add doxygen comments to expansion.h * Update/add doxygen comments * Update ExpansionModules.md * Github: new global code owner * FuriHal: naming in serial control * Expansion: check mutex acquire return result Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com> Co-authored-by: hedger <hedger@users.noreply.github.com> Co-authored-by: SkorP <skorpionm@yandex.ru> Co-authored-by: SG <who.just.the.doctor@gmail.com> Co-authored-by: Skorpionm <85568270+Skorpionm@users.noreply.github.com>
		
			
				
	
	
		
			455 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			455 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /**
 | |
|  * @file expansion_test.c
 | |
|  * @brief Expansion module support testing application.
 | |
|  *
 | |
|  * Before running, connect pins using the following scheme:
 | |
|  * 13 -> 16 (USART TX to LPUART RX)
 | |
|  * 14 -> 15 (USART RX to LPUART TX)
 | |
|  *
 | |
|  * What this application does:
 | |
|  *
 | |
|  * - Enables module support and emulates the module on a single device
 | |
|  *   (hence the above connection),
 | |
|  * - Connects to the expansion module service, sets baud rate,
 | |
|  * - Starts the RPC session,
 | |
|  * - Creates a directory at `/ext/ExpansionTest` and writes a file
 | |
|  *   named `test.txt` under it,
 | |
|  * - Plays an audiovisual alert (sound and blinking display),
 | |
|  * - Waits 10 cycles of idle loop,
 | |
|  * - Stops the RPC session,
 | |
|  * - Waits another 10 cycles of idle loop,
 | |
|  * - Exits (plays a sound if any of the above steps failed).
 | |
|  */
 | |
| #include <furi.h>
 | |
| 
 | |
| #include <furi_hal_resources.h>
 | |
| 
 | |
| #include <furi_hal_serial.h>
 | |
| #include <furi_hal_serial_control.h>
 | |
| 
 | |
| #include <pb.h>
 | |
| #include <pb_decode.h>
 | |
| #include <pb_encode.h>
 | |
| 
 | |
| #include <flipper.pb.h>
 | |
| 
 | |
| #include <storage/storage.h>
 | |
| #include <expansion/expansion.h>
 | |
| #include <notification/notification_messages.h>
 | |
| #include <expansion/expansion_protocol.h>
 | |
| 
 | |
| #define TAG "ExpansionTest"
 | |
| 
 | |
| #define TEST_DIR_PATH EXT_PATH(TAG)
 | |
| #define TEST_FILE_NAME "test.txt"
 | |
| #define TEST_FILE_PATH EXT_PATH(TAG "/" TEST_FILE_NAME)
 | |
| 
 | |
| #define HOST_SERIAL_ID (FuriHalSerialIdLpuart)
 | |
| #define MODULE_SERIAL_ID (FuriHalSerialIdUsart)
 | |
| 
 | |
| #define RECEIVE_BUFFER_SIZE (sizeof(ExpansionFrame) + sizeof(ExpansionFrameChecksum))
 | |
| 
 | |
| typedef enum {
 | |
|     ExpansionTestAppFlagData = 1U << 0,
 | |
|     ExpansionTestAppFlagExit = 1U << 1,
 | |
| } ExpansionTestAppFlag;
 | |
| 
 | |
| #define EXPANSION_TEST_APP_ALL_FLAGS (ExpansionTestAppFlagData | ExpansionTestAppFlagExit)
 | |
| 
 | |
| typedef struct {
 | |
|     FuriThreadId thread_id;
 | |
|     Expansion* expansion;
 | |
|     FuriHalSerialHandle* handle;
 | |
|     FuriStreamBuffer* buf;
 | |
|     ExpansionFrame frame;
 | |
|     PB_Main msg;
 | |
|     Storage* storage;
 | |
| } ExpansionTestApp;
 | |
| 
 | |
| static void expansion_test_app_serial_rx_callback(
 | |
|     FuriHalSerialHandle* handle,
 | |
|     FuriHalSerialRxEvent event,
 | |
|     void* context) {
 | |
|     furi_assert(handle);
 | |
|     furi_assert(context);
 | |
|     ExpansionTestApp* app = context;
 | |
| 
 | |
|     if(event == FuriHalSerialRxEventData) {
 | |
|         const uint8_t data = furi_hal_serial_async_rx(handle);
 | |
|         furi_stream_buffer_send(app->buf, &data, sizeof(data), 0);
 | |
|         furi_thread_flags_set(app->thread_id, ExpansionTestAppFlagData);
 | |
|     }
 | |
| }
 | |
| 
 | |
| static ExpansionTestApp* expansion_test_app_alloc() {
 | |
|     ExpansionTestApp* instance = malloc(sizeof(ExpansionTestApp));
 | |
|     instance->buf = furi_stream_buffer_alloc(RECEIVE_BUFFER_SIZE, 1);
 | |
|     return instance;
 | |
| }
 | |
| 
 | |
| static void expansion_test_app_free(ExpansionTestApp* instance) {
 | |
|     furi_stream_buffer_free(instance->buf);
 | |
|     free(instance);
 | |
| }
 | |
| 
 | |
| static void expansion_test_app_start(ExpansionTestApp* instance) {
 | |
|     instance->thread_id = furi_thread_get_current_id();
 | |
|     instance->expansion = furi_record_open(RECORD_EXPANSION);
 | |
|     instance->handle = furi_hal_serial_control_acquire(MODULE_SERIAL_ID);
 | |
|     // Configure the serial port
 | |
|     furi_hal_serial_init(instance->handle, EXPANSION_PROTOCOL_DEFAULT_BAUD_RATE);
 | |
|     // Start waiting for the initial pulse
 | |
|     expansion_enable(instance->expansion, HOST_SERIAL_ID);
 | |
| 
 | |
|     furi_hal_serial_async_rx_start(
 | |
|         instance->handle, expansion_test_app_serial_rx_callback, instance, false);
 | |
| }
 | |
| 
 | |
| static void expansion_test_app_stop(ExpansionTestApp* instance) {
 | |
|     // Give back the module handle
 | |
|     furi_hal_serial_control_release(instance->handle);
 | |
|     // Turn expansion module support off
 | |
|     expansion_disable(instance->expansion);
 | |
|     furi_record_close(RECORD_EXPANSION);
 | |
| }
 | |
| 
 | |
| static inline bool expansion_test_app_is_success_response(const ExpansionFrame* response) {
 | |
|     return response->header.type == ExpansionFrameTypeStatus &&
 | |
|            response->content.status.error == ExpansionFrameErrorNone;
 | |
| }
 | |
| 
 | |
| static inline bool expansion_test_app_is_success_rpc_message(const PB_Main* message) {
 | |
|     return (message->command_status == PB_CommandStatus_OK ||
 | |
|             message->command_status == PB_CommandStatus_ERROR_STORAGE_EXIST) &&
 | |
|            (message->which_content == PB_Main_empty_tag);
 | |
| }
 | |
| 
 | |
| static size_t expansion_test_app_receive_callback(uint8_t* data, size_t data_size, void* context) {
 | |
|     ExpansionTestApp* instance = context;
 | |
| 
 | |
|     size_t received_size = 0;
 | |
| 
 | |
|     while(true) {
 | |
|         received_size += furi_stream_buffer_receive(
 | |
|             instance->buf, data + received_size, data_size - received_size, 0);
 | |
|         if(received_size == data_size) break;
 | |
| 
 | |
|         const uint32_t flags = furi_thread_flags_wait(
 | |
|             EXPANSION_TEST_APP_ALL_FLAGS, FuriFlagWaitAny, EXPANSION_PROTOCOL_TIMEOUT_MS);
 | |
| 
 | |
|         // Exit on any error
 | |
|         if(flags & FuriFlagError) break;
 | |
|     }
 | |
| 
 | |
|     return received_size;
 | |
| }
 | |
| 
 | |
| static size_t
 | |
|     expansion_test_app_send_callback(const uint8_t* data, size_t data_size, void* context) {
 | |
|     ExpansionTestApp* instance = context;
 | |
| 
 | |
|     furi_hal_serial_tx(instance->handle, data, data_size);
 | |
|     furi_hal_serial_tx_wait_complete(instance->handle);
 | |
| 
 | |
|     return data_size;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_receive_frame(ExpansionTestApp* instance, ExpansionFrame* frame) {
 | |
|     return expansion_protocol_decode(frame, expansion_test_app_receive_callback, instance) ==
 | |
|            ExpansionProtocolStatusOk;
 | |
| }
 | |
| 
 | |
| static bool
 | |
|     expansion_test_app_send_status_response(ExpansionTestApp* instance, ExpansionFrameError error) {
 | |
|     ExpansionFrame frame = {
 | |
|         .header.type = ExpansionFrameTypeStatus,
 | |
|         .content.status.error = error,
 | |
|     };
 | |
|     return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
 | |
|            ExpansionProtocolStatusOk;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_send_heartbeat(ExpansionTestApp* instance) {
 | |
|     ExpansionFrame frame = {
 | |
|         .header.type = ExpansionFrameTypeHeartbeat,
 | |
|         .content.heartbeat = {},
 | |
|     };
 | |
|     return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
 | |
|            ExpansionProtocolStatusOk;
 | |
| }
 | |
| 
 | |
| static bool
 | |
|     expansion_test_app_send_baud_rate_request(ExpansionTestApp* instance, uint32_t baud_rate) {
 | |
|     ExpansionFrame frame = {
 | |
|         .header.type = ExpansionFrameTypeBaudRate,
 | |
|         .content.baud_rate.baud = baud_rate,
 | |
|     };
 | |
|     return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
 | |
|            ExpansionProtocolStatusOk;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_send_control_request(
 | |
|     ExpansionTestApp* instance,
 | |
|     ExpansionFrameControlCommand command) {
 | |
|     ExpansionFrame frame = {
 | |
|         .header.type = ExpansionFrameTypeControl,
 | |
|         .content.control.command = command,
 | |
|     };
 | |
|     return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
 | |
|            ExpansionProtocolStatusOk;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_send_data_request(
 | |
|     ExpansionTestApp* instance,
 | |
|     const uint8_t* data,
 | |
|     size_t data_size) {
 | |
|     furi_assert(data_size <= EXPANSION_PROTOCOL_MAX_DATA_SIZE);
 | |
| 
 | |
|     ExpansionFrame frame = {
 | |
|         .header.type = ExpansionFrameTypeData,
 | |
|         .content.data.size = data_size,
 | |
|     };
 | |
| 
 | |
|     memcpy(frame.content.data.bytes, data, data_size);
 | |
|     return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
 | |
|            ExpansionProtocolStatusOk;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_rpc_encode_callback(
 | |
|     pb_ostream_t* stream,
 | |
|     const pb_byte_t* data,
 | |
|     size_t data_size) {
 | |
|     ExpansionTestApp* instance = stream->state;
 | |
| 
 | |
|     size_t size_sent = 0;
 | |
| 
 | |
|     while(size_sent < data_size) {
 | |
|         const size_t current_size = MIN(data_size - size_sent, EXPANSION_PROTOCOL_MAX_DATA_SIZE);
 | |
|         if(!expansion_test_app_send_data_request(instance, data + size_sent, current_size)) break;
 | |
|         if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
 | |
|         if(!expansion_test_app_is_success_response(&instance->frame)) break;
 | |
|         size_sent += current_size;
 | |
|     }
 | |
| 
 | |
|     return size_sent == data_size;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_send_rpc_request(ExpansionTestApp* instance, PB_Main* message) {
 | |
|     pb_ostream_t stream = {
 | |
|         .callback = expansion_test_app_rpc_encode_callback,
 | |
|         .state = instance,
 | |
|         .max_size = SIZE_MAX,
 | |
|         .bytes_written = 0,
 | |
|         .errmsg = NULL,
 | |
|     };
 | |
| 
 | |
|     const bool success = pb_encode_ex(&stream, &PB_Main_msg, message, PB_ENCODE_DELIMITED);
 | |
|     pb_release(&PB_Main_msg, message);
 | |
|     return success;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_receive_rpc_request(ExpansionTestApp* instance, PB_Main* message) {
 | |
|     bool success = false;
 | |
| 
 | |
|     do {
 | |
|         if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
 | |
|         if(!expansion_test_app_send_status_response(instance, ExpansionFrameErrorNone)) break;
 | |
|         if(instance->frame.header.type != ExpansionFrameTypeData) break;
 | |
|         pb_istream_t stream = pb_istream_from_buffer(
 | |
|             instance->frame.content.data.bytes, instance->frame.content.data.size);
 | |
|         if(!pb_decode_ex(&stream, &PB_Main_msg, message, PB_DECODE_DELIMITED)) break;
 | |
|         success = true;
 | |
|     } while(false);
 | |
| 
 | |
|     return success;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_send_presence(ExpansionTestApp* instance) {
 | |
|     // Send pulses to emulate module insertion
 | |
|     const uint8_t init = 0xAA;
 | |
|     furi_hal_serial_tx(instance->handle, &init, sizeof(init));
 | |
|     furi_hal_serial_tx_wait_complete(instance->handle);
 | |
|     return true;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_wait_ready(ExpansionTestApp* instance) {
 | |
|     bool success = false;
 | |
| 
 | |
|     do {
 | |
|         if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
 | |
|         if(instance->frame.header.type != ExpansionFrameTypeHeartbeat) break;
 | |
|         success = true;
 | |
|     } while(false);
 | |
| 
 | |
|     return success;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_handshake(ExpansionTestApp* instance) {
 | |
|     bool success = false;
 | |
| 
 | |
|     do {
 | |
|         if(!expansion_test_app_send_baud_rate_request(instance, 230400)) break;
 | |
|         if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
 | |
|         if(!expansion_test_app_is_success_response(&instance->frame)) break;
 | |
|         furi_hal_serial_set_br(instance->handle, 230400);
 | |
|         furi_delay_ms(EXPANSION_PROTOCOL_BAUD_CHANGE_DT_MS);
 | |
|         success = true;
 | |
|     } while(false);
 | |
| 
 | |
|     return success;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_start_rpc(ExpansionTestApp* instance) {
 | |
|     bool success = false;
 | |
| 
 | |
|     do {
 | |
|         if(!expansion_test_app_send_control_request(instance, ExpansionFrameControlCommandStartRpc))
 | |
|             break;
 | |
|         if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
 | |
|         if(!expansion_test_app_is_success_response(&instance->frame)) break;
 | |
|         success = true;
 | |
|     } while(false);
 | |
| 
 | |
|     return success;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_rpc_mkdir(ExpansionTestApp* instance) {
 | |
|     bool success = false;
 | |
| 
 | |
|     instance->msg.command_id++;
 | |
|     instance->msg.command_status = PB_CommandStatus_OK;
 | |
|     instance->msg.which_content = PB_Main_storage_mkdir_request_tag;
 | |
|     instance->msg.has_next = false;
 | |
|     instance->msg.content.storage_mkdir_request.path = TEST_DIR_PATH;
 | |
| 
 | |
|     do {
 | |
|         if(!expansion_test_app_send_rpc_request(instance, &instance->msg)) break;
 | |
|         if(!expansion_test_app_receive_rpc_request(instance, &instance->msg)) break;
 | |
|         if(!expansion_test_app_is_success_rpc_message(&instance->msg)) break;
 | |
|         success = true;
 | |
|     } while(false);
 | |
| 
 | |
|     return success;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_rpc_write(ExpansionTestApp* instance) {
 | |
|     bool success = false;
 | |
| 
 | |
|     Storage* storage = furi_record_open(RECORD_STORAGE);
 | |
|     File* file = storage_file_alloc(storage);
 | |
| 
 | |
|     do {
 | |
|         if(!storage_file_open(file, APP_ASSETS_PATH(TEST_FILE_NAME), FSAM_READ, FSOM_OPEN_EXISTING))
 | |
|             break;
 | |
| 
 | |
|         const uint64_t file_size = storage_file_size(file);
 | |
| 
 | |
|         instance->msg.command_id++;
 | |
|         instance->msg.command_status = PB_CommandStatus_OK;
 | |
|         instance->msg.which_content = PB_Main_storage_write_request_tag;
 | |
|         instance->msg.has_next = false;
 | |
|         instance->msg.content.storage_write_request.path = TEST_FILE_PATH;
 | |
|         instance->msg.content.storage_write_request.has_file = true;
 | |
|         instance->msg.content.storage_write_request.file.data =
 | |
|             malloc(PB_BYTES_ARRAY_T_ALLOCSIZE(file_size));
 | |
|         instance->msg.content.storage_write_request.file.data->size = file_size;
 | |
| 
 | |
|         const size_t bytes_read = storage_file_read(
 | |
|             file, instance->msg.content.storage_write_request.file.data->bytes, file_size);
 | |
| 
 | |
|         if(bytes_read != file_size) {
 | |
|             pb_release(&PB_Main_msg, &instance->msg);
 | |
|             break;
 | |
|         }
 | |
| 
 | |
|         if(!expansion_test_app_send_rpc_request(instance, &instance->msg)) break;
 | |
|         if(!expansion_test_app_receive_rpc_request(instance, &instance->msg)) break;
 | |
|         if(!expansion_test_app_is_success_rpc_message(&instance->msg)) break;
 | |
|         success = true;
 | |
|     } while(false);
 | |
| 
 | |
|     storage_file_free(file);
 | |
|     furi_record_close(RECORD_STORAGE);
 | |
| 
 | |
|     return success;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_rpc_alert(ExpansionTestApp* instance) {
 | |
|     bool success = false;
 | |
| 
 | |
|     instance->msg.command_id++;
 | |
|     instance->msg.command_status = PB_CommandStatus_OK;
 | |
|     instance->msg.which_content = PB_Main_system_play_audiovisual_alert_request_tag;
 | |
|     instance->msg.has_next = false;
 | |
| 
 | |
|     do {
 | |
|         if(!expansion_test_app_send_rpc_request(instance, &instance->msg)) break;
 | |
|         if(!expansion_test_app_receive_rpc_request(instance, &instance->msg)) break;
 | |
|         if(instance->msg.which_content != PB_Main_empty_tag) break;
 | |
|         if(instance->msg.command_status != PB_CommandStatus_OK) break;
 | |
|         success = true;
 | |
|     } while(false);
 | |
| 
 | |
|     return success;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_idle(ExpansionTestApp* instance, uint32_t num_cycles) {
 | |
|     uint32_t num_cycles_done;
 | |
|     for(num_cycles_done = 0; num_cycles_done < num_cycles; ++num_cycles_done) {
 | |
|         if(!expansion_test_app_send_heartbeat(instance)) break;
 | |
|         if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
 | |
|         if(instance->frame.header.type != ExpansionFrameTypeHeartbeat) break;
 | |
|         furi_delay_ms(EXPANSION_PROTOCOL_TIMEOUT_MS - 50);
 | |
|     }
 | |
| 
 | |
|     return num_cycles_done == num_cycles;
 | |
| }
 | |
| 
 | |
| static bool expansion_test_app_stop_rpc(ExpansionTestApp* instance) {
 | |
|     bool success = false;
 | |
| 
 | |
|     do {
 | |
|         if(!expansion_test_app_send_control_request(instance, ExpansionFrameControlCommandStopRpc))
 | |
|             break;
 | |
|         if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
 | |
|         if(!expansion_test_app_is_success_response(&instance->frame)) break;
 | |
|         success = true;
 | |
|     } while(false);
 | |
| 
 | |
|     return success;
 | |
| }
 | |
| 
 | |
| int32_t expansion_test_app(void* p) {
 | |
|     UNUSED(p);
 | |
| 
 | |
|     ExpansionTestApp* instance = expansion_test_app_alloc();
 | |
|     expansion_test_app_start(instance);
 | |
| 
 | |
|     bool success = false;
 | |
| 
 | |
|     do {
 | |
|         if(!expansion_test_app_send_presence(instance)) break;
 | |
|         if(!expansion_test_app_wait_ready(instance)) break;
 | |
|         if(!expansion_test_app_handshake(instance)) break;
 | |
|         if(!expansion_test_app_start_rpc(instance)) break;
 | |
|         if(!expansion_test_app_rpc_mkdir(instance)) break;
 | |
|         if(!expansion_test_app_rpc_write(instance)) break;
 | |
|         if(!expansion_test_app_rpc_alert(instance)) break;
 | |
|         if(!expansion_test_app_idle(instance, 10)) break;
 | |
|         if(!expansion_test_app_stop_rpc(instance)) break;
 | |
|         if(!expansion_test_app_idle(instance, 10)) break;
 | |
|         success = true;
 | |
|     } while(false);
 | |
| 
 | |
|     expansion_test_app_stop(instance);
 | |
|     expansion_test_app_free(instance);
 | |
| 
 | |
|     if(!success) {
 | |
|         NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
 | |
|         notification_message(notification, &sequence_error);
 | |
|         furi_record_close(RECORD_NOTIFICATION);
 | |
|     }
 | |
| 
 | |
|     return 0;
 | |
| }
 |