From fa2d611652c2658c3f5499493e9020f07b549062 Mon Sep 17 00:00:00 2001 From: Georgii Surkov <37121527+gsurkov@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:40:14 +0100 Subject: [PATCH] [FL-3889] 5V on GPIO control for ext. modules (#3830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make file extensions case-insensitive * Bump protobuf version * Add support for 5V control via RPC * Add support for 5V control via Expansion protocol * Update running instructions * Update expansion module documentation * Prettify condition * Test RPC OTG control as well * Assets: bump protobuf version * Disable PVS license expiration check, fix PVS warnings Co-authored-by: あく --- .../debug/expansion_test/expansion_test.c | 59 +++++++++++++++++-- .../services/expansion/expansion_protocol.h | 24 +++++++- .../services/expansion/expansion_worker.c | 26 ++++++-- .../services/gui/modules/byte_input.c | 6 +- applications/services/rpc/rpc_gpio.c | 45 ++++++++++++++ assets/protobuf | 2 +- documentation/ExpansionModules.md | 18 ++++-- scripts/fbt_tools/pvsstudio.py | 1 + 8 files changed, 160 insertions(+), 21 deletions(-) diff --git a/applications/debug/expansion_test/expansion_test.c b/applications/debug/expansion_test/expansion_test.c index e3bcf4e9c..665874f47 100644 --- a/applications/debug/expansion_test/expansion_test.c +++ b/applications/debug/expansion_test/expansion_test.c @@ -6,18 +6,27 @@ * 13 -> 16 (USART TX to LPUART RX) * 14 -> 15 (USART RX to LPUART TX) * + * Optional: Connect an LED with an appropriate series resistor + * between pins 1 and 8. It will always be on if the device is + * connected to USB power, so unplug it before running the app. + * * 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, + * - Enables OTG (5V) on GPIO via plain expansion protocol, + * - Waits 5 cycles of idle loop (1 second), * - Starts the RPC session, + * - Disables OTG (5V) on GPIO via RPC messages, + * - Waits 5 cycles of idle loop (1 second), * - 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, + * - Enables OTG (5V) on GPIO via RPC messages, + * - Waits 5 cycles of idle loop (1 second), * - Stops the RPC session, - * - Waits another 10 cycles of idle loop, + * - Disables OTG (5V) on GPIO via plain expansion protocol, * - Exits (plays a sound if any of the above steps failed). */ #include @@ -302,6 +311,22 @@ static bool expansion_test_app_handshake(ExpansionTestApp* instance) { return success; } +static bool expansion_test_app_enable_otg(ExpansionTestApp* instance, bool enable) { + bool success = false; + + do { + const ExpansionFrameControlCommand command = enable ? + ExpansionFrameControlCommandEnableOtg : + ExpansionFrameControlCommandDisableOtg; + if(!expansion_test_app_send_control_request(instance, command)) 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_start_rpc(ExpansionTestApp* instance) { bool success = false; @@ -396,6 +421,27 @@ static bool expansion_test_app_rpc_alert(ExpansionTestApp* instance) { return success; } +static bool expansion_test_app_rpc_enable_otg(ExpansionTestApp* instance, bool enable) { + bool success = false; + + instance->msg.command_id++; + instance->msg.command_status = PB_CommandStatus_OK; + instance->msg.which_content = PB_Main_gpio_set_otg_mode_tag; + instance->msg.content.gpio_set_otg_mode.mode = enable ? PB_Gpio_GpioOtgMode_ON : + PB_Gpio_GpioOtgMode_OFF; + 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) { @@ -434,13 +480,18 @@ int32_t expansion_test_app(void* p) { 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_enable_otg(instance, true)) break; + if(!expansion_test_app_idle(instance, 5)) break; if(!expansion_test_app_start_rpc(instance)) break; + if(!expansion_test_app_rpc_enable_otg(instance, false)) break; + if(!expansion_test_app_idle(instance, 5)) 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_rpc_enable_otg(instance, true)) break; + if(!expansion_test_app_idle(instance, 5)) break; if(!expansion_test_app_stop_rpc(instance)) break; - if(!expansion_test_app_idle(instance, 10)) break; + if(!expansion_test_app_enable_otg(instance, false)) break; success = true; } while(false); diff --git a/applications/services/expansion/expansion_protocol.h b/applications/services/expansion/expansion_protocol.h index 6ed818f82..a8d682330 100644 --- a/applications/services/expansion/expansion_protocol.h +++ b/applications/services/expansion/expansion_protocol.h @@ -64,8 +64,28 @@ typedef enum { * @brief Enumeration of suported control commands. */ typedef enum { - ExpansionFrameControlCommandStartRpc = 0x00, /**< Start an RPC session. */ - ExpansionFrameControlCommandStopRpc = 0x01, /**< Stop an open RPC session. */ + /** @brief Start an RPC session. + * + * Must only be used while the RPC session is NOT active. + */ + ExpansionFrameControlCommandStartRpc = 0x00, + /** @brief Stop an open RPC session. + * + * Must only be used while the RPC session IS active. + */ + ExpansionFrameControlCommandStopRpc = 0x01, + /** @brief Enable OTG (5V) on external GPIO. + * + * Must only be used while the RPC session is NOT active, + * otherwise OTG is to be controlled via RPC messages. + */ + ExpansionFrameControlCommandEnableOtg = 0x02, + /** @brief Disable OTG (5V) on external GPIO. + * + * Must only be used while the RPC session is NOT active, + * otherwise OTG is to be controlled via RPC messages. + */ + ExpansionFrameControlCommandDisableOtg = 0x03, } ExpansionFrameControlCommand; #pragma pack(push, 1) diff --git a/applications/services/expansion/expansion_worker.c b/applications/services/expansion/expansion_worker.c index 449d02cff..c05b9cc85 100644 --- a/applications/services/expansion/expansion_worker.c +++ b/applications/services/expansion/expansion_worker.c @@ -245,9 +245,18 @@ static bool expansion_worker_handle_state_connected( do { if(rx_frame->header.type == ExpansionFrameTypeControl) { - if(rx_frame->content.control.command != ExpansionFrameControlCommandStartRpc) break; - instance->state = ExpansionWorkerStateRpcActive; - if(!expansion_worker_rpc_session_open(instance)) break; + const uint8_t command = rx_frame->content.control.command; + if(command == ExpansionFrameControlCommandStartRpc) { + if(!expansion_worker_rpc_session_open(instance)) break; + instance->state = ExpansionWorkerStateRpcActive; + } else if(command == ExpansionFrameControlCommandEnableOtg) { + furi_hal_power_enable_otg(); + } else if(command == ExpansionFrameControlCommandDisableOtg) { + furi_hal_power_disable_otg(); + } else { + break; + } + if(!expansion_worker_send_status_response(instance, ExpansionFrameErrorNone)) break; } else if(rx_frame->header.type == ExpansionFrameTypeHeartbeat) { @@ -279,9 +288,14 @@ static bool expansion_worker_handle_state_rpc_active( if(size_consumed != rx_frame->content.data.size) break; } else if(rx_frame->header.type == ExpansionFrameTypeControl) { - if(rx_frame->content.control.command != ExpansionFrameControlCommandStopRpc) break; - instance->state = ExpansionWorkerStateConnected; - expansion_worker_rpc_session_close(instance); + const uint8_t command = rx_frame->content.control.command; + if(command == ExpansionFrameControlCommandStopRpc) { + instance->state = ExpansionWorkerStateConnected; + expansion_worker_rpc_session_close(instance); + } else { + break; + } + if(!expansion_worker_send_status_response(instance, ExpansionFrameErrorNone)) break; } else if(rx_frame->header.type == ExpansionFrameTypeStatus) { diff --git a/applications/services/gui/modules/byte_input.c b/applications/services/gui/modules/byte_input.c index 6e85401da..be94ed9ab 100644 --- a/applications/services/gui/modules/byte_input.c +++ b/applications/services/gui/modules/byte_input.c @@ -33,7 +33,7 @@ typedef struct { static const uint8_t keyboard_origin_x = 7; static const uint8_t keyboard_origin_y = 31; -static const uint8_t keyboard_row_count = 2; +static const int8_t keyboard_row_count = 2; static const uint8_t enter_symbol = '\r'; static const uint8_t backspace_symbol = '\b'; static const uint8_t max_drawable_bytes = 8; @@ -649,11 +649,11 @@ static void byte_input_view_draw_callback(Canvas* canvas, void* _model) { } canvas_set_font(canvas, FontKeyboard); // Draw keyboard - for(uint8_t row = 0; row < keyboard_row_count; row++) { + for(int8_t row = 0; row < keyboard_row_count; row++) { const uint8_t column_count = byte_input_get_row_size(row); const ByteInputKey* keys = byte_input_get_row(row); - for(size_t column = 0; column < column_count; column++) { + for(uint8_t column = 0; column < column_count; column++) { if(keys[column].value == enter_symbol) { canvas_set_color(canvas, ColorBlack); if(model->selected_row == row && model->selected_column == column) { diff --git a/applications/services/rpc/rpc_gpio.c b/applications/services/rpc/rpc_gpio.c index 09e738505..40fc898a0 100644 --- a/applications/services/rpc/rpc_gpio.c +++ b/applications/services/rpc/rpc_gpio.c @@ -2,6 +2,7 @@ #include "rpc_i.h" #include "gpio.pb.h" #include +#include #include static const GpioPin* rpc_pin_to_hal_pin(PB_Gpio_GpioPin rpc_pin) { @@ -188,6 +189,44 @@ void rpc_system_gpio_set_input_pull(const PB_Main* request, void* context) { free(response); } +void rpc_system_gpio_get_otg_mode(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(context); + furi_assert(request->which_content == PB_Main_gpio_get_otg_mode_tag); + + RpcSession* session = context; + + const bool otg_enabled = furi_hal_power_is_otg_enabled(); + + PB_Main* response = malloc(sizeof(PB_Main)); + response->command_id = request->command_id; + response->which_content = PB_Main_gpio_get_otg_mode_response_tag; + response->content.gpio_get_otg_mode_response.mode = otg_enabled ? PB_Gpio_GpioOtgMode_ON : + PB_Gpio_GpioOtgMode_OFF; + + rpc_send_and_release(session, response); + + free(response); +} + +void rpc_system_gpio_set_otg_mode(const PB_Main* request, void* context) { + furi_assert(request); + furi_assert(context); + furi_assert(request->which_content == PB_Main_gpio_set_otg_mode_tag); + + RpcSession* session = context; + + const PB_Gpio_GpioOtgMode mode = request->content.gpio_set_otg_mode.mode; + + if(mode == PB_Gpio_GpioOtgMode_OFF) { + furi_hal_power_disable_otg(); + } else { + furi_hal_power_enable_otg(); + } + + rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); +} + void* rpc_system_gpio_alloc(RpcSession* session) { furi_assert(session); @@ -212,5 +251,11 @@ void* rpc_system_gpio_alloc(RpcSession* session) { rpc_handler.message_handler = rpc_system_gpio_set_input_pull; rpc_add_handler(session, PB_Main_gpio_set_input_pull_tag, &rpc_handler); + rpc_handler.message_handler = rpc_system_gpio_get_otg_mode; + rpc_add_handler(session, PB_Main_gpio_get_otg_mode_tag, &rpc_handler); + + rpc_handler.message_handler = rpc_system_gpio_set_otg_mode; + rpc_add_handler(session, PB_Main_gpio_set_otg_mode_tag, &rpc_handler); + return NULL; } diff --git a/assets/protobuf b/assets/protobuf index 816de200a..6c7c0d55e 160000 --- a/assets/protobuf +++ b/assets/protobuf @@ -1 +1 @@ -Subproject commit 816de200a4a43efc25c5b92d6a57fc982d7e988a +Subproject commit 6c7c0d55e82cb89223cf4890a540af4cff837fa7 diff --git a/documentation/ExpansionModules.md b/documentation/ExpansionModules.md index 470564e57..fd9703adc 100644 --- a/documentation/ExpansionModules.md +++ b/documentation/ExpansionModules.md @@ -73,7 +73,7 @@ If the requested baud rate is supported by the host, it SHALL respond with a STA ### Control frame -CONTROL frames are used to control various aspects of the communication. As of now, the sole purpose of CONTROL frames is to start and stop the RPC session. +CONTROL frames are used to control various aspects of the communication and enable/disable various device features. | Header (1 byte) | Contents (1 byte) | Checksum (1 byte) | |-----------------|-------------------|-------------------| @@ -81,10 +81,18 @@ CONTROL frames are used to control various aspects of the communication. As of n The `Command` field SHALL have one of the followind values: -| Command | Meaning | -|---------|-------------------| -| 0x00 | Start RPC session | -| 0x01 | Stop RPC session | +| Command | Meaning | Note | +|---------|--------------------------|:----:| +| 0x00 | Start RPC session | 1 | +| 0x01 | Stop RPC session | 2 | +| 0x02 | Enable OTG (5V) on GPIO | 3 | +| 0x03 | Disable OTG (5V) on GPIO | 3 | + +Notes: + +1. Must only be used while the RPC session NOT active. +2. Must only be used while the RPC session IS active. +3. See 1, otherwise OTG is to be controlled via RPC messages. ### Data frame diff --git a/scripts/fbt_tools/pvsstudio.py b/scripts/fbt_tools/pvsstudio.py index 290531321..1a55278dc 100644 --- a/scripts/fbt_tools/pvsstudio.py +++ b/scripts/fbt_tools/pvsstudio.py @@ -47,6 +47,7 @@ def generate(env): PVSOPTIONS=[ "@.pvsoptions", "-j${PVSNCORES}", + "--disableLicenseExpirationCheck", # "--incremental", # kinda broken on PVS side ], PVSCONVOPTIONS=[