├── chatpad_wiring.png ├── flipper_zero.png ├── firmware-overlay ├── unl-079 │ └── applications │ │ ├── settings │ │ ├── chatpad_settings │ │ │ └── application.fam │ │ └── application.fam │ │ └── services │ │ └── rpc │ │ ├── rpc_chatpad.h │ │ ├── rpc_keyboard.h │ │ ├── rpc_keyboard.c │ │ ├── rpc_chatpad.c │ │ └── rpc.c ├── mntm-008 │ └── applications │ │ ├── settings │ │ ├── chatpad_settings │ │ │ ├── application.fam │ │ │ └── rpc_keyboard_stub.c │ │ └── application.fam │ │ └── services │ │ └── rpc │ │ ├── rpc_chatpad.h │ │ ├── rpc_keyboard.h │ │ ├── rpc_keyboard.c │ │ ├── rpc_chatpad.c │ │ └── rpc.c ├── ofw-1.1.2 │ └── applications │ │ ├── settings │ │ ├── chatpad_settings │ │ │ └── application.fam │ │ └── application.fam │ │ └── services │ │ └── rpc │ │ ├── rpc_chatpad.h │ │ ├── rpc_keyboard.h │ │ ├── rpc_keyboard.c │ │ ├── rpc_chatpad.c │ │ └── rpc.c └── rm-1202-0837-0.420.0-6d10bad │ └── applications │ ├── settings │ ├── chatpad_settings │ │ ├── application.fam │ │ └── rpc_keyboard_stub.c │ └── application.fam │ └── services │ └── rpc │ ├── rpc_chatpad.h │ ├── rpc_keyboard.h │ ├── rpc_keyboard.c │ ├── rpc_chatpad.c │ └── rpc.c ├── .gitignore ├── javascript └── chatpad.js └── README.md /chatpad_wiring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamisonderek/flipper-zero-input/HEAD/chatpad_wiring.png -------------------------------------------------------------------------------- /flipper_zero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamisonderek/flipper-zero-input/HEAD/flipper_zero.png -------------------------------------------------------------------------------- /firmware-overlay/unl-079/applications/settings/chatpad_settings/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="chatpad_settings", 3 | name="Chatpad", 4 | apptype=FlipperAppType.SETTINGS, 5 | entry_point="chatpad_app", 6 | requires=["gui","rpc_start"], 7 | stack_size=2 * 1024, 8 | order=85, 9 | ) 10 | -------------------------------------------------------------------------------- /firmware-overlay/mntm-008/applications/settings/chatpad_settings/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="chatpad_settings", 3 | name="Chatpad", 4 | apptype=FlipperAppType.SETTINGS, 5 | entry_point="chatpad_app", 6 | requires=["gui","rpc_start"], 7 | stack_size=2 * 1024, 8 | order=85, 9 | ) 10 | -------------------------------------------------------------------------------- /firmware-overlay/ofw-1.1.2/applications/settings/chatpad_settings/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="chatpad_settings", 3 | name="Chatpad", 4 | apptype=FlipperAppType.SETTINGS, 5 | entry_point="chatpad_app", 6 | requires=["gui","rpc_start"], 7 | stack_size=2 * 1024, 8 | order=85, 9 | ) 10 | -------------------------------------------------------------------------------- /firmware-overlay/rm-1202-0837-0.420.0-6d10bad/applications/settings/chatpad_settings/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="chatpad_settings", 3 | name="Chatpad", 4 | apptype=FlipperAppType.EXTSETTINGS, 5 | entry_point="chatpad_app", 6 | requires=["gui", "rpc_start"], 7 | stack_size=2 * 1024, 8 | order=85, 9 | ) 10 | -------------------------------------------------------------------------------- /firmware-overlay/mntm-008/applications/settings/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="settings_apps", 3 | name="Basic settings apps bundle", 4 | apptype=FlipperAppType.METAPACKAGE, 5 | provides=[ 6 | "passport", 7 | "system_settings", 8 | "clock_settings", 9 | "about", 10 | "chatpad_settings", 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /firmware-overlay/ofw-1.1.2/applications/settings/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="settings_apps", 3 | name="Basic settings apps bundle", 4 | apptype=FlipperAppType.METAPACKAGE, 5 | provides=[ 6 | "passport", 7 | "system_settings", 8 | "clock_settings", 9 | "about", 10 | "chatpad_settings", 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /firmware-overlay/unl-079/applications/settings/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="settings_apps", 3 | name="Basic settings apps bundle", 4 | apptype=FlipperAppType.METAPACKAGE, 5 | provides=[ 6 | "passport", 7 | "system_settings", 8 | "clock_settings", 9 | "about", 10 | "chatpad_settings", 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /firmware-overlay/rm-1202-0837-0.420.0-6d10bad/applications/settings/application.fam: -------------------------------------------------------------------------------- 1 | App( 2 | appid="settings_apps", 3 | name="Basic settings apps bundle", 4 | apptype=FlipperAppType.METAPACKAGE, 5 | provides=[ 6 | # "dolphin_passport", 7 | # "desktop_settings", 8 | # "passport_settings", 9 | "system_settings", 10 | "about", 11 | "chatpad_settings", 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | -------------------------------------------------------------------------------- /firmware-overlay/mntm-008/applications/services/rpc/rpc_chatpad.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct RpcChatpad RpcChatpad; 9 | 10 | /** 11 | * @brief Allocates a chatpad. 12 | * @return RpcChatpad* pointer to the chatpad. 13 | */ 14 | RpcChatpad* rpc_chatpad_alloc(void); 15 | 16 | /** 17 | * @brief Frees the chatpad. 18 | * @param[in] chatpad pointer to the chatpad. 19 | */ 20 | void rpc_chatpad_free(RpcChatpad* chatpad); 21 | 22 | /** 23 | * @brief determines if chatpad is ready for use. 24 | * @param[in] chatpad pointer to the chatpad. 25 | * @return true if the chatpad is responding to data. 26 | */ 27 | bool rpc_chatpad_ready(RpcChatpad* chatpad); 28 | -------------------------------------------------------------------------------- /firmware-overlay/ofw-1.1.2/applications/services/rpc/rpc_chatpad.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct RpcChatpad RpcChatpad; 9 | 10 | /** 11 | * @brief Allocates a chatpad. 12 | * @return RpcChatpad* pointer to the chatpad. 13 | */ 14 | RpcChatpad* rpc_chatpad_alloc(void); 15 | 16 | /** 17 | * @brief Frees the chatpad. 18 | * @param[in] chatpad pointer to the chatpad. 19 | */ 20 | void rpc_chatpad_free(RpcChatpad* chatpad); 21 | 22 | /** 23 | * @brief determines if chatpad is ready for use. 24 | * @param[in] chatpad pointer to the chatpad. 25 | * @return true if the chatpad is responding to data. 26 | */ 27 | bool rpc_chatpad_ready(RpcChatpad* chatpad); 28 | -------------------------------------------------------------------------------- /firmware-overlay/unl-079/applications/services/rpc/rpc_chatpad.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct RpcChatpad RpcChatpad; 9 | 10 | /** 11 | * @brief Allocates a chatpad. 12 | * @return RpcChatpad* pointer to the chatpad. 13 | */ 14 | RpcChatpad* rpc_chatpad_alloc(void); 15 | 16 | /** 17 | * @brief Frees the chatpad. 18 | * @param[in] chatpad pointer to the chatpad. 19 | */ 20 | void rpc_chatpad_free(RpcChatpad* chatpad); 21 | 22 | /** 23 | * @brief determines if chatpad is ready for use. 24 | * @param[in] chatpad pointer to the chatpad. 25 | * @return true if the chatpad is responding to data. 26 | */ 27 | bool rpc_chatpad_ready(RpcChatpad* chatpad); 28 | -------------------------------------------------------------------------------- /firmware-overlay/rm-1202-0837-0.420.0-6d10bad/applications/services/rpc/rpc_chatpad.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct RpcChatpad RpcChatpad; 9 | 10 | /** 11 | * @brief Allocates a chatpad. 12 | * @return RpcChatpad* pointer to the chatpad. 13 | */ 14 | RpcChatpad* rpc_chatpad_alloc(void); 15 | 16 | /** 17 | * @brief Frees the chatpad. 18 | * @param[in] chatpad pointer to the chatpad. 19 | */ 20 | void rpc_chatpad_free(RpcChatpad* chatpad); 21 | 22 | /** 23 | * @brief determines if chatpad is ready for use. 24 | * @param[in] chatpad pointer to the chatpad. 25 | * @return true if the chatpad is responding to data. 26 | */ 27 | bool rpc_chatpad_ready(RpcChatpad* chatpad); 28 | -------------------------------------------------------------------------------- /javascript/chatpad.js: -------------------------------------------------------------------------------- 1 | let serial = require("serial"); 2 | serial.setup("lpuart", 19200); 3 | serial.write([0x87, 0x02, 0x8C, 0x1F, 0xCC]); //init 4 | serial.read(1, 1000); // wait for response 5 | 6 | let keyMap = [['7', '6', '5', '4', '3', '2', '1'], 7 | ['U', 'Y', 'T', 'R', 'E', 'W', 'Q'], 8 | ['J', 'H', 'G', 'F', 'D', 'S', 'A'], 9 | ['N', 'B', 'V', 'C', 'X', 'Z', '?'], 10 | ['\x12', 'M', '.', ' ', '\x11', '?', '?'], // Right (0x12), Left (0x11) 11 | ['?', ',', '\0x0D', 'P', '0', '9', '8'], // enter 12 | ['\x08', 'L', '?', '?', 'O', 'I', 'K'] // backspace 13 | ]; 14 | 15 | let orangeMap = {"R": "$", "P":"=", ",": ";", "J": "\"", "H": "\\", "V":"_", "B": "+"}; 16 | let greenMap = {"Q": "!", "W": "@", "R" : "#", "T": "%", "Y": "^", "U": "&", "I": "*", "O": "(", "P": ")", "A": "~", "D": "{", "F": "}", "H": "/", "J": "'", "K": "[", "L": "]", ",":":", "Z": "`", "V": "-", "B": "|", "N":"<", "M": ">", ".": "?"}; 17 | 18 | function toChar(code, mod) { 19 | let row = code >> 4; 20 | let col = code & 0xF; 21 | let button = 'x'; 22 | if (row > 0 && col > 0 && row <= keyMap.length && col <= keyMap[row - 1].length) { 23 | button = keyMap[row - 1][col - 1]; 24 | if (mod === 4) { 25 | button = orangeMap[button]; 26 | } else if (mod === 2) { 27 | button = greenMap[button]; 28 | } else if (mod === 0 && button.length === 1) { 29 | button = button.toLowerCase(); 30 | } 31 | } else if (code === 0) { 32 | if (mod === 1) { 33 | button = 'Shift'; 34 | } else if (mod === 2) { 35 | button = 'Green'; 36 | } else if (mod === 4) { 37 | button = 'Orange'; 38 | } else if (mod === 8) { 39 | button = 'People'; 40 | } 41 | } 42 | return button; 43 | } 44 | 45 | let line = 0; 46 | let show = true; 47 | let ready = false; 48 | while (true) { 49 | if (line++ % 100 === 0) { 50 | serial.write([0x87, 0x02, 0x8C, 0x1B, 0xD0]); //sync 51 | } 52 | 53 | let chars = serial.read(8, 10); 54 | if (chars === undefined || chars.length < 8) { 55 | continue; 56 | } 57 | 58 | let ch = chars.charCodeAt(0); 59 | if ((ch !== 0xB4) && (ch !== 0xA5)) { 60 | let codes = chars.charCodeAt(0).toString(16); 61 | for (let j = 1; j < chars.length; j++) { 62 | let ch = chars.charCodeAt(j); 63 | codes = codes + "," + ch.toString(16); 64 | } 65 | console.error("Codes: ", codes); 66 | continue; 67 | } 68 | 69 | if (ch === 0xb4 && chars.charCodeAt(1) === 0xc5 && show) { 70 | let mod = chars.charCodeAt(3); 71 | let btn = chars.charCodeAt(4); 72 | if (mod !== 0 || btn !== 0) { 73 | console.error("mod: ", mod.toString(16), "btn: ", btn.toString(16), "key: ", toChar(btn, mod)); 74 | if (btn !== 0) { 75 | print(toChar(btn, mod)); 76 | } 77 | } 78 | show = false; 79 | } else if (ch === 0xa5) { 80 | if (!ready) { 81 | ready = true; 82 | console.error("Ready! ", line); 83 | print("Ready! ", line); 84 | } 85 | show = true; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /firmware-overlay/mntm-008/applications/settings/chatpad_settings/rpc_keyboard_stub.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | static bool rpc_keyboard_functions_check_version(RpcKeyboardFunctions* stub) { 6 | furi_check(stub); 7 | if(stub->major == 1 && stub->minor > 2) { 8 | return true; 9 | } 10 | FURI_LOG_D("RpcKeyboard", "Unsupported version %d.%d", stub->major, stub->minor); 11 | return false; 12 | } 13 | 14 | /** 15 | * @brief Get the pubsub object for the remote keyboard. 16 | * @details This function returns the pubsub object, use to subscribe to keyboard events. 17 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 18 | * @return FuriPubSub* pointer to the pubsub object. 19 | */ 20 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard) { 21 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 22 | if(!rpc_keyboard_functions_check_version(stub)) { 23 | return NULL; 24 | } 25 | return stub->fn_get_pubsub((RpcKeyboard*)rpc_keyboard); 26 | } 27 | 28 | /** 29 | * @brief Enable or disable newline character submitting the text. 30 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 31 | * @param[in] enable true to enable, false to disable. 32 | */ 33 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable) { 34 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 35 | if(!rpc_keyboard_functions_check_version(stub)) { 36 | return; 37 | } 38 | stub->fn_newline_enable((RpcKeyboard*)rpc_keyboard, enable); 39 | } 40 | 41 | /** 42 | * @brief Publish a single key pressed on the remote keyboard. 43 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 44 | * @param[in] character the character that was pressed. 45 | */ 46 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character) { 47 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 48 | if(!rpc_keyboard_functions_check_version(stub)) { 49 | return; 50 | } 51 | stub->fn_publish_char((RpcKeyboard*)rpc_keyboard, character); 52 | } 53 | 54 | /** 55 | * @brief Publish a macro key pressed on the remote keyboard. 56 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 57 | * @param[in] character the macro key that was pressed. 58 | */ 59 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro) { 60 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 61 | if(!rpc_keyboard_functions_check_version(stub)) { 62 | return; 63 | } 64 | stub->fn_publish_macro((RpcKeyboard*)rpc_keyboard, macro); 65 | } 66 | 67 | /** 68 | * @brief Get the macro text associated with a macro key. 69 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 70 | * @param[in] macro the macro key. 71 | * @return char* pointer to the macro text. NULL if the macro key is not set. User must free the memory. 72 | */ 73 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro) { 74 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 75 | if(!rpc_keyboard_functions_check_version(stub)) { 76 | return NULL; 77 | } 78 | return stub->fn_get_macro((RpcKeyboard*)rpc_keyboard, macro); 79 | } 80 | 81 | /** 82 | * @brief Set the macro text associated with a macro key. 83 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 84 | * @param[in] macro the macro key. 85 | * @param[in] value the macro text. 86 | */ 87 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value) { 88 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 89 | if(!rpc_keyboard_functions_check_version(stub)) { 90 | return; 91 | } 92 | stub->fn_set_macro((RpcKeyboard*)rpc_keyboard, macro, value); 93 | } 94 | 95 | /** 96 | * @brief Initializes the chatpad and starts listening for keypresses. 97 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 98 | */ 99 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard) { 100 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 101 | if(!rpc_keyboard_functions_check_version(stub)) { 102 | return; 103 | } 104 | stub->fn_chatpad_start((RpcKeyboard*)rpc_keyboard); 105 | } 106 | 107 | /** 108 | * @brief Stops the chatpad & frees resources. 109 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 110 | */ 111 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard) { 112 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 113 | if(!rpc_keyboard_functions_check_version(stub)) { 114 | return; 115 | } 116 | stub->fn_chatpad_stop((RpcKeyboard*)rpc_keyboard); 117 | } 118 | 119 | /** 120 | * @brief Get the status of the chatpad. 121 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 122 | * @return RpcKeyboardChatpadStatus the status of the chatpad. 123 | */ 124 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard) { 125 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 126 | if(!rpc_keyboard_functions_check_version(stub)) { 127 | return RpcKeyboardChatpadStatusError; 128 | } 129 | return stub->fn_chatpad_status((RpcKeyboard*)rpc_keyboard); 130 | } 131 | -------------------------------------------------------------------------------- /firmware-overlay/rm-1202-0837-0.420.0-6d10bad/applications/settings/chatpad_settings/rpc_keyboard_stub.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | static bool rpc_keyboard_functions_check_version(RpcKeyboardFunctions* stub) { 6 | furi_check(stub); 7 | if(stub->major == 1 && stub->minor > 2) { 8 | return true; 9 | } 10 | FURI_LOG_D("RpcKeyboard", "Unsupported version %d.%d", stub->major, stub->minor); 11 | return false; 12 | } 13 | 14 | /** 15 | * @brief Get the pubsub object for the remote keyboard. 16 | * @details This function returns the pubsub object, use to subscribe to keyboard events. 17 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 18 | * @return FuriPubSub* pointer to the pubsub object. 19 | */ 20 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard) { 21 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 22 | if(!rpc_keyboard_functions_check_version(stub)) { 23 | return NULL; 24 | } 25 | return stub->fn_get_pubsub((RpcKeyboard*)rpc_keyboard); 26 | } 27 | 28 | /** 29 | * @brief Enable or disable newline character submitting the text. 30 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 31 | * @param[in] enable true to enable, false to disable. 32 | */ 33 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable) { 34 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 35 | if(!rpc_keyboard_functions_check_version(stub)) { 36 | return; 37 | } 38 | stub->fn_newline_enable((RpcKeyboard*)rpc_keyboard, enable); 39 | } 40 | 41 | /** 42 | * @brief Publish a single key pressed on the remote keyboard. 43 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 44 | * @param[in] character the character that was pressed. 45 | */ 46 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character) { 47 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 48 | if(!rpc_keyboard_functions_check_version(stub)) { 49 | return; 50 | } 51 | stub->fn_publish_char((RpcKeyboard*)rpc_keyboard, character); 52 | } 53 | 54 | /** 55 | * @brief Publish a macro key pressed on the remote keyboard. 56 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 57 | * @param[in] character the macro key that was pressed. 58 | */ 59 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro) { 60 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 61 | if(!rpc_keyboard_functions_check_version(stub)) { 62 | return; 63 | } 64 | stub->fn_publish_macro((RpcKeyboard*)rpc_keyboard, macro); 65 | } 66 | 67 | /** 68 | * @brief Get the macro text associated with a macro key. 69 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 70 | * @param[in] macro the macro key. 71 | * @return char* pointer to the macro text. NULL if the macro key is not set. User must free the memory. 72 | */ 73 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro) { 74 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 75 | if(!rpc_keyboard_functions_check_version(stub)) { 76 | return NULL; 77 | } 78 | return stub->fn_get_macro((RpcKeyboard*)rpc_keyboard, macro); 79 | } 80 | 81 | /** 82 | * @brief Set the macro text associated with a macro key. 83 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 84 | * @param[in] macro the macro key. 85 | * @param[in] value the macro text. 86 | */ 87 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value) { 88 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 89 | if(!rpc_keyboard_functions_check_version(stub)) { 90 | return; 91 | } 92 | stub->fn_set_macro((RpcKeyboard*)rpc_keyboard, macro, value); 93 | } 94 | 95 | /** 96 | * @brief Initializes the chatpad and starts listening for keypresses. 97 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 98 | */ 99 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard) { 100 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 101 | if(!rpc_keyboard_functions_check_version(stub)) { 102 | return; 103 | } 104 | stub->fn_chatpad_start((RpcKeyboard*)rpc_keyboard); 105 | } 106 | 107 | /** 108 | * @brief Stops the chatpad & frees resources. 109 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 110 | */ 111 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard) { 112 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 113 | if(!rpc_keyboard_functions_check_version(stub)) { 114 | return; 115 | } 116 | stub->fn_chatpad_stop((RpcKeyboard*)rpc_keyboard); 117 | } 118 | 119 | /** 120 | * @brief Get the status of the chatpad. 121 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 122 | * @return RpcKeyboardChatpadStatus the status of the chatpad. 123 | */ 124 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard) { 125 | RpcKeyboardFunctions* stub = (RpcKeyboardFunctions*)rpc_keyboard; 126 | if(!rpc_keyboard_functions_check_version(stub)) { 127 | return RpcKeyboardChatpadStatusError; 128 | } 129 | return stub->fn_chatpad_status((RpcKeyboard*)rpc_keyboard); 130 | } 131 | -------------------------------------------------------------------------------- /firmware-overlay/unl-079/applications/services/rpc/rpc_keyboard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define RECORD_RPC_KEYBOARD "rpckeyboard" 8 | 9 | #define RPC_KEYBOARD_KEY_RIGHT '\x13' 10 | #define RPC_KEYBOARD_KEY_LEFT '\x14' 11 | #define RPC_KEYBOARD_KEY_ENTER '\x0D' 12 | #define RPC_KEYBOARD_KEY_BACKSPACE '\x08' 13 | 14 | typedef enum { 15 | // Unknown error occurred 16 | RpcKeyboardChatpadStatusError, 17 | // The chatpad worker is stopped 18 | RpcKeyboardChatpadStatusStopped, 19 | // The chatpad worker is started, but not ready 20 | RpcKeyboardChatpadStatusStarted, 21 | // The chatpad worker is ready and got response from chatpad 22 | RpcKeyboardChatpadStatusReady, 23 | } RpcKeyboardChatpadStatus; 24 | 25 | typedef struct RpcKeyboard RpcKeyboard; 26 | 27 | typedef enum { 28 | // Replacement text was provided by the user 29 | RpcKeyboardEventTypeTextEntered, 30 | // A single character was provided by the user 31 | RpcKeyboardEventTypeCharEntered, 32 | // A macro was entered by the user 33 | RpcKeyboardEventTypeMacroEntered, 34 | } RpcKeyboardEventType; 35 | 36 | typedef struct { 37 | // The mutex to protect the data, call furi_mutex_acquire/furi_mutex_release. 38 | FuriMutex* mutex; 39 | // The text message, macro or character. 40 | char message[256]; 41 | // The length of the message. 42 | uint16_t length; 43 | // The newline enabled flag, allow newline to submit text. 44 | bool newline_enabled; 45 | } RpcKeyboardEventData; 46 | 47 | typedef struct { 48 | RpcKeyboardEventType type; 49 | RpcKeyboardEventData data; 50 | } RpcKeyboardEvent; 51 | 52 | typedef FuriPubSub* (*RpcKeyboardGetPubsub)(RpcKeyboard* rpc_keyboard); 53 | typedef void (*RpcKeyboardNewlineEnable)(RpcKeyboard* rpc_keyboard, bool enable); 54 | typedef void (*RpcKeyboardPublishCharFn)(RpcKeyboard* keyboard, char character); 55 | typedef void (*RpcKeyboardPublishMacroFn)(RpcKeyboard* rpc_keyboard, char macro); 56 | typedef char* (*RpcKeyboardGetMacroFn)(RpcKeyboard* rpc_keyboard, char macro); 57 | typedef void (*RpcKeyboardSetMacroFn)(RpcKeyboard* rpc_keyboard, char macro, char* value); 58 | typedef void (*RpcKeyboardChatpadStartFn)(RpcKeyboard* rpc_keyboard); 59 | typedef void (*RpcKeyboardChatpadStopFn)(RpcKeyboard* rpc_keyboard); 60 | typedef RpcKeyboardChatpadStatus (*RpcKeyboardChatpadStatusFn)(RpcKeyboard* rpc_keyboard); 61 | 62 | typedef struct RpcKeyboardFunctions RpcKeyboardFunctions; 63 | struct RpcKeyboardFunctions { 64 | uint16_t major; 65 | uint16_t minor; 66 | RpcKeyboardGetPubsub fn_get_pubsub; 67 | RpcKeyboardNewlineEnable fn_newline_enable; 68 | RpcKeyboardPublishCharFn fn_publish_char; 69 | RpcKeyboardPublishMacroFn fn_publish_macro; 70 | RpcKeyboardGetMacroFn fn_get_macro; 71 | RpcKeyboardSetMacroFn fn_set_macro; 72 | RpcKeyboardChatpadStartFn fn_chatpad_start; 73 | RpcKeyboardChatpadStopFn fn_chatpad_stop; 74 | RpcKeyboardChatpadStatusFn fn_chatpad_status; 75 | }; 76 | 77 | /** 78 | * @brief STARTUP - Register the remote keyboard. 79 | */ 80 | void rpc_keyboard_register(void); 81 | 82 | /** 83 | * @brief UNUSED - Unregister the remote keyboard. 84 | */ 85 | void rpc_keyboard_release(void); 86 | 87 | /** 88 | * @brief Get the pubsub object for the remote keyboard. 89 | * @details This function returns the pubsub object, use to subscribe to keyboard events. 90 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 91 | * @return FuriPubSub* pointer to the pubsub object. 92 | */ 93 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard); 94 | 95 | /** 96 | * @brief Enable or disable newline character submitting the text. 97 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 98 | * @param[in] enable true to enable, false to disable. 99 | */ 100 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable); 101 | 102 | /** 103 | * @brief Publish the replacement text to the remote keyboard. 104 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 105 | * @param[in] bytes pointer to the text buffer. 106 | * @param[in] buffer_size size of the text buffer. 107 | */ 108 | void rpc_keyboard_publish_text(RpcKeyboard* rpc_keyboard, uint8_t* bytes, uint32_t buffer_size); 109 | 110 | /** 111 | * @brief Publish a single key pressed on the remote keyboard. 112 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 113 | * @param[in] character the character that was pressed. 114 | */ 115 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character); 116 | 117 | /** 118 | * @brief Publish a macro key pressed on the remote keyboard. 119 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 120 | * @param[in] character the macro key that was pressed. 121 | */ 122 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro); 123 | 124 | /** 125 | * @brief Get the macro text associated with a macro key. 126 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 127 | * @param[in] macro the macro key. 128 | * @return char* pointer to the macro text. NULL if the macro key is not set. User must free the memory. 129 | */ 130 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro); 131 | 132 | /** 133 | * @brief Set the macro text associated with a macro key. 134 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 135 | * @param[in] macro the macro key. 136 | * @param[in] value the macro text. 137 | */ 138 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value); 139 | 140 | /** 141 | * @brief Initializes the chatpad and starts listening for keypresses. 142 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 143 | */ 144 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard); 145 | 146 | /** 147 | * @brief Stops the chatpad & frees resources. 148 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 149 | */ 150 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard); 151 | 152 | /** 153 | * @brief Get the status of the chatpad. 154 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 155 | * @return RpcKeyboardChatpadStatus the status of the chatpad. 156 | */ 157 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard); 158 | -------------------------------------------------------------------------------- /firmware-overlay/mntm-008/applications/services/rpc/rpc_keyboard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define RECORD_RPC_KEYBOARD "rpckeyboard" 8 | 9 | #define RPC_KEYBOARD_KEY_RIGHT '\x13' 10 | #define RPC_KEYBOARD_KEY_LEFT '\x14' 11 | #define RPC_KEYBOARD_KEY_ENTER '\x0D' 12 | #define RPC_KEYBOARD_KEY_BACKSPACE '\x08' 13 | 14 | typedef enum { 15 | // Unknown error occurred 16 | RpcKeyboardChatpadStatusError, 17 | // The chatpad worker is stopped 18 | RpcKeyboardChatpadStatusStopped, 19 | // The chatpad worker is started, but not ready 20 | RpcKeyboardChatpadStatusStarted, 21 | // The chatpad worker is ready and got response from chatpad 22 | RpcKeyboardChatpadStatusReady, 23 | } RpcKeyboardChatpadStatus; 24 | 25 | typedef struct RpcKeyboard RpcKeyboard; 26 | 27 | typedef enum { 28 | // Replacement text was provided by the user 29 | RpcKeyboardEventTypeTextEntered, 30 | // A single character was provided by the user 31 | RpcKeyboardEventTypeCharEntered, 32 | // A macro was entered by the user 33 | RpcKeyboardEventTypeMacroEntered, 34 | } RpcKeyboardEventType; 35 | 36 | typedef struct { 37 | // The mutex to protect the data, call furi_mutex_acquire/furi_mutex_release. 38 | FuriMutex* mutex; 39 | // The text message, macro or character. 40 | char message[256]; 41 | // The length of the message. 42 | uint16_t length; 43 | // The newline enabled flag, allow newline to submit text. 44 | bool newline_enabled; 45 | } RpcKeyboardEventData; 46 | 47 | typedef struct { 48 | RpcKeyboardEventType type; 49 | RpcKeyboardEventData data; 50 | } RpcKeyboardEvent; 51 | 52 | typedef FuriPubSub* (*RpcKeyboardGetPubsub)(RpcKeyboard* rpc_keyboard); 53 | typedef void (*RpcKeyboardNewlineEnable)(RpcKeyboard* rpc_keyboard, bool enable); 54 | typedef void (*RpcKeyboardPublishCharFn)(RpcKeyboard* keyboard, char character); 55 | typedef void (*RpcKeyboardPublishMacroFn)(RpcKeyboard* rpc_keyboard, char macro); 56 | typedef char* (*RpcKeyboardGetMacroFn)(RpcKeyboard* rpc_keyboard, char macro); 57 | typedef void (*RpcKeyboardSetMacroFn)(RpcKeyboard* rpc_keyboard, char macro, char* value); 58 | typedef void (*RpcKeyboardChatpadStartFn)(RpcKeyboard* rpc_keyboard); 59 | typedef void (*RpcKeyboardChatpadStopFn)(RpcKeyboard* rpc_keyboard); 60 | typedef RpcKeyboardChatpadStatus (*RpcKeyboardChatpadStatusFn)(RpcKeyboard* rpc_keyboard); 61 | 62 | typedef struct RpcKeyboardFunctions RpcKeyboardFunctions; 63 | struct RpcKeyboardFunctions { 64 | uint16_t major; 65 | uint16_t minor; 66 | RpcKeyboardGetPubsub fn_get_pubsub; 67 | RpcKeyboardNewlineEnable fn_newline_enable; 68 | RpcKeyboardPublishCharFn fn_publish_char; 69 | RpcKeyboardPublishMacroFn fn_publish_macro; 70 | RpcKeyboardGetMacroFn fn_get_macro; 71 | RpcKeyboardSetMacroFn fn_set_macro; 72 | RpcKeyboardChatpadStartFn fn_chatpad_start; 73 | RpcKeyboardChatpadStopFn fn_chatpad_stop; 74 | RpcKeyboardChatpadStatusFn fn_chatpad_status; 75 | }; 76 | 77 | /** 78 | * @brief STARTUP - Register the remote keyboard. 79 | */ 80 | void rpc_keyboard_register(void); 81 | 82 | /** 83 | * @brief UNUSED - Unregister the remote keyboard. 84 | */ 85 | void rpc_keyboard_release(void); 86 | 87 | /** 88 | * @brief Get the pubsub object for the remote keyboard. 89 | * @details This function returns the pubsub object, use to subscribe to keyboard events. 90 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 91 | * @return FuriPubSub* pointer to the pubsub object. 92 | */ 93 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard); 94 | 95 | /** 96 | * @brief Enable or disable newline character submitting the text. 97 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 98 | * @param[in] enable true to enable, false to disable. 99 | */ 100 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable); 101 | 102 | /** 103 | * @brief Publish the replacement text to the remote keyboard. 104 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 105 | * @param[in] bytes pointer to the text buffer. 106 | * @param[in] buffer_size size of the text buffer. 107 | */ 108 | void rpc_keyboard_publish_text(RpcKeyboard* rpc_keyboard, uint8_t* bytes, uint32_t buffer_size); 109 | 110 | /** 111 | * @brief Publish a single key pressed on the remote keyboard. 112 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 113 | * @param[in] character the character that was pressed. 114 | */ 115 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character); 116 | 117 | /** 118 | * @brief Publish a macro key pressed on the remote keyboard. 119 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 120 | * @param[in] character the macro key that was pressed. 121 | */ 122 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro); 123 | 124 | /** 125 | * @brief Get the macro text associated with a macro key. 126 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 127 | * @param[in] macro the macro key. 128 | * @return char* pointer to the macro text. NULL if the macro key is not set. User must free the memory. 129 | */ 130 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro); 131 | 132 | /** 133 | * @brief Set the macro text associated with a macro key. 134 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 135 | * @param[in] macro the macro key. 136 | * @param[in] value the macro text. 137 | */ 138 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value); 139 | 140 | /** 141 | * @brief Initializes the chatpad and starts listening for keypresses. 142 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 143 | */ 144 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard); 145 | 146 | /** 147 | * @brief Stops the chatpad & frees resources. 148 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 149 | */ 150 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard); 151 | 152 | /** 153 | * @brief Get the status of the chatpad. 154 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 155 | * @return RpcKeyboardChatpadStatus the status of the chatpad. 156 | */ 157 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard); 158 | -------------------------------------------------------------------------------- /firmware-overlay/ofw-1.1.2/applications/services/rpc/rpc_keyboard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define RECORD_RPC_KEYBOARD "rpckeyboard" 8 | 9 | #define RPC_KEYBOARD_KEY_RIGHT '\x13' 10 | #define RPC_KEYBOARD_KEY_LEFT '\x14' 11 | #define RPC_KEYBOARD_KEY_ENTER '\x0D' 12 | #define RPC_KEYBOARD_KEY_BACKSPACE '\x08' 13 | 14 | typedef enum { 15 | // Unknown error occurred 16 | RpcKeyboardChatpadStatusError, 17 | // The chatpad worker is stopped 18 | RpcKeyboardChatpadStatusStopped, 19 | // The chatpad worker is started, but not ready 20 | RpcKeyboardChatpadStatusStarted, 21 | // The chatpad worker is ready and got response from chatpad 22 | RpcKeyboardChatpadStatusReady, 23 | } RpcKeyboardChatpadStatus; 24 | 25 | typedef struct RpcKeyboard RpcKeyboard; 26 | 27 | typedef enum { 28 | // Replacement text was provided by the user 29 | RpcKeyboardEventTypeTextEntered, 30 | // A single character was provided by the user 31 | RpcKeyboardEventTypeCharEntered, 32 | // A macro was entered by the user 33 | RpcKeyboardEventTypeMacroEntered, 34 | } RpcKeyboardEventType; 35 | 36 | typedef struct { 37 | // The mutex to protect the data, call furi_mutex_acquire/furi_mutex_release. 38 | FuriMutex* mutex; 39 | // The text message, macro or character. 40 | char message[256]; 41 | // The length of the message. 42 | uint16_t length; 43 | // The newline enabled flag, allow newline to submit text. 44 | bool newline_enabled; 45 | } RpcKeyboardEventData; 46 | 47 | typedef struct { 48 | RpcKeyboardEventType type; 49 | RpcKeyboardEventData data; 50 | } RpcKeyboardEvent; 51 | 52 | typedef FuriPubSub* (*RpcKeyboardGetPubsub)(RpcKeyboard* rpc_keyboard); 53 | typedef void (*RpcKeyboardNewlineEnable)(RpcKeyboard* rpc_keyboard, bool enable); 54 | typedef void (*RpcKeyboardPublishCharFn)(RpcKeyboard* keyboard, char character); 55 | typedef void (*RpcKeyboardPublishMacroFn)(RpcKeyboard* rpc_keyboard, char macro); 56 | typedef char* (*RpcKeyboardGetMacroFn)(RpcKeyboard* rpc_keyboard, char macro); 57 | typedef void (*RpcKeyboardSetMacroFn)(RpcKeyboard* rpc_keyboard, char macro, char* value); 58 | typedef void (*RpcKeyboardChatpadStartFn)(RpcKeyboard* rpc_keyboard); 59 | typedef void (*RpcKeyboardChatpadStopFn)(RpcKeyboard* rpc_keyboard); 60 | typedef RpcKeyboardChatpadStatus (*RpcKeyboardChatpadStatusFn)(RpcKeyboard* rpc_keyboard); 61 | 62 | typedef struct RpcKeyboardFunctions RpcKeyboardFunctions; 63 | struct RpcKeyboardFunctions { 64 | uint16_t major; 65 | uint16_t minor; 66 | RpcKeyboardGetPubsub fn_get_pubsub; 67 | RpcKeyboardNewlineEnable fn_newline_enable; 68 | RpcKeyboardPublishCharFn fn_publish_char; 69 | RpcKeyboardPublishMacroFn fn_publish_macro; 70 | RpcKeyboardGetMacroFn fn_get_macro; 71 | RpcKeyboardSetMacroFn fn_set_macro; 72 | RpcKeyboardChatpadStartFn fn_chatpad_start; 73 | RpcKeyboardChatpadStopFn fn_chatpad_stop; 74 | RpcKeyboardChatpadStatusFn fn_chatpad_status; 75 | }; 76 | 77 | /** 78 | * @brief STARTUP - Register the remote keyboard. 79 | */ 80 | void rpc_keyboard_register(void); 81 | 82 | /** 83 | * @brief UNUSED - Unregister the remote keyboard. 84 | */ 85 | void rpc_keyboard_release(void); 86 | 87 | /** 88 | * @brief Get the pubsub object for the remote keyboard. 89 | * @details This function returns the pubsub object, use to subscribe to keyboard events. 90 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 91 | * @return FuriPubSub* pointer to the pubsub object. 92 | */ 93 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard); 94 | 95 | /** 96 | * @brief Enable or disable newline character submitting the text. 97 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 98 | * @param[in] enable true to enable, false to disable. 99 | */ 100 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable); 101 | 102 | /** 103 | * @brief Publish the replacement text to the remote keyboard. 104 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 105 | * @param[in] bytes pointer to the text buffer. 106 | * @param[in] buffer_size size of the text buffer. 107 | */ 108 | void rpc_keyboard_publish_text(RpcKeyboard* rpc_keyboard, uint8_t* bytes, uint32_t buffer_size); 109 | 110 | /** 111 | * @brief Publish a single key pressed on the remote keyboard. 112 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 113 | * @param[in] character the character that was pressed. 114 | */ 115 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character); 116 | 117 | /** 118 | * @brief Publish a macro key pressed on the remote keyboard. 119 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 120 | * @param[in] character the macro key that was pressed. 121 | */ 122 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro); 123 | 124 | /** 125 | * @brief Get the macro text associated with a macro key. 126 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 127 | * @param[in] macro the macro key. 128 | * @return char* pointer to the macro text. NULL if the macro key is not set. User must free the memory. 129 | */ 130 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro); 131 | 132 | /** 133 | * @brief Set the macro text associated with a macro key. 134 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 135 | * @param[in] macro the macro key. 136 | * @param[in] value the macro text. 137 | */ 138 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value); 139 | 140 | /** 141 | * @brief Initializes the chatpad and starts listening for keypresses. 142 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 143 | */ 144 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard); 145 | 146 | /** 147 | * @brief Stops the chatpad & frees resources. 148 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 149 | */ 150 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard); 151 | 152 | /** 153 | * @brief Get the status of the chatpad. 154 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 155 | * @return RpcKeyboardChatpadStatus the status of the chatpad. 156 | */ 157 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard); 158 | -------------------------------------------------------------------------------- /firmware-overlay/rm-1202-0837-0.420.0-6d10bad/applications/services/rpc/rpc_keyboard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define RECORD_RPC_KEYBOARD "rpckeyboard" 8 | 9 | #define RPC_KEYBOARD_KEY_RIGHT '\x13' 10 | #define RPC_KEYBOARD_KEY_LEFT '\x14' 11 | #define RPC_KEYBOARD_KEY_ENTER '\x0D' 12 | #define RPC_KEYBOARD_KEY_BACKSPACE '\x08' 13 | 14 | typedef enum { 15 | // Unknown error occurred 16 | RpcKeyboardChatpadStatusError, 17 | // The chatpad worker is stopped 18 | RpcKeyboardChatpadStatusStopped, 19 | // The chatpad worker is started, but not ready 20 | RpcKeyboardChatpadStatusStarted, 21 | // The chatpad worker is ready and got response from chatpad 22 | RpcKeyboardChatpadStatusReady, 23 | } RpcKeyboardChatpadStatus; 24 | 25 | typedef struct RpcKeyboard RpcKeyboard; 26 | 27 | typedef enum { 28 | // Replacement text was provided by the user 29 | RpcKeyboardEventTypeTextEntered, 30 | // A single character was provided by the user 31 | RpcKeyboardEventTypeCharEntered, 32 | // A macro was entered by the user 33 | RpcKeyboardEventTypeMacroEntered, 34 | } RpcKeyboardEventType; 35 | 36 | typedef struct { 37 | // The mutex to protect the data, call furi_mutex_acquire/furi_mutex_release. 38 | FuriMutex* mutex; 39 | // The text message, macro or character. 40 | char message[256]; 41 | // The length of the message. 42 | uint16_t length; 43 | // The newline enabled flag, allow newline to submit text. 44 | bool newline_enabled; 45 | } RpcKeyboardEventData; 46 | 47 | typedef struct { 48 | RpcKeyboardEventType type; 49 | RpcKeyboardEventData data; 50 | } RpcKeyboardEvent; 51 | 52 | typedef FuriPubSub* (*RpcKeyboardGetPubsub)(RpcKeyboard* rpc_keyboard); 53 | typedef void (*RpcKeyboardNewlineEnable)(RpcKeyboard* rpc_keyboard, bool enable); 54 | typedef void (*RpcKeyboardPublishCharFn)(RpcKeyboard* keyboard, char character); 55 | typedef void (*RpcKeyboardPublishMacroFn)(RpcKeyboard* rpc_keyboard, char macro); 56 | typedef char* (*RpcKeyboardGetMacroFn)(RpcKeyboard* rpc_keyboard, char macro); 57 | typedef void (*RpcKeyboardSetMacroFn)(RpcKeyboard* rpc_keyboard, char macro, char* value); 58 | typedef void (*RpcKeyboardChatpadStartFn)(RpcKeyboard* rpc_keyboard); 59 | typedef void (*RpcKeyboardChatpadStopFn)(RpcKeyboard* rpc_keyboard); 60 | typedef RpcKeyboardChatpadStatus (*RpcKeyboardChatpadStatusFn)(RpcKeyboard* rpc_keyboard); 61 | 62 | typedef struct RpcKeyboardFunctions RpcKeyboardFunctions; 63 | struct RpcKeyboardFunctions { 64 | uint16_t major; 65 | uint16_t minor; 66 | RpcKeyboardGetPubsub fn_get_pubsub; 67 | RpcKeyboardNewlineEnable fn_newline_enable; 68 | RpcKeyboardPublishCharFn fn_publish_char; 69 | RpcKeyboardPublishMacroFn fn_publish_macro; 70 | RpcKeyboardGetMacroFn fn_get_macro; 71 | RpcKeyboardSetMacroFn fn_set_macro; 72 | RpcKeyboardChatpadStartFn fn_chatpad_start; 73 | RpcKeyboardChatpadStopFn fn_chatpad_stop; 74 | RpcKeyboardChatpadStatusFn fn_chatpad_status; 75 | }; 76 | 77 | /** 78 | * @brief STARTUP - Register the remote keyboard. 79 | */ 80 | void rpc_keyboard_register(void); 81 | 82 | /** 83 | * @brief UNUSED - Unregister the remote keyboard. 84 | */ 85 | void rpc_keyboard_release(void); 86 | 87 | /** 88 | * @brief Get the pubsub object for the remote keyboard. 89 | * @details This function returns the pubsub object, use to subscribe to keyboard events. 90 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 91 | * @return FuriPubSub* pointer to the pubsub object. 92 | */ 93 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard); 94 | 95 | /** 96 | * @brief Enable or disable newline character submitting the text. 97 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 98 | * @param[in] enable true to enable, false to disable. 99 | */ 100 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable); 101 | 102 | /** 103 | * @brief Publish the replacement text to the remote keyboard. 104 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 105 | * @param[in] bytes pointer to the text buffer. 106 | * @param[in] buffer_size size of the text buffer. 107 | */ 108 | void rpc_keyboard_publish_text(RpcKeyboard* rpc_keyboard, uint8_t* bytes, uint32_t buffer_size); 109 | 110 | /** 111 | * @brief Publish a single key pressed on the remote keyboard. 112 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 113 | * @param[in] character the character that was pressed. 114 | */ 115 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character); 116 | 117 | /** 118 | * @brief Publish a macro key pressed on the remote keyboard. 119 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 120 | * @param[in] character the macro key that was pressed. 121 | */ 122 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro); 123 | 124 | /** 125 | * @brief Get the macro text associated with a macro key. 126 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 127 | * @param[in] macro the macro key. 128 | * @return char* pointer to the macro text. NULL if the macro key is not set. User must free the memory. 129 | */ 130 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro); 131 | 132 | /** 133 | * @brief Set the macro text associated with a macro key. 134 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 135 | * @param[in] macro the macro key. 136 | * @param[in] value the macro text. 137 | */ 138 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value); 139 | 140 | /** 141 | * @brief Initializes the chatpad and starts listening for keypresses. 142 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 143 | */ 144 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard); 145 | 146 | /** 147 | * @brief Stops the chatpad & frees resources. 148 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 149 | */ 150 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard); 151 | 152 | /** 153 | * @brief Get the status of the chatpad. 154 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 155 | * @return RpcKeyboardChatpadStatus the status of the chatpad. 156 | */ 157 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard); 158 | -------------------------------------------------------------------------------- /firmware-overlay/rm-1202-0837-0.420.0-6d10bad/applications/services/rpc/rpc_keyboard.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define TAG "RpcKeyboard" 9 | 10 | struct RpcKeyboard { 11 | RpcKeyboardFunctions funcs; 12 | FuriPubSub* event_pubsub; 13 | RpcKeyboardEvent event; 14 | RpcChatpad* chatpad; 15 | FuriString* macros; 16 | }; 17 | 18 | void rpc_keyboard_register(void) { 19 | RpcKeyboard* keyboard = malloc(sizeof(RpcKeyboard)); 20 | keyboard->event = (RpcKeyboardEvent){ 21 | .data.newline_enabled = true, 22 | .data.length = 0, 23 | .data.mutex = furi_mutex_alloc(FuriMutexTypeRecursive)}; 24 | keyboard->event_pubsub = furi_pubsub_alloc(); 25 | keyboard->chatpad = NULL; 26 | keyboard->macros = furi_string_alloc(); 27 | keyboard->funcs.major = 1; 28 | keyboard->funcs.minor = 6; 29 | keyboard->funcs.fn_get_pubsub = rpc_keyboard_get_pubsub; 30 | keyboard->funcs.fn_newline_enable = rpc_keyboard_newline_enable; 31 | keyboard->funcs.fn_publish_char = rpc_keyboard_publish_char; 32 | keyboard->funcs.fn_publish_macro = rpc_keyboard_publish_macro; 33 | keyboard->funcs.fn_get_macro = rpc_keyboard_get_macro; 34 | keyboard->funcs.fn_set_macro = rpc_keyboard_set_macro; 35 | keyboard->funcs.fn_chatpad_start = rpc_keyboard_chatpad_start; 36 | keyboard->funcs.fn_chatpad_stop = rpc_keyboard_chatpad_stop; 37 | keyboard->funcs.fn_chatpad_status = rpc_keyboard_chatpad_status; 38 | furi_record_create(RECORD_RPC_KEYBOARD, keyboard); 39 | } 40 | 41 | void rpc_keyboard_release(void) { 42 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 43 | if(!rpc_keyboard) { 44 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 45 | return; 46 | } 47 | furi_pubsub_free(rpc_keyboard->event_pubsub); 48 | furi_mutex_free(rpc_keyboard->event.data.mutex); 49 | if(rpc_keyboard->chatpad) { 50 | rpc_chatpad_free(rpc_keyboard->chatpad); 51 | } 52 | furi_string_free(rpc_keyboard->macros); 53 | furi_record_destroy(RECORD_RPC_KEYBOARD); 54 | } 55 | 56 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard) { 57 | if(rpc_keyboard) { 58 | return rpc_keyboard->event_pubsub; 59 | } else { 60 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 61 | return NULL; 62 | } 63 | } 64 | 65 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable) { 66 | if(rpc_keyboard) { 67 | rpc_keyboard->event.data.newline_enabled = enable; 68 | } else { 69 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 70 | } 71 | } 72 | 73 | /** 74 | * @brief Internal API - publishes a remote keyboard event. 75 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 76 | * @param[in] bytes the data to publish. 77 | * @param[in] buffer_size the size of the bytes. 78 | * @param[in] type the type of the event. 79 | */ 80 | static void rpc_keyboard_publish( 81 | RpcKeyboard* rpc_keyboard, 82 | uint8_t* bytes, 83 | uint32_t buffer_size, 84 | RpcKeyboardEventType type) { 85 | if(!rpc_keyboard) { 86 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 87 | return; 88 | } 89 | furi_mutex_acquire(rpc_keyboard->event.data.mutex, FuriWaitForever); 90 | if(buffer_size > sizeof(rpc_keyboard->event.data.message) - 1) { 91 | buffer_size = sizeof(rpc_keyboard->event.data.message) - 1; 92 | } 93 | strncpy(rpc_keyboard->event.data.message, (const char*)bytes, buffer_size); 94 | rpc_keyboard->event.data.length = buffer_size; 95 | rpc_keyboard->event.data.message[rpc_keyboard->event.data.length] = '\0'; 96 | rpc_keyboard->event.type = type; 97 | furi_mutex_release(rpc_keyboard->event.data.mutex); 98 | 99 | if(rpc_keyboard->event_pubsub) { 100 | furi_pubsub_publish(rpc_keyboard->event_pubsub, &rpc_keyboard->event); 101 | } 102 | } 103 | 104 | void rpc_keyboard_publish_text(RpcKeyboard* rpc_keyboard, uint8_t* bytes, uint32_t buffer_size) { 105 | rpc_keyboard_publish(rpc_keyboard, bytes, buffer_size, RpcKeyboardEventTypeTextEntered); 106 | } 107 | 108 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character) { 109 | rpc_keyboard_publish(rpc_keyboard, (uint8_t*)&character, 1, RpcKeyboardEventTypeCharEntered); 110 | } 111 | 112 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro) { 113 | if(isupper(macro)) { // our macro table is lowercase 114 | macro = tolower(macro); 115 | } 116 | char* macro_text = rpc_keyboard_get_macro(rpc_keyboard, macro); 117 | if(macro_text == NULL) { 118 | macro_text = malloc(sizeof(char) * 2); 119 | macro_text[0] = macro; // send the macro key if no macro is set 120 | macro_text[1] = '\0'; 121 | } 122 | 123 | rpc_keyboard_publish( 124 | rpc_keyboard, (uint8_t*)macro_text, strlen(macro_text), RpcKeyboardEventTypeMacroEntered); 125 | free(macro_text); 126 | } 127 | 128 | /** 129 | * @brief Internal API - searches for the start of a macro in the macro string. 130 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 131 | * @param[in] macro the macro key to search for. 132 | * @return size_t the index of the start of the macro in the string. 133 | */ 134 | static size_t rpc_keyboard_get_macro_start_index(RpcKeyboard* rpc_keyboard, char macro) { 135 | furi_check(rpc_keyboard); 136 | char macro_start[3] = {'\x80', macro, '\0'}; 137 | return furi_string_search_str(rpc_keyboard->macros, macro_start, 0); 138 | } 139 | static const char macro_end[2] = {'\x81', '\0'}; 140 | 141 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro) { 142 | if(!rpc_keyboard) { 143 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 144 | return NULL; 145 | } 146 | char* macro_text = NULL; 147 | size_t index_start = rpc_keyboard_get_macro_start_index(rpc_keyboard, macro); 148 | if(index_start != FURI_STRING_FAILURE) { 149 | size_t index_end = furi_string_search_str(rpc_keyboard->macros, macro_end, index_start); 150 | if(index_end != FURI_STRING_FAILURE) { 151 | size_t len = index_end - index_start - 2; 152 | macro_text = malloc(sizeof(char) * (len + 1)); 153 | strncpy(macro_text, furi_string_get_cstr(rpc_keyboard->macros) + index_start + 2, len); 154 | macro_text[len] = '\0'; 155 | } 156 | } 157 | FURI_LOG_D( 158 | TAG, 159 | "Get Macro %c: %d %s >%s", 160 | macro, 161 | index_start, 162 | macro_text ? macro_text : "NULL", 163 | furi_string_get_cstr(rpc_keyboard->macros)); 164 | return macro_text; 165 | } 166 | 167 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value) { 168 | if(!rpc_keyboard) { 169 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 170 | return; 171 | } 172 | size_t index_start = rpc_keyboard_get_macro_start_index(rpc_keyboard, macro); 173 | if(index_start != FURI_STRING_FAILURE) { 174 | size_t index_end = furi_string_search_str(rpc_keyboard->macros, macro_end, index_start); 175 | if(index_end != FURI_STRING_FAILURE) { 176 | size_t len = index_end - index_start + 1; 177 | furi_string_replace_at(rpc_keyboard->macros, index_start, len, ""); 178 | } 179 | } 180 | if(strlen(value) != 0) { 181 | furi_string_cat_printf(rpc_keyboard->macros, "\x80%c%s\x81", macro, value); 182 | } 183 | FURI_LOG_D( 184 | TAG, 185 | "Set Macro %c: %d %s >%s", 186 | macro, 187 | index_start, 188 | value ? value : "NULL", 189 | furi_string_get_cstr(rpc_keyboard->macros)); 190 | } 191 | 192 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard) { 193 | if(!rpc_keyboard) { 194 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 195 | return; 196 | } 197 | if(rpc_keyboard->chatpad) { 198 | rpc_keyboard_chatpad_stop(rpc_keyboard); 199 | } 200 | rpc_keyboard->chatpad = rpc_chatpad_alloc(); 201 | } 202 | 203 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard) { 204 | if(!rpc_keyboard) { 205 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 206 | return; 207 | } 208 | rpc_chatpad_free(rpc_keyboard->chatpad); 209 | rpc_keyboard->chatpad = NULL; 210 | } 211 | 212 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard) { 213 | if(!rpc_keyboard) { 214 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 215 | return RpcKeyboardChatpadStatusError; 216 | } 217 | if(rpc_keyboard->chatpad == NULL) { 218 | return RpcKeyboardChatpadStatusStopped; 219 | } 220 | if(rpc_chatpad_ready(rpc_keyboard->chatpad)) { 221 | return RpcKeyboardChatpadStatusReady; 222 | } else { 223 | return RpcKeyboardChatpadStatusStarted; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /firmware-overlay/mntm-008/applications/services/rpc/rpc_keyboard.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define TAG "RpcKeyboard" 9 | 10 | struct RpcKeyboard { 11 | RpcKeyboardFunctions funcs; 12 | FuriPubSub* event_pubsub; 13 | RpcKeyboardEvent event; 14 | RpcChatpad* chatpad; 15 | FuriString* macros; 16 | }; 17 | 18 | void rpc_keyboard_register(void) { 19 | RpcKeyboard* keyboard = malloc(sizeof(RpcKeyboard)); 20 | keyboard->event = (RpcKeyboardEvent){ 21 | .data.newline_enabled = true, 22 | .data.length = 0, 23 | .data.mutex = furi_mutex_alloc(FuriMutexTypeRecursive)}; 24 | keyboard->event_pubsub = furi_pubsub_alloc(); 25 | keyboard->chatpad = NULL; 26 | keyboard->macros = furi_string_alloc(); 27 | keyboard->funcs.major = 1; 28 | keyboard->funcs.minor = 6; 29 | keyboard->funcs.fn_get_pubsub = rpc_keyboard_get_pubsub; 30 | keyboard->funcs.fn_newline_enable = rpc_keyboard_newline_enable; 31 | keyboard->funcs.fn_publish_char = rpc_keyboard_publish_char; 32 | keyboard->funcs.fn_publish_macro = rpc_keyboard_publish_macro; 33 | keyboard->funcs.fn_get_macro = rpc_keyboard_get_macro; 34 | keyboard->funcs.fn_set_macro = rpc_keyboard_set_macro; 35 | keyboard->funcs.fn_chatpad_start = rpc_keyboard_chatpad_start; 36 | keyboard->funcs.fn_chatpad_stop = rpc_keyboard_chatpad_stop; 37 | keyboard->funcs.fn_chatpad_status = rpc_keyboard_chatpad_status; 38 | furi_record_create(RECORD_RPC_KEYBOARD, keyboard); 39 | } 40 | 41 | void rpc_keyboard_release(void) { 42 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 43 | if(!rpc_keyboard) { 44 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 45 | return; 46 | } 47 | furi_pubsub_free(rpc_keyboard->event_pubsub); 48 | furi_mutex_free(rpc_keyboard->event.data.mutex); 49 | if(rpc_keyboard->chatpad) { 50 | rpc_chatpad_free(rpc_keyboard->chatpad); 51 | } 52 | furi_string_free(rpc_keyboard->macros); 53 | furi_record_destroy(RECORD_RPC_KEYBOARD); 54 | } 55 | 56 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard) { 57 | if(rpc_keyboard) { 58 | return rpc_keyboard->event_pubsub; 59 | } else { 60 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 61 | return NULL; 62 | } 63 | } 64 | 65 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable) { 66 | if(rpc_keyboard) { 67 | rpc_keyboard->event.data.newline_enabled = enable; 68 | } else { 69 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 70 | } 71 | } 72 | 73 | /** 74 | * @brief Internal API - publishes a remote keyboard event. 75 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 76 | * @param[in] bytes the data to publish. 77 | * @param[in] buffer_size the size of the bytes. 78 | * @param[in] type the type of the event. 79 | */ 80 | static void rpc_keyboard_publish( 81 | RpcKeyboard* rpc_keyboard, 82 | uint8_t* bytes, 83 | uint32_t buffer_size, 84 | RpcKeyboardEventType type) { 85 | if(!rpc_keyboard) { 86 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 87 | return; 88 | } 89 | furi_mutex_acquire(rpc_keyboard->event.data.mutex, FuriWaitForever); 90 | if(buffer_size > sizeof(rpc_keyboard->event.data.message) - 1) { 91 | buffer_size = sizeof(rpc_keyboard->event.data.message) - 1; 92 | } 93 | strncpy(rpc_keyboard->event.data.message, (const char*)bytes, buffer_size); 94 | rpc_keyboard->event.data.length = buffer_size; 95 | rpc_keyboard->event.data.message[rpc_keyboard->event.data.length] = '\0'; 96 | for(uint32_t i = 0; i < rpc_keyboard->event.data.length; i++) { 97 | if(rpc_keyboard->event.data.message[i] == '\r' || 98 | rpc_keyboard->event.data.message[i] == '\n') { 99 | rpc_keyboard->event.data.message[i] = '\r'; // text_input.c: #define ENTER_KEY '\r' 100 | rpc_keyboard->event.data.length = i + 1; 101 | rpc_keyboard->event.data.message[rpc_keyboard->event.data.length] = '\0'; 102 | break; 103 | } 104 | } 105 | rpc_keyboard->event.type = type; 106 | furi_mutex_release(rpc_keyboard->event.data.mutex); 107 | 108 | if(rpc_keyboard->event_pubsub) { 109 | furi_pubsub_publish(rpc_keyboard->event_pubsub, &rpc_keyboard->event); 110 | } 111 | } 112 | 113 | void rpc_keyboard_publish_text(RpcKeyboard* rpc_keyboard, uint8_t* bytes, uint32_t buffer_size) { 114 | rpc_keyboard_publish(rpc_keyboard, bytes, buffer_size, RpcKeyboardEventTypeTextEntered); 115 | } 116 | 117 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character) { 118 | rpc_keyboard_publish(rpc_keyboard, (uint8_t*)&character, 1, RpcKeyboardEventTypeCharEntered); 119 | } 120 | 121 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro) { 122 | if(isupper(macro)) { // our macro table is lowercase 123 | macro = tolower(macro); 124 | } 125 | char* macro_text = rpc_keyboard_get_macro(rpc_keyboard, macro); 126 | if(macro_text == NULL) { 127 | macro_text = malloc(sizeof(char) * 2); 128 | macro_text[0] = macro; // send the macro key if no macro is set 129 | macro_text[1] = '\0'; 130 | } 131 | 132 | rpc_keyboard_publish( 133 | rpc_keyboard, (uint8_t*)macro_text, strlen(macro_text), RpcKeyboardEventTypeMacroEntered); 134 | free(macro_text); 135 | } 136 | 137 | /** 138 | * @brief Internal API - searches for the start of a macro in the macro string. 139 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 140 | * @param[in] macro the macro key to search for. 141 | * @return size_t the index of the start of the macro in the string. 142 | */ 143 | static size_t rpc_keyboard_get_macro_start_index(RpcKeyboard* rpc_keyboard, char macro) { 144 | furi_check(rpc_keyboard); 145 | char macro_start[3] = {'\x80', macro, '\0'}; 146 | return furi_string_search_str(rpc_keyboard->macros, macro_start, 0); 147 | } 148 | static const char macro_end[2] = {'\x81', '\0'}; 149 | 150 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro) { 151 | if(!rpc_keyboard) { 152 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 153 | return NULL; 154 | } 155 | char* macro_text = NULL; 156 | size_t index_start = rpc_keyboard_get_macro_start_index(rpc_keyboard, macro); 157 | if(index_start != FURI_STRING_FAILURE) { 158 | size_t index_end = furi_string_search_str(rpc_keyboard->macros, macro_end, index_start); 159 | if(index_end != FURI_STRING_FAILURE) { 160 | size_t len = index_end - index_start - 2; 161 | macro_text = malloc(sizeof(char) * (len + 1)); 162 | strncpy(macro_text, furi_string_get_cstr(rpc_keyboard->macros) + index_start + 2, len); 163 | macro_text[len] = '\0'; 164 | } 165 | } 166 | FURI_LOG_D( 167 | TAG, 168 | "Get Macro %c: %d %s >%s", 169 | macro, 170 | index_start, 171 | macro_text ? macro_text : "NULL", 172 | furi_string_get_cstr(rpc_keyboard->macros)); 173 | return macro_text; 174 | } 175 | 176 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value) { 177 | if(!rpc_keyboard) { 178 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 179 | return; 180 | } 181 | size_t index_start = rpc_keyboard_get_macro_start_index(rpc_keyboard, macro); 182 | if(index_start != FURI_STRING_FAILURE) { 183 | size_t index_end = furi_string_search_str(rpc_keyboard->macros, macro_end, index_start); 184 | if(index_end != FURI_STRING_FAILURE) { 185 | size_t len = index_end - index_start + 1; 186 | furi_string_replace_at(rpc_keyboard->macros, index_start, len, ""); 187 | } 188 | } 189 | if(strlen(value) != 0) { 190 | furi_string_cat_printf(rpc_keyboard->macros, "\x80%c%s\x81", macro, value); 191 | } 192 | FURI_LOG_D( 193 | TAG, 194 | "Set Macro %c: %d %s >%s", 195 | macro, 196 | index_start, 197 | value ? value : "NULL", 198 | furi_string_get_cstr(rpc_keyboard->macros)); 199 | } 200 | 201 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard) { 202 | if(!rpc_keyboard) { 203 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 204 | return; 205 | } 206 | if(rpc_keyboard->chatpad) { 207 | rpc_keyboard_chatpad_stop(rpc_keyboard); 208 | } 209 | rpc_keyboard->chatpad = rpc_chatpad_alloc(); 210 | } 211 | 212 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard) { 213 | if(!rpc_keyboard) { 214 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 215 | return; 216 | } 217 | rpc_chatpad_free(rpc_keyboard->chatpad); 218 | rpc_keyboard->chatpad = NULL; 219 | } 220 | 221 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard) { 222 | if(!rpc_keyboard) { 223 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 224 | return RpcKeyboardChatpadStatusError; 225 | } 226 | if(rpc_keyboard->chatpad == NULL) { 227 | return RpcKeyboardChatpadStatusStopped; 228 | } 229 | if(rpc_chatpad_ready(rpc_keyboard->chatpad)) { 230 | return RpcKeyboardChatpadStatusReady; 231 | } else { 232 | return RpcKeyboardChatpadStatusStarted; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /firmware-overlay/unl-079/applications/services/rpc/rpc_keyboard.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define TAG "RpcKeyboard" 9 | 10 | struct RpcKeyboard { 11 | RpcKeyboardFunctions funcs; 12 | FuriPubSub* event_pubsub; 13 | RpcKeyboardEvent event; 14 | RpcChatpad* chatpad; 15 | FuriString* macros; 16 | }; 17 | 18 | void rpc_keyboard_register(void) { 19 | RpcKeyboard* keyboard = malloc(sizeof(RpcKeyboard)); 20 | keyboard->event = (RpcKeyboardEvent){ 21 | .data.newline_enabled = true, 22 | .data.length = 0, 23 | .data.mutex = furi_mutex_alloc(FuriMutexTypeRecursive)}; 24 | keyboard->event_pubsub = furi_pubsub_alloc(); 25 | keyboard->chatpad = NULL; 26 | keyboard->macros = furi_string_alloc(); 27 | keyboard->funcs.major = 1; 28 | keyboard->funcs.minor = 6; 29 | keyboard->funcs.fn_get_pubsub = rpc_keyboard_get_pubsub; 30 | keyboard->funcs.fn_newline_enable = rpc_keyboard_newline_enable; 31 | keyboard->funcs.fn_publish_char = rpc_keyboard_publish_char; 32 | keyboard->funcs.fn_publish_macro = rpc_keyboard_publish_macro; 33 | keyboard->funcs.fn_get_macro = rpc_keyboard_get_macro; 34 | keyboard->funcs.fn_set_macro = rpc_keyboard_set_macro; 35 | keyboard->funcs.fn_chatpad_start = rpc_keyboard_chatpad_start; 36 | keyboard->funcs.fn_chatpad_stop = rpc_keyboard_chatpad_stop; 37 | keyboard->funcs.fn_chatpad_status = rpc_keyboard_chatpad_status; 38 | furi_record_create(RECORD_RPC_KEYBOARD, keyboard); 39 | } 40 | 41 | void rpc_keyboard_release(void) { 42 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 43 | if(!rpc_keyboard) { 44 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 45 | return; 46 | } 47 | furi_pubsub_free(rpc_keyboard->event_pubsub); 48 | furi_mutex_free(rpc_keyboard->event.data.mutex); 49 | if(rpc_keyboard->chatpad) { 50 | rpc_chatpad_free(rpc_keyboard->chatpad); 51 | } 52 | furi_string_free(rpc_keyboard->macros); 53 | furi_record_destroy(RECORD_RPC_KEYBOARD); 54 | } 55 | 56 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard) { 57 | if(rpc_keyboard) { 58 | return rpc_keyboard->event_pubsub; 59 | } else { 60 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 61 | return NULL; 62 | } 63 | } 64 | 65 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable) { 66 | if(rpc_keyboard) { 67 | rpc_keyboard->event.data.newline_enabled = enable; 68 | } else { 69 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 70 | } 71 | } 72 | 73 | /** 74 | * @brief Internal API - publishes a remote keyboard event. 75 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 76 | * @param[in] bytes the data to publish. 77 | * @param[in] buffer_size the size of the bytes. 78 | * @param[in] type the type of the event. 79 | */ 80 | static void rpc_keyboard_publish( 81 | RpcKeyboard* rpc_keyboard, 82 | uint8_t* bytes, 83 | uint32_t buffer_size, 84 | RpcKeyboardEventType type) { 85 | if(!rpc_keyboard) { 86 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 87 | return; 88 | } 89 | furi_mutex_acquire(rpc_keyboard->event.data.mutex, FuriWaitForever); 90 | if(buffer_size > sizeof(rpc_keyboard->event.data.message) - 1) { 91 | buffer_size = sizeof(rpc_keyboard->event.data.message) - 1; 92 | } 93 | strncpy(rpc_keyboard->event.data.message, (const char*)bytes, buffer_size); 94 | rpc_keyboard->event.data.length = buffer_size; 95 | rpc_keyboard->event.data.message[rpc_keyboard->event.data.length] = '\0'; 96 | for(uint32_t i = 0; i < rpc_keyboard->event.data.length; i++) { 97 | if(rpc_keyboard->event.data.message[i] == '\r' || 98 | rpc_keyboard->event.data.message[i] == '\n') { 99 | rpc_keyboard->event.data.message[i] = '\r'; // text_input.c: #define ENTER_KEY '\r' 100 | rpc_keyboard->event.data.length = i + 1; 101 | rpc_keyboard->event.data.message[rpc_keyboard->event.data.length] = '\0'; 102 | break; 103 | } 104 | } 105 | rpc_keyboard->event.type = type; 106 | furi_mutex_release(rpc_keyboard->event.data.mutex); 107 | 108 | if(rpc_keyboard->event_pubsub) { 109 | furi_pubsub_publish(rpc_keyboard->event_pubsub, &rpc_keyboard->event); 110 | } 111 | } 112 | 113 | void rpc_keyboard_publish_text(RpcKeyboard* rpc_keyboard, uint8_t* bytes, uint32_t buffer_size) { 114 | rpc_keyboard_publish(rpc_keyboard, bytes, buffer_size, RpcKeyboardEventTypeTextEntered); 115 | } 116 | 117 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character) { 118 | rpc_keyboard_publish(rpc_keyboard, (uint8_t*)&character, 1, RpcKeyboardEventTypeCharEntered); 119 | } 120 | 121 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro) { 122 | if(isupper(macro)) { // our macro table is lowercase 123 | macro = tolower(macro); 124 | } 125 | char* macro_text = rpc_keyboard_get_macro(rpc_keyboard, macro); 126 | if(macro_text == NULL) { 127 | macro_text = malloc(sizeof(char) * 2); 128 | macro_text[0] = macro; // send the macro key if no macro is set 129 | macro_text[1] = '\0'; 130 | } 131 | 132 | rpc_keyboard_publish( 133 | rpc_keyboard, (uint8_t*)macro_text, strlen(macro_text), RpcKeyboardEventTypeMacroEntered); 134 | free(macro_text); 135 | } 136 | 137 | /** 138 | * @brief Internal API - searches for the start of a macro in the macro string. 139 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 140 | * @param[in] macro the macro key to search for. 141 | * @return size_t the index of the start of the macro in the string. 142 | */ 143 | static size_t rpc_keyboard_get_macro_start_index(RpcKeyboard* rpc_keyboard, char macro) { 144 | furi_check(rpc_keyboard); 145 | char macro_start[3] = {'\x80', macro, '\0'}; 146 | return furi_string_search_str(rpc_keyboard->macros, macro_start, 0); 147 | } 148 | static const char macro_end[2] = {'\x81', '\0'}; 149 | 150 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro) { 151 | if(!rpc_keyboard) { 152 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 153 | return NULL; 154 | } 155 | char* macro_text = NULL; 156 | size_t index_start = rpc_keyboard_get_macro_start_index(rpc_keyboard, macro); 157 | if(index_start != FURI_STRING_FAILURE) { 158 | size_t index_end = furi_string_search_str(rpc_keyboard->macros, macro_end, index_start); 159 | if(index_end != FURI_STRING_FAILURE) { 160 | size_t len = index_end - index_start - 2; 161 | macro_text = malloc(sizeof(char) * (len + 1)); 162 | strncpy(macro_text, furi_string_get_cstr(rpc_keyboard->macros) + index_start + 2, len); 163 | macro_text[len] = '\0'; 164 | } 165 | } 166 | FURI_LOG_D( 167 | TAG, 168 | "Get Macro %c: %d %s >%s", 169 | macro, 170 | index_start, 171 | macro_text ? macro_text : "NULL", 172 | furi_string_get_cstr(rpc_keyboard->macros)); 173 | return macro_text; 174 | } 175 | 176 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value) { 177 | if(!rpc_keyboard) { 178 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 179 | return; 180 | } 181 | size_t index_start = rpc_keyboard_get_macro_start_index(rpc_keyboard, macro); 182 | if(index_start != FURI_STRING_FAILURE) { 183 | size_t index_end = furi_string_search_str(rpc_keyboard->macros, macro_end, index_start); 184 | if(index_end != FURI_STRING_FAILURE) { 185 | size_t len = index_end - index_start + 1; 186 | furi_string_replace_at(rpc_keyboard->macros, index_start, len, ""); 187 | } 188 | } 189 | if(strlen(value) != 0) { 190 | furi_string_cat_printf(rpc_keyboard->macros, "\x80%c%s\x81", macro, value); 191 | } 192 | FURI_LOG_D( 193 | TAG, 194 | "Set Macro %c: %d %s >%s", 195 | macro, 196 | index_start, 197 | value ? value : "NULL", 198 | furi_string_get_cstr(rpc_keyboard->macros)); 199 | } 200 | 201 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard) { 202 | if(!rpc_keyboard) { 203 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 204 | return; 205 | } 206 | if(rpc_keyboard->chatpad) { 207 | rpc_keyboard_chatpad_stop(rpc_keyboard); 208 | } 209 | rpc_keyboard->chatpad = rpc_chatpad_alloc(); 210 | } 211 | 212 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard) { 213 | if(!rpc_keyboard) { 214 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 215 | return; 216 | } 217 | rpc_chatpad_free(rpc_keyboard->chatpad); 218 | rpc_keyboard->chatpad = NULL; 219 | } 220 | 221 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard) { 222 | if(!rpc_keyboard) { 223 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 224 | return RpcKeyboardChatpadStatusError; 225 | } 226 | if(rpc_keyboard->chatpad == NULL) { 227 | return RpcKeyboardChatpadStatusStopped; 228 | } 229 | if(rpc_chatpad_ready(rpc_keyboard->chatpad)) { 230 | return RpcKeyboardChatpadStatusReady; 231 | } else { 232 | return RpcKeyboardChatpadStatusStarted; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /firmware-overlay/ofw-1.1.2/applications/services/rpc/rpc_keyboard.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define TAG "RpcKeyboard" 9 | 10 | struct RpcKeyboard { 11 | RpcKeyboardFunctions funcs; 12 | FuriPubSub* event_pubsub; 13 | RpcKeyboardEvent event; 14 | RpcChatpad* chatpad; 15 | FuriString* macros; 16 | }; 17 | 18 | void rpc_keyboard_register(void) { 19 | RpcKeyboard* keyboard = malloc(sizeof(RpcKeyboard)); 20 | keyboard->event = (RpcKeyboardEvent){ 21 | .data.newline_enabled = true, 22 | .data.length = 0, 23 | .data.mutex = furi_mutex_alloc(FuriMutexTypeRecursive)}; 24 | keyboard->event_pubsub = furi_pubsub_alloc(); 25 | keyboard->chatpad = NULL; 26 | keyboard->macros = furi_string_alloc(); 27 | keyboard->funcs.major = 1; 28 | keyboard->funcs.minor = 6; 29 | keyboard->funcs.fn_get_pubsub = rpc_keyboard_get_pubsub; 30 | keyboard->funcs.fn_newline_enable = rpc_keyboard_newline_enable; 31 | keyboard->funcs.fn_publish_char = rpc_keyboard_publish_char; 32 | keyboard->funcs.fn_publish_macro = rpc_keyboard_publish_macro; 33 | keyboard->funcs.fn_get_macro = rpc_keyboard_get_macro; 34 | keyboard->funcs.fn_set_macro = rpc_keyboard_set_macro; 35 | keyboard->funcs.fn_chatpad_start = rpc_keyboard_chatpad_start; 36 | keyboard->funcs.fn_chatpad_stop = rpc_keyboard_chatpad_stop; 37 | keyboard->funcs.fn_chatpad_status = rpc_keyboard_chatpad_status; 38 | furi_record_create(RECORD_RPC_KEYBOARD, keyboard); 39 | } 40 | 41 | void rpc_keyboard_release(void) { 42 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 43 | if(!rpc_keyboard) { 44 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 45 | return; 46 | } 47 | furi_pubsub_free(rpc_keyboard->event_pubsub); 48 | furi_mutex_free(rpc_keyboard->event.data.mutex); 49 | if(rpc_keyboard->chatpad) { 50 | rpc_chatpad_free(rpc_keyboard->chatpad); 51 | } 52 | furi_string_free(rpc_keyboard->macros); 53 | furi_record_destroy(RECORD_RPC_KEYBOARD); 54 | } 55 | 56 | FuriPubSub* rpc_keyboard_get_pubsub(RpcKeyboard* rpc_keyboard) { 57 | if(rpc_keyboard) { 58 | return rpc_keyboard->event_pubsub; 59 | } else { 60 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 61 | return NULL; 62 | } 63 | } 64 | 65 | void rpc_keyboard_newline_enable(RpcKeyboard* rpc_keyboard, bool enable) { 66 | if(rpc_keyboard) { 67 | rpc_keyboard->event.data.newline_enabled = enable; 68 | } else { 69 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 70 | } 71 | } 72 | 73 | /** 74 | * @brief Internal API - publishes a remote keyboard event. 75 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 76 | * @param[in] bytes the data to publish. 77 | * @param[in] buffer_size the size of the bytes. 78 | * @param[in] type the type of the event. 79 | */ 80 | static void rpc_keyboard_publish( 81 | RpcKeyboard* rpc_keyboard, 82 | uint8_t* bytes, 83 | uint32_t buffer_size, 84 | RpcKeyboardEventType type) { 85 | if(!rpc_keyboard) { 86 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 87 | return; 88 | } 89 | furi_mutex_acquire(rpc_keyboard->event.data.mutex, FuriWaitForever); 90 | if(buffer_size > sizeof(rpc_keyboard->event.data.message) - 1) { 91 | buffer_size = sizeof(rpc_keyboard->event.data.message) - 1; 92 | } 93 | strncpy(rpc_keyboard->event.data.message, (const char*)bytes, buffer_size); 94 | rpc_keyboard->event.data.length = buffer_size; 95 | rpc_keyboard->event.data.message[rpc_keyboard->event.data.length] = '\0'; 96 | for(uint32_t i = 0; i < rpc_keyboard->event.data.length; i++) { 97 | if(rpc_keyboard->event.data.message[i] == '\r' || 98 | rpc_keyboard->event.data.message[i] == '\n') { 99 | rpc_keyboard->event.data.message[i] = '\r'; // text_input.c: #define ENTER_KEY '\r' 100 | rpc_keyboard->event.data.length = i + 1; 101 | rpc_keyboard->event.data.message[rpc_keyboard->event.data.length] = '\0'; 102 | break; 103 | } 104 | } 105 | rpc_keyboard->event.type = type; 106 | furi_mutex_release(rpc_keyboard->event.data.mutex); 107 | 108 | if(rpc_keyboard->event_pubsub) { 109 | furi_pubsub_publish(rpc_keyboard->event_pubsub, &rpc_keyboard->event); 110 | } 111 | } 112 | 113 | void rpc_keyboard_publish_text(RpcKeyboard* rpc_keyboard, uint8_t* bytes, uint32_t buffer_size) { 114 | rpc_keyboard_publish(rpc_keyboard, bytes, buffer_size, RpcKeyboardEventTypeTextEntered); 115 | } 116 | 117 | void rpc_keyboard_publish_char(RpcKeyboard* rpc_keyboard, char character) { 118 | rpc_keyboard_publish(rpc_keyboard, (uint8_t*)&character, 1, RpcKeyboardEventTypeCharEntered); 119 | } 120 | 121 | void rpc_keyboard_publish_macro(RpcKeyboard* rpc_keyboard, char macro) { 122 | if(isupper(macro)) { // our macro table is lowercase 123 | macro = tolower(macro); 124 | } 125 | char* macro_text = rpc_keyboard_get_macro(rpc_keyboard, macro); 126 | if(macro_text == NULL) { 127 | macro_text = malloc(sizeof(char) * 2); 128 | macro_text[0] = macro; // send the macro key if no macro is set 129 | macro_text[1] = '\0'; 130 | } 131 | 132 | rpc_keyboard_publish( 133 | rpc_keyboard, (uint8_t*)macro_text, strlen(macro_text), RpcKeyboardEventTypeMacroEntered); 134 | free(macro_text); 135 | } 136 | 137 | /** 138 | * @brief Internal API - searches for the start of a macro in the macro string. 139 | * @param[in] rpc_keyboard pointer to the RECORD_RPC_KEYBOARD. 140 | * @param[in] macro the macro key to search for. 141 | * @return size_t the index of the start of the macro in the string. 142 | */ 143 | static size_t rpc_keyboard_get_macro_start_index(RpcKeyboard* rpc_keyboard, char macro) { 144 | furi_check(rpc_keyboard); 145 | char macro_start[3] = {'\x80', macro, '\0'}; 146 | return furi_string_search_str(rpc_keyboard->macros, macro_start, 0); 147 | } 148 | static const char macro_end[2] = {'\x81', '\0'}; 149 | 150 | char* rpc_keyboard_get_macro(RpcKeyboard* rpc_keyboard, char macro) { 151 | if(!rpc_keyboard) { 152 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 153 | return NULL; 154 | } 155 | char* macro_text = NULL; 156 | size_t index_start = rpc_keyboard_get_macro_start_index(rpc_keyboard, macro); 157 | if(index_start != FURI_STRING_FAILURE) { 158 | size_t index_end = furi_string_search_str(rpc_keyboard->macros, macro_end, index_start); 159 | if(index_end != FURI_STRING_FAILURE) { 160 | size_t len = index_end - index_start - 2; 161 | macro_text = malloc(sizeof(char) * (len + 1)); 162 | strncpy(macro_text, furi_string_get_cstr(rpc_keyboard->macros) + index_start + 2, len); 163 | macro_text[len] = '\0'; 164 | } 165 | } 166 | FURI_LOG_D( 167 | TAG, 168 | "Get Macro %c: %d %s >%s", 169 | macro, 170 | index_start, 171 | macro_text ? macro_text : "NULL", 172 | furi_string_get_cstr(rpc_keyboard->macros)); 173 | return macro_text; 174 | } 175 | 176 | void rpc_keyboard_set_macro(RpcKeyboard* rpc_keyboard, char macro, char* value) { 177 | if(!rpc_keyboard) { 178 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 179 | return; 180 | } 181 | size_t index_start = rpc_keyboard_get_macro_start_index(rpc_keyboard, macro); 182 | if(index_start != FURI_STRING_FAILURE) { 183 | size_t index_end = furi_string_search_str(rpc_keyboard->macros, macro_end, index_start); 184 | if(index_end != FURI_STRING_FAILURE) { 185 | size_t len = index_end - index_start + 1; 186 | furi_string_replace_at(rpc_keyboard->macros, index_start, len, ""); 187 | } 188 | } 189 | if(strlen(value) != 0) { 190 | furi_string_cat_printf(rpc_keyboard->macros, "\x80%c%s\x81", macro, value); 191 | } 192 | FURI_LOG_D( 193 | TAG, 194 | "Set Macro %c: %d %s >%s", 195 | macro, 196 | index_start, 197 | value ? value : "NULL", 198 | furi_string_get_cstr(rpc_keyboard->macros)); 199 | } 200 | 201 | void rpc_keyboard_chatpad_start(RpcKeyboard* rpc_keyboard) { 202 | if(!rpc_keyboard) { 203 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 204 | return; 205 | } 206 | if(rpc_keyboard->chatpad) { 207 | rpc_keyboard_chatpad_stop(rpc_keyboard); 208 | } 209 | rpc_keyboard->chatpad = rpc_chatpad_alloc(); 210 | } 211 | 212 | void rpc_keyboard_chatpad_stop(RpcKeyboard* rpc_keyboard) { 213 | if(!rpc_keyboard) { 214 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 215 | return; 216 | } 217 | rpc_chatpad_free(rpc_keyboard->chatpad); 218 | rpc_keyboard->chatpad = NULL; 219 | } 220 | 221 | RpcKeyboardChatpadStatus rpc_keyboard_chatpad_status(RpcKeyboard* rpc_keyboard) { 222 | if(!rpc_keyboard) { 223 | FURI_LOG_E(TAG, "rpc keyboard is NULL"); 224 | return RpcKeyboardChatpadStatusError; 225 | } 226 | if(rpc_keyboard->chatpad == NULL) { 227 | return RpcKeyboardChatpadStatusStopped; 228 | } 229 | if(rpc_chatpad_ready(rpc_keyboard->chatpad)) { 230 | return RpcKeyboardChatpadStatusReady; 231 | } else { 232 | return RpcKeyboardChatpadStatusStarted; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /firmware-overlay/mntm-008/applications/services/rpc/rpc_chatpad.c: -------------------------------------------------------------------------------- 1 | // baud rate and tx data from https://github.com/frequem/Chatpad 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define TAG "RpcChatpad" 13 | 14 | struct RpcChatpad { 15 | FuriHalBus lpuart_bus; 16 | FuriHalSerialHandle* lpuart_handle; 17 | bool lpuart_already_opened; 18 | FuriStreamBuffer* rx_stream; 19 | FuriThread* worker_thread; 20 | FuriApiLock worker_thread_exit_lock; 21 | bool ready; 22 | }; 23 | 24 | static uint8_t rpc_chatpad_keymap[7][7] = { 25 | {'7', '6', '5', '4', '3', '2', '1'}, 26 | {'U', 'Y', 'T', 'R', 'E', 'W', 'Q'}, 27 | {'J', 'H', 'G', 'F', 'D', 'S', 'A'}, 28 | {'N', 'B', 'V', 'C', 'X', 'Z', '?'}, 29 | {RPC_KEYBOARD_KEY_RIGHT, 'M', '.', ' ', RPC_KEYBOARD_KEY_LEFT, '?', '?'}, 30 | {'?', ',', RPC_KEYBOARD_KEY_ENTER, 'P', '0', '9', '8'}, 31 | {RPC_KEYBOARD_KEY_BACKSPACE, 'L', '?', '?', 'O', 'I', 'K'}}; 32 | 33 | static char orangeMap(uint8_t button) { 34 | switch(button) { 35 | case 'R': 36 | return '$'; 37 | case 'P': 38 | return '='; 39 | case ',': 40 | return ';'; 41 | case 'J': 42 | return '\"'; 43 | case 'H': 44 | return '\\'; 45 | case 'V': 46 | return '_'; 47 | case 'B': 48 | return '+'; 49 | default: 50 | return button; 51 | } 52 | } 53 | 54 | static char greenMap(uint8_t button) { 55 | switch(button) { 56 | case 'Q': 57 | return '!'; 58 | case 'W': 59 | return '@'; 60 | case 'R': 61 | return '#'; 62 | case 'T': 63 | return '%'; 64 | case 'Y': 65 | return '^'; 66 | case 'U': 67 | return '&'; 68 | case 'I': 69 | return '*'; 70 | case 'O': 71 | return '('; 72 | case 'P': 73 | return ')'; 74 | case 'A': 75 | return '~'; 76 | case 'D': 77 | return '{'; 78 | case 'F': 79 | return '}'; 80 | case 'H': 81 | return '/'; 82 | case 'J': 83 | return '\''; 84 | case 'K': 85 | return '['; 86 | case 'L': 87 | return ']'; 88 | case ',': 89 | return ':'; 90 | case 'Z': 91 | return '`'; 92 | case 'V': 93 | return '-'; 94 | case 'B': 95 | return '|'; 96 | case 'N': 97 | return '<'; 98 | case 'M': 99 | return '>'; 100 | case '.': 101 | return '?'; 102 | default: 103 | return button; 104 | } 105 | } 106 | 107 | static char rpc_chatpad_code_to_char(uint8_t code, uint8_t modifier, uint8_t caplock) { 108 | uint8_t row = code >> 4; 109 | uint8_t col = code & 0xF; 110 | uint8_t button = '\0'; 111 | 112 | if(row > 0 && col > 0 && row <= 7 && col <= 7) { 113 | button = rpc_chatpad_keymap[row - 1][col - 1]; 114 | if(modifier == 4) { 115 | button = orangeMap(button); 116 | } else if(modifier == 2) { 117 | button = greenMap(button); 118 | } else if((modifier & 1) == caplock && isupper(button)) { 119 | button = button + 32; // make lowercase 120 | } 121 | } 122 | 123 | return button; 124 | } 125 | 126 | static void rpc_chatpad_rx_callback( 127 | FuriHalSerialHandle* handle, 128 | FuriHalSerialRxEvent event, 129 | void* context) { 130 | furi_check(context); 131 | RpcChatpad* chatpad = context; 132 | if(event == FuriHalSerialRxEventData) { 133 | uint8_t data = furi_hal_serial_async_rx(handle); 134 | furi_stream_buffer_send(chatpad->rx_stream, (void*)&data, 1, FuriWaitForever); 135 | } 136 | } 137 | 138 | static bool rpc_chatpad_init(RpcChatpad* chatpad) { 139 | furi_check(chatpad); 140 | const uint8_t init_sequence[] = {0x87, 0x02, 0x8C, 0x1F, 0xCC}; 141 | for(size_t retry_loop = 0; retry_loop < 5; retry_loop++) { 142 | furi_hal_serial_tx(chatpad->lpuart_handle, init_sequence, 5); 143 | uint8_t init_response[8]; 144 | size_t bytes_read = 145 | furi_stream_buffer_receive(chatpad->rx_stream, &init_response, 8, 1000); 146 | if(bytes_read != 8) { 147 | FURI_LOG_E(TAG, "Failed to get response from initialize chatpad."); 148 | furi_delay_ms(500); 149 | continue; 150 | } 151 | furi_delay_ms(500); 152 | return true; 153 | } 154 | return false; 155 | } 156 | 157 | static void rpc_chatpad_heartbeat(RpcChatpad* chatpad) { 158 | const uint8_t heartbeat_sequence[] = {0x87, 0x02, 0x8C, 0x1B, 0xD0}; 159 | furi_hal_serial_tx(chatpad->lpuart_handle, heartbeat_sequence, 5); 160 | } 161 | 162 | static int32_t rpc_chatpad_worker(void* context) { 163 | furi_check(context); 164 | RpcChatpad* chatpad = context; 165 | FURI_LOG_D(TAG, "Chatpad worker started."); 166 | rpc_chatpad_init(chatpad); 167 | 168 | uint32_t line = 0; 169 | bool show = true; 170 | uint8_t caplock = 0; 171 | while(api_lock_is_locked(chatpad->worker_thread_exit_lock)) { 172 | if(line++ % 100 == 0) { 173 | FURI_LOG_T(TAG, "Chatpad heartbeat."); 174 | rpc_chatpad_heartbeat(chatpad); 175 | } 176 | uint8_t data[8]; 177 | size_t bytes_read = furi_stream_buffer_receive(chatpad->rx_stream, &data, 8, 20); 178 | if(bytes_read == 0) { 179 | continue; 180 | } 181 | if(bytes_read != 8) { 182 | FURI_LOG_T(TAG, "Invalid data size: %zu", bytes_read); 183 | continue; 184 | } 185 | 186 | if(data[0] != 0xB4 && data[0] != 0xA5) { 187 | FURI_LOG_D( 188 | TAG, 189 | "Invalid data: %02x %02x %02x %02x %02x %02x %02x %02x", 190 | data[0], 191 | data[1], 192 | data[2], 193 | data[3], 194 | data[4], 195 | data[5], 196 | data[6], 197 | data[7]); 198 | 199 | uint8_t sync_bytes = 0; 200 | for(size_t i = 1; i < 8; i++) { 201 | if(data[i] == 0xA5 || data[i] == 0xB4) { 202 | sync_bytes = i; 203 | } 204 | } 205 | if(sync_bytes != 0) { 206 | bytes_read = 207 | furi_stream_buffer_receive(chatpad->rx_stream, &data, sync_bytes, 100); 208 | if(bytes_read != sync_bytes) { 209 | FURI_LOG_D( 210 | TAG, 211 | "Failed to sync data. attempted: %d actual: %d", 212 | sync_bytes, 213 | bytes_read); 214 | } else { 215 | FURI_LOG_T(TAG, "Synced data."); 216 | } 217 | } 218 | 219 | continue; 220 | } 221 | 222 | if((data[0] + data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]) % 256 != 223 | 0) { 224 | FURI_LOG_E( 225 | TAG, 226 | "Checksum failed. data: %02x %02x %02x %02x %02x %02x %02x %02x", 227 | data[0], 228 | data[1], 229 | data[2], 230 | data[3], 231 | data[4], 232 | data[5], 233 | data[6], 234 | data[7]); 235 | continue; 236 | } 237 | 238 | if(data[0] == 0xB4 && data[1] == 0xC5 && show) { 239 | uint8_t mod = data[3]; 240 | uint8_t btn = data[4]; 241 | if(mod != 0 || btn != 0) { 242 | char ch = rpc_chatpad_code_to_char(btn, mod, caplock); 243 | if(mod == 8 && btn != 0) { 244 | // People button + other key. 245 | FURI_LOG_T(TAG, "Char: %c Mod: %02x, Btn: %02x", ch, mod, btn); 246 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 247 | rpc_keyboard_publish_macro(rpc_keyboard, ch); 248 | furi_record_close(RECORD_RPC_KEYBOARD); 249 | } else if(mod == 5) { 250 | // Caps lock button pressed. 251 | furi_hal_vibro_on(true); 252 | caplock = (caplock == 0) ? 1 : 0; 253 | furi_delay_ms(100); 254 | furi_hal_vibro_on(false); 255 | } else if(ch != '\0') { 256 | FURI_LOG_T(TAG, "Char: %c Mod: %02x, Btn: %02x", ch, mod, btn); 257 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 258 | rpc_keyboard_publish_char(rpc_keyboard, ch); 259 | furi_record_close(RECORD_RPC_KEYBOARD); 260 | } else if(btn != 0) { 261 | FURI_LOG_D(TAG, "Mod: %02x, Btn: %02x", mod, btn); 262 | } 263 | } 264 | show = false; 265 | line = 1; 266 | } else if(data[0] == 0xA5) { 267 | if(!chatpad->ready) { 268 | FURI_LOG_I(TAG, "Chatpad ready."); 269 | chatpad->ready = true; 270 | } 271 | show = true; 272 | } 273 | } 274 | 275 | FURI_LOG_D(TAG, "Chatpad worker exiting."); 276 | return 0; 277 | } 278 | 279 | RpcChatpad* rpc_chatpad_alloc(void) { 280 | RpcChatpad* chatpad = malloc(sizeof(RpcChatpad)); 281 | const uint32_t lpuart_baud = 19200; 282 | chatpad->ready = false; 283 | chatpad->worker_thread_exit_lock = api_lock_alloc_locked(); 284 | chatpad->lpuart_bus = FuriHalBusLPUART1; 285 | chatpad->lpuart_handle = furi_hal_serial_control_acquire(FuriHalSerialIdLpuart); 286 | chatpad->lpuart_already_opened = furi_hal_bus_is_enabled(chatpad->lpuart_bus); 287 | if(!chatpad->lpuart_already_opened) { 288 | furi_hal_serial_init(chatpad->lpuart_handle, lpuart_baud); 289 | } else { 290 | FURI_LOG_D(TAG, "LPUART already initialized."); 291 | } 292 | furi_hal_serial_set_br(chatpad->lpuart_handle, lpuart_baud); 293 | chatpad->rx_stream = furi_stream_buffer_alloc(32, 8); 294 | furi_hal_serial_async_rx_start( 295 | chatpad->lpuart_handle, rpc_chatpad_rx_callback, chatpad, false); 296 | chatpad->worker_thread = 297 | furi_thread_alloc_ex("rpc_chatpad_work", 1024, rpc_chatpad_worker, chatpad); 298 | furi_thread_start(chatpad->worker_thread); 299 | return chatpad; 300 | } 301 | 302 | void rpc_chatpad_free(RpcChatpad* chatpad) { 303 | furi_check(chatpad); 304 | chatpad->ready = false; 305 | furi_hal_serial_async_rx_stop(chatpad->lpuart_handle); 306 | api_lock_unlock(chatpad->worker_thread_exit_lock); 307 | furi_thread_join(chatpad->worker_thread); 308 | chatpad->worker_thread = NULL; 309 | FURI_LOG_D(TAG, "Chatpad freeing resources."); 310 | if(chatpad->lpuart_already_opened) { 311 | furi_hal_serial_deinit(chatpad->lpuart_handle); 312 | } 313 | furi_hal_serial_control_release(chatpad->lpuart_handle); 314 | chatpad->lpuart_handle = NULL; 315 | furi_stream_buffer_free(chatpad->rx_stream); 316 | chatpad->rx_stream = NULL; 317 | free(chatpad); 318 | } 319 | 320 | bool rpc_chatpad_ready(RpcChatpad* chatpad) { 321 | furi_check(chatpad); 322 | return chatpad->ready; 323 | } 324 | -------------------------------------------------------------------------------- /firmware-overlay/ofw-1.1.2/applications/services/rpc/rpc_chatpad.c: -------------------------------------------------------------------------------- 1 | // baud rate and tx data from https://github.com/frequem/Chatpad 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define TAG "RpcChatpad" 13 | 14 | struct RpcChatpad { 15 | FuriHalBus lpuart_bus; 16 | FuriHalSerialHandle* lpuart_handle; 17 | bool lpuart_already_opened; 18 | FuriStreamBuffer* rx_stream; 19 | FuriThread* worker_thread; 20 | FuriApiLock worker_thread_exit_lock; 21 | bool ready; 22 | }; 23 | 24 | static uint8_t rpc_chatpad_keymap[7][7] = { 25 | {'7', '6', '5', '4', '3', '2', '1'}, 26 | {'U', 'Y', 'T', 'R', 'E', 'W', 'Q'}, 27 | {'J', 'H', 'G', 'F', 'D', 'S', 'A'}, 28 | {'N', 'B', 'V', 'C', 'X', 'Z', '?'}, 29 | {RPC_KEYBOARD_KEY_RIGHT, 'M', '.', ' ', RPC_KEYBOARD_KEY_LEFT, '?', '?'}, 30 | {'?', ',', RPC_KEYBOARD_KEY_ENTER, 'P', '0', '9', '8'}, 31 | {RPC_KEYBOARD_KEY_BACKSPACE, 'L', '?', '?', 'O', 'I', 'K'}}; 32 | 33 | static char orangeMap(uint8_t button) { 34 | switch(button) { 35 | case 'R': 36 | return '$'; 37 | case 'P': 38 | return '='; 39 | case ',': 40 | return ';'; 41 | case 'J': 42 | return '\"'; 43 | case 'H': 44 | return '\\'; 45 | case 'V': 46 | return '_'; 47 | case 'B': 48 | return '+'; 49 | default: 50 | return button; 51 | } 52 | } 53 | 54 | static char greenMap(uint8_t button) { 55 | switch(button) { 56 | case 'Q': 57 | return '!'; 58 | case 'W': 59 | return '@'; 60 | case 'R': 61 | return '#'; 62 | case 'T': 63 | return '%'; 64 | case 'Y': 65 | return '^'; 66 | case 'U': 67 | return '&'; 68 | case 'I': 69 | return '*'; 70 | case 'O': 71 | return '('; 72 | case 'P': 73 | return ')'; 74 | case 'A': 75 | return '~'; 76 | case 'D': 77 | return '{'; 78 | case 'F': 79 | return '}'; 80 | case 'H': 81 | return '/'; 82 | case 'J': 83 | return '\''; 84 | case 'K': 85 | return '['; 86 | case 'L': 87 | return ']'; 88 | case ',': 89 | return ':'; 90 | case 'Z': 91 | return '`'; 92 | case 'V': 93 | return '-'; 94 | case 'B': 95 | return '|'; 96 | case 'N': 97 | return '<'; 98 | case 'M': 99 | return '>'; 100 | case '.': 101 | return '?'; 102 | default: 103 | return button; 104 | } 105 | } 106 | 107 | static char rpc_chatpad_code_to_char(uint8_t code, uint8_t modifier, uint8_t caplock) { 108 | uint8_t row = code >> 4; 109 | uint8_t col = code & 0xF; 110 | uint8_t button = '\0'; 111 | 112 | if(row > 0 && col > 0 && row <= 7 && col <= 7) { 113 | button = rpc_chatpad_keymap[row - 1][col - 1]; 114 | if(modifier == 4) { 115 | button = orangeMap(button); 116 | } else if(modifier == 2) { 117 | button = greenMap(button); 118 | } else if((modifier & 1) == caplock && isupper(button)) { 119 | button = button + 32; // make lowercase 120 | } 121 | } 122 | 123 | return button; 124 | } 125 | 126 | static void rpc_chatpad_rx_callback( 127 | FuriHalSerialHandle* handle, 128 | FuriHalSerialRxEvent event, 129 | void* context) { 130 | furi_check(context); 131 | RpcChatpad* chatpad = context; 132 | if(event == FuriHalSerialRxEventData) { 133 | uint8_t data = furi_hal_serial_async_rx(handle); 134 | furi_stream_buffer_send(chatpad->rx_stream, (void*)&data, 1, FuriWaitForever); 135 | } 136 | } 137 | 138 | static bool rpc_chatpad_init(RpcChatpad* chatpad) { 139 | furi_check(chatpad); 140 | const uint8_t init_sequence[] = {0x87, 0x02, 0x8C, 0x1F, 0xCC}; 141 | for(size_t retry_loop = 0; retry_loop < 5; retry_loop++) { 142 | furi_hal_serial_tx(chatpad->lpuart_handle, init_sequence, 5); 143 | uint8_t init_response[8]; 144 | size_t bytes_read = 145 | furi_stream_buffer_receive(chatpad->rx_stream, &init_response, 8, 1000); 146 | if(bytes_read != 8) { 147 | FURI_LOG_E(TAG, "Failed to get response from initialize chatpad."); 148 | furi_delay_ms(500); 149 | continue; 150 | } 151 | furi_delay_ms(500); 152 | return true; 153 | } 154 | return false; 155 | } 156 | 157 | static void rpc_chatpad_heartbeat(RpcChatpad* chatpad) { 158 | const uint8_t heartbeat_sequence[] = {0x87, 0x02, 0x8C, 0x1B, 0xD0}; 159 | furi_hal_serial_tx(chatpad->lpuart_handle, heartbeat_sequence, 5); 160 | } 161 | 162 | static int32_t rpc_chatpad_worker(void* context) { 163 | furi_check(context); 164 | RpcChatpad* chatpad = context; 165 | FURI_LOG_D(TAG, "Chatpad worker started."); 166 | rpc_chatpad_init(chatpad); 167 | 168 | uint32_t line = 0; 169 | bool show = true; 170 | uint8_t caplock = 0; 171 | while(api_lock_is_locked(chatpad->worker_thread_exit_lock)) { 172 | if(line++ % 100 == 0) { 173 | FURI_LOG_T(TAG, "Chatpad heartbeat."); 174 | rpc_chatpad_heartbeat(chatpad); 175 | } 176 | uint8_t data[8]; 177 | size_t bytes_read = furi_stream_buffer_receive(chatpad->rx_stream, &data, 8, 20); 178 | if(bytes_read == 0) { 179 | continue; 180 | } 181 | if(bytes_read != 8) { 182 | FURI_LOG_T(TAG, "Invalid data size: %zu", bytes_read); 183 | continue; 184 | } 185 | 186 | if(data[0] != 0xB4 && data[0] != 0xA5) { 187 | FURI_LOG_D( 188 | TAG, 189 | "Invalid data: %02x %02x %02x %02x %02x %02x %02x %02x", 190 | data[0], 191 | data[1], 192 | data[2], 193 | data[3], 194 | data[4], 195 | data[5], 196 | data[6], 197 | data[7]); 198 | 199 | uint8_t sync_bytes = 0; 200 | for(size_t i = 1; i < 8; i++) { 201 | if(data[i] == 0xA5 || data[i] == 0xB4) { 202 | sync_bytes = i; 203 | } 204 | } 205 | if(sync_bytes != 0) { 206 | bytes_read = 207 | furi_stream_buffer_receive(chatpad->rx_stream, &data, sync_bytes, 100); 208 | if(bytes_read != sync_bytes) { 209 | FURI_LOG_D( 210 | TAG, 211 | "Failed to sync data. attempted: %d actual: %d", 212 | sync_bytes, 213 | bytes_read); 214 | } else { 215 | FURI_LOG_T(TAG, "Synced data."); 216 | } 217 | } 218 | 219 | continue; 220 | } 221 | 222 | if((data[0] + data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]) % 256 != 223 | 0) { 224 | FURI_LOG_E( 225 | TAG, 226 | "Checksum failed. data: %02x %02x %02x %02x %02x %02x %02x %02x", 227 | data[0], 228 | data[1], 229 | data[2], 230 | data[3], 231 | data[4], 232 | data[5], 233 | data[6], 234 | data[7]); 235 | continue; 236 | } 237 | 238 | if(data[0] == 0xB4 && data[1] == 0xC5 && show) { 239 | uint8_t mod = data[3]; 240 | uint8_t btn = data[4]; 241 | if(mod != 0 || btn != 0) { 242 | char ch = rpc_chatpad_code_to_char(btn, mod, caplock); 243 | if(mod == 8 && btn != 0) { 244 | // People button + other key. 245 | FURI_LOG_T(TAG, "Char: %c Mod: %02x, Btn: %02x", ch, mod, btn); 246 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 247 | rpc_keyboard_publish_macro(rpc_keyboard, ch); 248 | furi_record_close(RECORD_RPC_KEYBOARD); 249 | } else if(mod == 5) { 250 | // Caps lock button pressed. 251 | furi_hal_vibro_on(true); 252 | caplock = (caplock == 0) ? 1 : 0; 253 | furi_delay_ms(100); 254 | furi_hal_vibro_on(false); 255 | } else if(ch != '\0') { 256 | FURI_LOG_T(TAG, "Char: %c Mod: %02x, Btn: %02x", ch, mod, btn); 257 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 258 | rpc_keyboard_publish_char(rpc_keyboard, ch); 259 | furi_record_close(RECORD_RPC_KEYBOARD); 260 | } else if(btn != 0) { 261 | FURI_LOG_D(TAG, "Mod: %02x, Btn: %02x", mod, btn); 262 | } 263 | } 264 | show = false; 265 | line = 1; 266 | } else if(data[0] == 0xA5) { 267 | if(!chatpad->ready) { 268 | FURI_LOG_I(TAG, "Chatpad ready."); 269 | chatpad->ready = true; 270 | } 271 | show = true; 272 | } 273 | } 274 | 275 | FURI_LOG_D(TAG, "Chatpad worker exiting."); 276 | return 0; 277 | } 278 | 279 | RpcChatpad* rpc_chatpad_alloc(void) { 280 | RpcChatpad* chatpad = malloc(sizeof(RpcChatpad)); 281 | const uint32_t lpuart_baud = 19200; 282 | chatpad->ready = false; 283 | chatpad->worker_thread_exit_lock = api_lock_alloc_locked(); 284 | chatpad->lpuart_bus = FuriHalBusLPUART1; 285 | chatpad->lpuart_handle = furi_hal_serial_control_acquire(FuriHalSerialIdLpuart); 286 | chatpad->lpuart_already_opened = furi_hal_bus_is_enabled(chatpad->lpuart_bus); 287 | if(!chatpad->lpuart_already_opened) { 288 | furi_hal_serial_init(chatpad->lpuart_handle, lpuart_baud); 289 | } else { 290 | FURI_LOG_D(TAG, "LPUART already initialized."); 291 | } 292 | furi_hal_serial_set_br(chatpad->lpuart_handle, lpuart_baud); 293 | chatpad->rx_stream = furi_stream_buffer_alloc(32, 8); 294 | furi_hal_serial_async_rx_start( 295 | chatpad->lpuart_handle, rpc_chatpad_rx_callback, chatpad, false); 296 | chatpad->worker_thread = 297 | furi_thread_alloc_ex("rpc_chatpad_work", 1024, rpc_chatpad_worker, chatpad); 298 | furi_thread_start(chatpad->worker_thread); 299 | return chatpad; 300 | } 301 | 302 | void rpc_chatpad_free(RpcChatpad* chatpad) { 303 | furi_check(chatpad); 304 | chatpad->ready = false; 305 | furi_hal_serial_async_rx_stop(chatpad->lpuart_handle); 306 | api_lock_unlock(chatpad->worker_thread_exit_lock); 307 | furi_thread_join(chatpad->worker_thread); 308 | chatpad->worker_thread = NULL; 309 | FURI_LOG_D(TAG, "Chatpad freeing resources."); 310 | if(chatpad->lpuart_already_opened) { 311 | furi_hal_serial_deinit(chatpad->lpuart_handle); 312 | } 313 | furi_hal_serial_control_release(chatpad->lpuart_handle); 314 | chatpad->lpuart_handle = NULL; 315 | furi_stream_buffer_free(chatpad->rx_stream); 316 | chatpad->rx_stream = NULL; 317 | free(chatpad); 318 | } 319 | 320 | bool rpc_chatpad_ready(RpcChatpad* chatpad) { 321 | furi_check(chatpad); 322 | return chatpad->ready; 323 | } 324 | -------------------------------------------------------------------------------- /firmware-overlay/unl-079/applications/services/rpc/rpc_chatpad.c: -------------------------------------------------------------------------------- 1 | // baud rate and tx data from https://github.com/frequem/Chatpad 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define TAG "RpcChatpad" 13 | 14 | struct RpcChatpad { 15 | FuriHalBus lpuart_bus; 16 | FuriHalSerialHandle* lpuart_handle; 17 | bool lpuart_already_opened; 18 | FuriStreamBuffer* rx_stream; 19 | FuriThread* worker_thread; 20 | FuriApiLock worker_thread_exit_lock; 21 | bool ready; 22 | }; 23 | 24 | static uint8_t rpc_chatpad_keymap[7][7] = { 25 | {'7', '6', '5', '4', '3', '2', '1'}, 26 | {'U', 'Y', 'T', 'R', 'E', 'W', 'Q'}, 27 | {'J', 'H', 'G', 'F', 'D', 'S', 'A'}, 28 | {'N', 'B', 'V', 'C', 'X', 'Z', '?'}, 29 | {RPC_KEYBOARD_KEY_RIGHT, 'M', '.', ' ', RPC_KEYBOARD_KEY_LEFT, '?', '?'}, 30 | {'?', ',', RPC_KEYBOARD_KEY_ENTER, 'P', '0', '9', '8'}, 31 | {RPC_KEYBOARD_KEY_BACKSPACE, 'L', '?', '?', 'O', 'I', 'K'}}; 32 | 33 | static char orangeMap(uint8_t button) { 34 | switch(button) { 35 | case 'R': 36 | return '$'; 37 | case 'P': 38 | return '='; 39 | case ',': 40 | return ';'; 41 | case 'J': 42 | return '\"'; 43 | case 'H': 44 | return '\\'; 45 | case 'V': 46 | return '_'; 47 | case 'B': 48 | return '+'; 49 | default: 50 | return button; 51 | } 52 | } 53 | 54 | static char greenMap(uint8_t button) { 55 | switch(button) { 56 | case 'Q': 57 | return '!'; 58 | case 'W': 59 | return '@'; 60 | case 'R': 61 | return '#'; 62 | case 'T': 63 | return '%'; 64 | case 'Y': 65 | return '^'; 66 | case 'U': 67 | return '&'; 68 | case 'I': 69 | return '*'; 70 | case 'O': 71 | return '('; 72 | case 'P': 73 | return ')'; 74 | case 'A': 75 | return '~'; 76 | case 'D': 77 | return '{'; 78 | case 'F': 79 | return '}'; 80 | case 'H': 81 | return '/'; 82 | case 'J': 83 | return '\''; 84 | case 'K': 85 | return '['; 86 | case 'L': 87 | return ']'; 88 | case ',': 89 | return ':'; 90 | case 'Z': 91 | return '`'; 92 | case 'V': 93 | return '-'; 94 | case 'B': 95 | return '|'; 96 | case 'N': 97 | return '<'; 98 | case 'M': 99 | return '>'; 100 | case '.': 101 | return '?'; 102 | default: 103 | return button; 104 | } 105 | } 106 | 107 | static char rpc_chatpad_code_to_char(uint8_t code, uint8_t modifier, uint8_t caplock) { 108 | uint8_t row = code >> 4; 109 | uint8_t col = code & 0xF; 110 | uint8_t button = '\0'; 111 | 112 | if(row > 0 && col > 0 && row <= 7 && col <= 7) { 113 | button = rpc_chatpad_keymap[row - 1][col - 1]; 114 | if(modifier == 4) { 115 | button = orangeMap(button); 116 | } else if(modifier == 2) { 117 | button = greenMap(button); 118 | } else if((modifier & 1) == caplock && isupper(button)) { 119 | button = button + 32; // make lowercase 120 | } 121 | } 122 | 123 | return button; 124 | } 125 | 126 | static void rpc_chatpad_rx_callback( 127 | FuriHalSerialHandle* handle, 128 | FuriHalSerialRxEvent event, 129 | void* context) { 130 | furi_check(context); 131 | RpcChatpad* chatpad = context; 132 | if(event == FuriHalSerialRxEventData) { 133 | uint8_t data = furi_hal_serial_async_rx(handle); 134 | furi_stream_buffer_send(chatpad->rx_stream, (void*)&data, 1, FuriWaitForever); 135 | } 136 | } 137 | 138 | static bool rpc_chatpad_init(RpcChatpad* chatpad) { 139 | furi_check(chatpad); 140 | const uint8_t init_sequence[] = {0x87, 0x02, 0x8C, 0x1F, 0xCC}; 141 | for(size_t retry_loop = 0; retry_loop < 5; retry_loop++) { 142 | furi_hal_serial_tx(chatpad->lpuart_handle, init_sequence, 5); 143 | uint8_t init_response[8]; 144 | size_t bytes_read = 145 | furi_stream_buffer_receive(chatpad->rx_stream, &init_response, 8, 1000); 146 | if(bytes_read != 8) { 147 | FURI_LOG_E(TAG, "Failed to get response from initialize chatpad."); 148 | furi_delay_ms(500); 149 | continue; 150 | } 151 | furi_delay_ms(500); 152 | return true; 153 | } 154 | return false; 155 | } 156 | 157 | static void rpc_chatpad_heartbeat(RpcChatpad* chatpad) { 158 | const uint8_t heartbeat_sequence[] = {0x87, 0x02, 0x8C, 0x1B, 0xD0}; 159 | furi_hal_serial_tx(chatpad->lpuart_handle, heartbeat_sequence, 5); 160 | } 161 | 162 | static int32_t rpc_chatpad_worker(void* context) { 163 | furi_check(context); 164 | RpcChatpad* chatpad = context; 165 | FURI_LOG_D(TAG, "Chatpad worker started."); 166 | rpc_chatpad_init(chatpad); 167 | 168 | uint32_t line = 0; 169 | bool show = true; 170 | uint8_t caplock = 0; 171 | while(api_lock_is_locked(chatpad->worker_thread_exit_lock)) { 172 | if(line++ % 100 == 0) { 173 | FURI_LOG_T(TAG, "Chatpad heartbeat."); 174 | rpc_chatpad_heartbeat(chatpad); 175 | } 176 | uint8_t data[8]; 177 | size_t bytes_read = furi_stream_buffer_receive(chatpad->rx_stream, &data, 8, 20); 178 | if(bytes_read == 0) { 179 | continue; 180 | } 181 | if(bytes_read != 8) { 182 | FURI_LOG_T(TAG, "Invalid data size: %zu", bytes_read); 183 | continue; 184 | } 185 | 186 | if(data[0] != 0xB4 && data[0] != 0xA5) { 187 | FURI_LOG_D( 188 | TAG, 189 | "Invalid data: %02x %02x %02x %02x %02x %02x %02x %02x", 190 | data[0], 191 | data[1], 192 | data[2], 193 | data[3], 194 | data[4], 195 | data[5], 196 | data[6], 197 | data[7]); 198 | 199 | uint8_t sync_bytes = 0; 200 | for(size_t i = 1; i < 8; i++) { 201 | if(data[i] == 0xA5 || data[i] == 0xB4) { 202 | sync_bytes = i; 203 | } 204 | } 205 | if(sync_bytes != 0) { 206 | bytes_read = 207 | furi_stream_buffer_receive(chatpad->rx_stream, &data, sync_bytes, 100); 208 | if(bytes_read != sync_bytes) { 209 | FURI_LOG_D( 210 | TAG, 211 | "Failed to sync data. attempted: %d actual: %d", 212 | sync_bytes, 213 | bytes_read); 214 | } else { 215 | FURI_LOG_T(TAG, "Synced data."); 216 | } 217 | } 218 | 219 | continue; 220 | } 221 | 222 | if((data[0] + data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]) % 256 != 223 | 0) { 224 | FURI_LOG_E( 225 | TAG, 226 | "Checksum failed. data: %02x %02x %02x %02x %02x %02x %02x %02x", 227 | data[0], 228 | data[1], 229 | data[2], 230 | data[3], 231 | data[4], 232 | data[5], 233 | data[6], 234 | data[7]); 235 | continue; 236 | } 237 | 238 | if(data[0] == 0xB4 && data[1] == 0xC5 && show) { 239 | uint8_t mod = data[3]; 240 | uint8_t btn = data[4]; 241 | if(mod != 0 || btn != 0) { 242 | char ch = rpc_chatpad_code_to_char(btn, mod, caplock); 243 | if(mod == 8 && btn != 0) { 244 | // People button + other key. 245 | FURI_LOG_T(TAG, "Char: %c Mod: %02x, Btn: %02x", ch, mod, btn); 246 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 247 | rpc_keyboard_publish_macro(rpc_keyboard, ch); 248 | furi_record_close(RECORD_RPC_KEYBOARD); 249 | } else if(mod == 5) { 250 | // Caps lock button pressed. 251 | furi_hal_vibro_on(true); 252 | caplock = (caplock == 0) ? 1 : 0; 253 | furi_delay_ms(100); 254 | furi_hal_vibro_on(false); 255 | } else if(ch != '\0') { 256 | FURI_LOG_T(TAG, "Char: %c Mod: %02x, Btn: %02x", ch, mod, btn); 257 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 258 | rpc_keyboard_publish_char(rpc_keyboard, ch); 259 | furi_record_close(RECORD_RPC_KEYBOARD); 260 | } else if(btn != 0) { 261 | FURI_LOG_D(TAG, "Mod: %02x, Btn: %02x", mod, btn); 262 | } 263 | } 264 | show = false; 265 | line = 1; 266 | } else if(data[0] == 0xA5) { 267 | if(!chatpad->ready) { 268 | FURI_LOG_I(TAG, "Chatpad ready."); 269 | chatpad->ready = true; 270 | } 271 | show = true; 272 | } 273 | } 274 | 275 | FURI_LOG_D(TAG, "Chatpad worker exiting."); 276 | return 0; 277 | } 278 | 279 | RpcChatpad* rpc_chatpad_alloc(void) { 280 | RpcChatpad* chatpad = malloc(sizeof(RpcChatpad)); 281 | const uint32_t lpuart_baud = 19200; 282 | chatpad->ready = false; 283 | chatpad->worker_thread_exit_lock = api_lock_alloc_locked(); 284 | chatpad->lpuart_bus = FuriHalBusLPUART1; 285 | chatpad->lpuart_handle = furi_hal_serial_control_acquire(FuriHalSerialIdLpuart); 286 | chatpad->lpuart_already_opened = furi_hal_bus_is_enabled(chatpad->lpuart_bus); 287 | if(!chatpad->lpuart_already_opened) { 288 | furi_hal_serial_init(chatpad->lpuart_handle, lpuart_baud); 289 | } else { 290 | FURI_LOG_D(TAG, "LPUART already initialized."); 291 | } 292 | furi_hal_serial_set_br(chatpad->lpuart_handle, lpuart_baud); 293 | chatpad->rx_stream = furi_stream_buffer_alloc(32, 8); 294 | furi_hal_serial_async_rx_start( 295 | chatpad->lpuart_handle, rpc_chatpad_rx_callback, chatpad, false); 296 | chatpad->worker_thread = 297 | furi_thread_alloc_ex("rpc_chatpad_work", 1024, rpc_chatpad_worker, chatpad); 298 | furi_thread_start(chatpad->worker_thread); 299 | return chatpad; 300 | } 301 | 302 | void rpc_chatpad_free(RpcChatpad* chatpad) { 303 | furi_check(chatpad); 304 | chatpad->ready = false; 305 | furi_hal_serial_async_rx_stop(chatpad->lpuart_handle); 306 | api_lock_unlock(chatpad->worker_thread_exit_lock); 307 | furi_thread_join(chatpad->worker_thread); 308 | chatpad->worker_thread = NULL; 309 | FURI_LOG_D(TAG, "Chatpad freeing resources."); 310 | if(chatpad->lpuart_already_opened) { 311 | furi_hal_serial_deinit(chatpad->lpuart_handle); 312 | } 313 | furi_hal_serial_control_release(chatpad->lpuart_handle); 314 | chatpad->lpuart_handle = NULL; 315 | furi_stream_buffer_free(chatpad->rx_stream); 316 | chatpad->rx_stream = NULL; 317 | free(chatpad); 318 | } 319 | 320 | bool rpc_chatpad_ready(RpcChatpad* chatpad) { 321 | furi_check(chatpad); 322 | return chatpad->ready; 323 | } 324 | -------------------------------------------------------------------------------- /firmware-overlay/rm-1202-0837-0.420.0-6d10bad/applications/services/rpc/rpc_chatpad.c: -------------------------------------------------------------------------------- 1 | // baud rate and tx data from https://github.com/frequem/Chatpad 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define TAG "RpcChatpad" 13 | 14 | struct RpcChatpad { 15 | FuriHalBus lpuart_bus; 16 | FuriHalSerialHandle* lpuart_handle; 17 | bool lpuart_already_opened; 18 | FuriStreamBuffer* rx_stream; 19 | FuriThread* worker_thread; 20 | FuriApiLock worker_thread_exit_lock; 21 | bool ready; 22 | }; 23 | 24 | static uint8_t rpc_chatpad_keymap[7][7] = { 25 | {'7', '6', '5', '4', '3', '2', '1'}, 26 | {'U', 'Y', 'T', 'R', 'E', 'W', 'Q'}, 27 | {'J', 'H', 'G', 'F', 'D', 'S', 'A'}, 28 | {'N', 'B', 'V', 'C', 'X', 'Z', '?'}, 29 | {RPC_KEYBOARD_KEY_RIGHT, 'M', '.', ' ', RPC_KEYBOARD_KEY_LEFT, '?', '?'}, 30 | {'?', ',', RPC_KEYBOARD_KEY_ENTER, 'P', '0', '9', '8'}, 31 | {RPC_KEYBOARD_KEY_BACKSPACE, 'L', '?', '?', 'O', 'I', 'K'}}; 32 | 33 | static char orangeMap(uint8_t button) { 34 | switch(button) { 35 | case 'R': 36 | return '$'; 37 | case 'P': 38 | return '='; 39 | case ',': 40 | return ';'; 41 | case 'J': 42 | return '\"'; 43 | case 'H': 44 | return '\\'; 45 | case 'V': 46 | return '_'; 47 | case 'B': 48 | return '+'; 49 | default: 50 | return button; 51 | } 52 | } 53 | 54 | static char greenMap(uint8_t button) { 55 | switch(button) { 56 | case 'Q': 57 | return '!'; 58 | case 'W': 59 | return '@'; 60 | case 'R': 61 | return '#'; 62 | case 'T': 63 | return '%'; 64 | case 'Y': 65 | return '^'; 66 | case 'U': 67 | return '&'; 68 | case 'I': 69 | return '*'; 70 | case 'O': 71 | return '('; 72 | case 'P': 73 | return ')'; 74 | case 'A': 75 | return '~'; 76 | case 'D': 77 | return '{'; 78 | case 'F': 79 | return '}'; 80 | case 'H': 81 | return '/'; 82 | case 'J': 83 | return '\''; 84 | case 'K': 85 | return '['; 86 | case 'L': 87 | return ']'; 88 | case ',': 89 | return ':'; 90 | case 'Z': 91 | return '`'; 92 | case 'V': 93 | return '-'; 94 | case 'B': 95 | return '|'; 96 | case 'N': 97 | return '<'; 98 | case 'M': 99 | return '>'; 100 | case '.': 101 | return '?'; 102 | default: 103 | return button; 104 | } 105 | } 106 | 107 | static char rpc_chatpad_code_to_char(uint8_t code, uint8_t modifier, uint8_t caplock) { 108 | uint8_t row = code >> 4; 109 | uint8_t col = code & 0xF; 110 | uint8_t button = '\0'; 111 | 112 | if(row > 0 && col > 0 && row <= 7 && col <= 7) { 113 | button = rpc_chatpad_keymap[row - 1][col - 1]; 114 | if(modifier == 4) { 115 | button = orangeMap(button); 116 | } else if(modifier == 2) { 117 | button = greenMap(button); 118 | } else if((modifier & 1) == caplock && isupper(button)) { 119 | button = button + 32; // make lowercase 120 | } 121 | } 122 | 123 | return button; 124 | } 125 | 126 | static void rpc_chatpad_rx_callback( 127 | FuriHalSerialHandle* handle, 128 | FuriHalSerialRxEvent event, 129 | void* context) { 130 | furi_check(context); 131 | RpcChatpad* chatpad = context; 132 | if(event == FuriHalSerialRxEventData) { 133 | uint8_t data = furi_hal_serial_async_rx(handle); 134 | furi_stream_buffer_send(chatpad->rx_stream, (void*)&data, 1, FuriWaitForever); 135 | } 136 | } 137 | 138 | static bool rpc_chatpad_init(RpcChatpad* chatpad) { 139 | furi_check(chatpad); 140 | const uint8_t init_sequence[] = {0x87, 0x02, 0x8C, 0x1F, 0xCC}; 141 | for(size_t retry_loop = 0; retry_loop < 5; retry_loop++) { 142 | furi_hal_serial_tx(chatpad->lpuart_handle, init_sequence, 5); 143 | uint8_t init_response[8]; 144 | size_t bytes_read = 145 | furi_stream_buffer_receive(chatpad->rx_stream, &init_response, 8, 1000); 146 | if(bytes_read != 8) { 147 | FURI_LOG_E(TAG, "Failed to get response from initialize chatpad."); 148 | furi_delay_ms(500); 149 | continue; 150 | } 151 | furi_delay_ms(500); 152 | return true; 153 | } 154 | return false; 155 | } 156 | 157 | static void rpc_chatpad_heartbeat(RpcChatpad* chatpad) { 158 | const uint8_t heartbeat_sequence[] = {0x87, 0x02, 0x8C, 0x1B, 0xD0}; 159 | furi_hal_serial_tx(chatpad->lpuart_handle, heartbeat_sequence, 5); 160 | } 161 | 162 | #define rpc_chatpad_api_lock_is_locked(_lock) (!(furi_event_flag_get(_lock) & API_LOCK_EVENT)) 163 | 164 | static int32_t rpc_chatpad_worker(void* context) { 165 | furi_check(context); 166 | RpcChatpad* chatpad = context; 167 | FURI_LOG_D(TAG, "Chatpad worker started."); 168 | rpc_chatpad_init(chatpad); 169 | 170 | uint32_t line = 0; 171 | bool show = true; 172 | uint8_t caplock = 0; 173 | while(rpc_chatpad_api_lock_is_locked(chatpad->worker_thread_exit_lock)) { 174 | if(line++ % 100 == 0) { 175 | FURI_LOG_T(TAG, "Chatpad heartbeat."); 176 | rpc_chatpad_heartbeat(chatpad); 177 | } 178 | uint8_t data[8]; 179 | size_t bytes_read = furi_stream_buffer_receive(chatpad->rx_stream, &data, 8, 20); 180 | if(bytes_read == 0) { 181 | continue; 182 | } 183 | if(bytes_read != 8) { 184 | FURI_LOG_T(TAG, "Invalid data size: %zu", bytes_read); 185 | continue; 186 | } 187 | 188 | if(data[0] != 0xB4 && data[0] != 0xA5) { 189 | FURI_LOG_D( 190 | TAG, 191 | "Invalid data: %02x %02x %02x %02x %02x %02x %02x %02x", 192 | data[0], 193 | data[1], 194 | data[2], 195 | data[3], 196 | data[4], 197 | data[5], 198 | data[6], 199 | data[7]); 200 | 201 | uint8_t sync_bytes = 0; 202 | for(size_t i = 1; i < 8; i++) { 203 | if(data[i] == 0xA5 || data[i] == 0xB4) { 204 | sync_bytes = i; 205 | } 206 | } 207 | if(sync_bytes != 0) { 208 | bytes_read = 209 | furi_stream_buffer_receive(chatpad->rx_stream, &data, sync_bytes, 100); 210 | if(bytes_read != sync_bytes) { 211 | FURI_LOG_D( 212 | TAG, 213 | "Failed to sync data. attempted: %d actual: %d", 214 | sync_bytes, 215 | bytes_read); 216 | } else { 217 | FURI_LOG_T(TAG, "Synced data."); 218 | } 219 | } 220 | 221 | continue; 222 | } 223 | 224 | if((data[0] + data[1] + data[2] + data[3] + data[4] + data[5] + data[6] + data[7]) % 256 != 225 | 0) { 226 | FURI_LOG_E( 227 | TAG, 228 | "Checksum failed. data: %02x %02x %02x %02x %02x %02x %02x %02x", 229 | data[0], 230 | data[1], 231 | data[2], 232 | data[3], 233 | data[4], 234 | data[5], 235 | data[6], 236 | data[7]); 237 | continue; 238 | } 239 | 240 | if(data[0] == 0xB4 && data[1] == 0xC5 && show) { 241 | uint8_t mod = data[3]; 242 | uint8_t btn = data[4]; 243 | if(mod != 0 || btn != 0) { 244 | char ch = rpc_chatpad_code_to_char(btn, mod, caplock); 245 | if(mod == 8 && btn != 0) { 246 | // People button + other key. 247 | FURI_LOG_T(TAG, "Char: %c Mod: %02x, Btn: %02x", ch, mod, btn); 248 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 249 | rpc_keyboard_publish_macro(rpc_keyboard, ch); 250 | furi_record_close(RECORD_RPC_KEYBOARD); 251 | } else if(mod == 5) { 252 | // Caps lock button pressed. 253 | furi_hal_vibro_on(true); 254 | caplock = (caplock == 0) ? 1 : 0; 255 | furi_delay_ms(100); 256 | furi_hal_vibro_on(false); 257 | } else if(ch != '\0') { 258 | FURI_LOG_T(TAG, "Char: %c Mod: %02x, Btn: %02x", ch, mod, btn); 259 | RpcKeyboard* rpc_keyboard = furi_record_open(RECORD_RPC_KEYBOARD); 260 | rpc_keyboard_publish_char(rpc_keyboard, ch); 261 | furi_record_close(RECORD_RPC_KEYBOARD); 262 | } else if(btn != 0) { 263 | FURI_LOG_D(TAG, "Mod: %02x, Btn: %02x", mod, btn); 264 | } 265 | } 266 | show = false; 267 | line = 1; 268 | } else if(data[0] == 0xA5) { 269 | if(!chatpad->ready) { 270 | FURI_LOG_I(TAG, "Chatpad ready."); 271 | chatpad->ready = true; 272 | } 273 | show = true; 274 | } 275 | } 276 | 277 | FURI_LOG_D(TAG, "Chatpad worker exiting."); 278 | return 0; 279 | } 280 | 281 | RpcChatpad* rpc_chatpad_alloc(void) { 282 | RpcChatpad* chatpad = malloc(sizeof(RpcChatpad)); 283 | const uint32_t lpuart_baud = 19200; 284 | chatpad->ready = false; 285 | chatpad->worker_thread_exit_lock = api_lock_alloc_locked(); 286 | chatpad->lpuart_bus = FuriHalBusLPUART1; 287 | chatpad->lpuart_handle = furi_hal_serial_control_acquire(FuriHalSerialIdLpuart); 288 | chatpad->lpuart_already_opened = furi_hal_bus_is_enabled(chatpad->lpuart_bus); 289 | if(!chatpad->lpuart_already_opened) { 290 | furi_hal_serial_init(chatpad->lpuart_handle, lpuart_baud); 291 | } else { 292 | FURI_LOG_D(TAG, "LPUART already initialized."); 293 | } 294 | furi_hal_serial_set_br(chatpad->lpuart_handle, lpuart_baud); 295 | chatpad->rx_stream = furi_stream_buffer_alloc(32, 8); 296 | furi_hal_serial_async_rx_start( 297 | chatpad->lpuart_handle, rpc_chatpad_rx_callback, chatpad, false); 298 | chatpad->worker_thread = 299 | furi_thread_alloc_ex("rpc_chatpad_work", 1024, rpc_chatpad_worker, chatpad); 300 | furi_thread_start(chatpad->worker_thread); 301 | return chatpad; 302 | } 303 | 304 | void rpc_chatpad_free(RpcChatpad* chatpad) { 305 | furi_check(chatpad); 306 | chatpad->ready = false; 307 | furi_hal_serial_async_rx_stop(chatpad->lpuart_handle); 308 | api_lock_unlock(chatpad->worker_thread_exit_lock); 309 | furi_thread_join(chatpad->worker_thread); 310 | chatpad->worker_thread = NULL; 311 | FURI_LOG_D(TAG, "Chatpad freeing resources."); 312 | if(chatpad->lpuart_already_opened) { 313 | furi_hal_serial_deinit(chatpad->lpuart_handle); 314 | } 315 | furi_hal_serial_control_release(chatpad->lpuart_handle); 316 | chatpad->lpuart_handle = NULL; 317 | furi_stream_buffer_free(chatpad->rx_stream); 318 | chatpad->rx_stream = NULL; 319 | free(chatpad); 320 | } 321 | 322 | bool rpc_chatpad_ready(RpcChatpad* chatpad) { 323 | furi_check(chatpad); 324 | return chatpad->ready; 325 | } 326 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flipper-zero-input 2 | Alternative input for the Flipper Zero 3 | 4 | ![Flipper Zero](flipper_zero.png) 5 | 6 | 7 | ## Introduction 8 | 9 | This project aims to provide an alternative input method for the Flipper Zero. The Flipper Zero is a multi-tool device that can be used for a variety of tasks, such as hacking, pentesting, and hardware hacking. However, the default input method for the Flipper Zero is limited to using the D-Pad for input. This often results in people typing short names for their files, like `liv.sub` instead of a descriptive name like `living room light.sub`. This project aims to expand the input capabilities of the Flipper Zero by adding support for external input devices, such as the Chatpad and Mobile phone input devices. 10 | 11 | - Installing firmware with Chatpad and Mobile input support 12 | - [Firmware Installation](#firmware-installation) 13 | 14 | - Bluetooth Phone 15 | - [Bluetooth Setup](#bluetooth-setup) 16 | 17 | - Chatpad 18 | - [Chatpad Hardware](#chatpad-hardware) 19 | - [Chatpad Setup](#chatpad-setup) 20 | 21 | 22 | ## Firmware Installation 23 | 24 | The following steps will guide you through the process of installing the latest version of the firmware on your Flipper Zero. 25 | 26 | Install prerequisites before proceeding: 27 | 28 | - Install [git](https://git-scm.com/downloads) 29 | - NOTE: I have a YouTube video on setting up a [Windows development environment](https://youtu.be/gqovwRkn2xw) or [Ubuntu environment](https://youtu.be/PaqK1H9brZQ). 30 | 31 | The following directions are for **Windows users using a command prompt**. If you are using a different operating system or using PowerShell, you will need to adjust the commands accordingly. If you don't want to use the latest `dev` branch, you can replace `"dev"` with the branch or tag you would like to use. Please see [using non-dev branches or tags](#using-non-dev-branches-or-tags) below for more information. 32 | 33 | Before proceeding, be sure to connect your Flipper Zero to your computer. Close qFlipper, lab.flipper.net, or any other application that may be using your Flipper Zero USB port! 34 | 35 | ### Official firmware 36 | 37 | ```bash 38 | mkdir \repos 39 | cd \repos 40 | rd /s /q flipperzero-firmware 2>nul 41 | git clone --recursive -j 8 https://github.com/flipperdevices/flipperzero-firmware.git flipperzero-firmware 42 | rd /s /q flipper-zero-input 2>nul 43 | git clone https://github.com/jamisonderek/flipper-zero-input.git 44 | cd flipperzero-firmware 45 | git pull 46 | git checkout "1.1.2" 47 | cd applications 48 | xcopy ..\..\flipper-zero-input\firmware-overlay\ofw-1.1.2\applications\*.* . /e /y 49 | cd .. 50 | git stash push -u 51 | git checkout "dev" 52 | git stash pop 53 | fbt vscode_dist 54 | fbt COMPACT=1 DEBUG=0 FORCE=1 flash_usb_full 55 | 56 | ``` 57 | 58 | ### Momentum firmware 59 | 60 | ```bash 61 | mkdir \repos 62 | cd \repos 63 | rd /s /q flipperzero-firmware 2>nul 64 | git clone --recursive -j 8 https://github.com/Next-Flip/Momentum-Firmware.git flipperzero-firmware 65 | rd /s /q flipper-zero-input 2>nul 66 | git clone https://github.com/jamisonderek/flipper-zero-input.git 67 | cd flipperzero-firmware 68 | git pull 69 | git checkout "mntm-008" 70 | cd applications 71 | xcopy ..\..\flipper-zero-input\firmware-overlay\mntm-008\applications\*.* . /e /y 72 | cd .. 73 | git stash push -u 74 | git checkout "dev" 75 | git stash pop 76 | fbt vscode_dist 77 | fbt COMPACT=1 DEBUG=0 FORCE=1 flash_usb_full 78 | 79 | ``` 80 | 81 | ### Unleashed firmware 82 | 83 | ```bash 84 | mkdir \repos 85 | cd \repos 86 | rd /s /q flipperzero-firmware 2>nul 87 | git clone --recursive -j 8 https://github.com/DarkFlippers/unleashed-firmware.git flipperzero-firmware 88 | rd /s /q flipper-zero-input 2>nul 89 | git clone https://github.com/jamisonderek/flipper-zero-input.git 90 | cd flipperzero-firmware 91 | git pull 92 | git checkout "unlshd-079" 93 | cd applications 94 | xcopy ..\..\flipper-zero-input\firmware-overlay\unl-079\applications\*.* . /e /y 95 | cd .. 96 | git stash push -u 97 | git checkout "dev" 98 | git stash pop 99 | fbt vscode_dist 100 | fbt COMPACT=1 DEBUG=0 FORCE=1 flash_usb_full 101 | 102 | ``` 103 | 104 | ### RogueMaster firmware 105 | 106 | ```bash 107 | mkdir \repos 108 | cd \repos 109 | rd /s /q flipperzero-firmware 2>nul 110 | git clone --recursive -j 8 https://github.com/RogueMaster/flipperzero-firmware-wPlugins.git flipperzero-firmware 111 | rd /s /q flipper-zero-input 2>nul 112 | git clone https://github.com/jamisonderek/flipper-zero-input.git 113 | cd flipperzero-firmware 114 | git pull 115 | git checkout "RM1202-0837-0.420.0-6d10bad" 116 | cd applications 117 | xcopy ..\..\flipper-zero-input\firmware-overlay\rm-1202-0837-0.420.0-6d10bad\applications\*.* . /e /y 118 | cd .. 119 | git stash push -u 120 | git checkout "dev" 121 | git stash pop 122 | fbt vscode_dist 123 | fbt COMPACT=1 DEBUG=0 FORCE=1 flash_usb_full 124 | 125 | ``` 126 | 127 | ### Using non-dev branches or tags 128 | NOTE: instead of `git checkout "dev"` you can replace `"dev"` with the branch or tag of the firmware you would like to use. The following branches and tags are available: 129 | 130 | Official firmware: 131 | - `dev` 132 | - `release` 133 | - [tags](https://github.com/flipperdevices/flipperzero-firmware/tags), like `1.1.2` 134 | 135 | Momentum firmware: 136 | 137 | - `dev` 138 | - `release` 139 | - [tags](https://github.com/Next-Flip/Momentum-Firmware/tags), like `mntm-008` 140 | 141 | Unleashed firmware: 142 | - `dev` 143 | - `release` 144 | - [tags](https://github.com/DarkFlippers/unleashed-firmware/tags), like `unlshd-079` 145 | 146 | RogueMaster firmware: 147 | - `420` 148 | - [tags](https://github.com/RogueMaster/flipperzero-firmware-wPlugins/tags), like `RM1202-0837-0.420.0-6d10bad` 149 | 150 | 151 | ### Notes about the above scripts 152 | 153 | - You can replace `\repros` with any directory you would like to use. 154 | 155 | - We recursively clone the firmware repo into a folder named `flipperzero-firmware`. You can choose a different name if you would like, but be sure to also update the other commands to use the new name. 156 | 157 | - We clone the `flipper-zero-input` repo into a folder named `flipper-zero-input`. You can choose a different name if you would like, but be sure to also update the other commands to use the new name. 158 | 159 | - The firmware-overlays contains updated `text_input.c`, `rpc.c`, `rpc_storage.c` and `settings/applications.fam` files which were part of the original firmware. We sync our firmware repo to the version I was using when I made the change, that way we know those files ONLY contain the necessary modifications. 160 | 161 | - The overlays are slightly different between firmware, since they have different text_input.c implementations (everyone has a slightly different keyboard implementation). 162 | 163 | - Once we apply the overlay, we use `git stash push -u` to stash away our edits. We then checkout the new version of firmware and use `git stash pop` to apply our edits in the new code. This assumes that the two versions of the firmware are compatible with the changes we made. If they are not, you will need to manually apply the changes to the new firmware. 164 | 165 | ## Bluetooth Setup 166 | 167 | The easiest way to input text on the Flipper Zero is to use a Bluetooth on your phone. This requires that you are running the [Flipper Zero mobile app](https://docs.flipper.net/mobile-app) and have paired it with your Flipper Zero. 168 | 169 | NOTE: if you switched firmware the pairing may be lost, so you may need to forget the device and add it again. 170 | 171 | Once you have the mobile app working, the following steps will send text to the text input to the Flipper Zero. 172 | 1. On your Flipper, go to `Settings`/`Chatpad`. This will create a "input-line.txt" file on the SD card. You can exit the settings, if you don't actually have a chatpad. 173 | 2. Open the [mobile app](https://docs.flipper.net/mobile-app) and connect to your Flipper Zero. 174 | 3. Click on the "Options" (it's on the first tab). 175 | 4. Click on the "File Manager". 176 | 5. You should be on the SD card folder (if not, click on the `/ext` folder). 177 | 6. Open the "input-line.txt" file (this file was created from step 1 above.) 178 | 7. Type a line of text you would like to send to the Flipper Zero. 179 | - NOTE: If you press enter on the phone, only the first line will used (and then a "save" will be submitted) 180 | 8. On your Flipper make sure you are in the text input screen (the one that shows a keyboard). 181 | 9. On your phone, click on the "Save" (or checkmark) button at the top right. 182 | 10. The text will be sent to the Flipper Zero and you should see it in the text input! 183 | 184 | In the future, it would be great to have a more seamless integration with the Flipper Zero mobile app. You can find the existing apps here: 185 | - [https://github.com/flipperdevices/Flipper-Android-App](https://github.com/flipperdevices/Flipper-Android-App) 186 | - [https://github.com/flipperdevices/Flipper-iOS-App](https://github.com/flipperdevices/Flipper-Android-App) 187 | 188 | Let me know if you want to collaborate on a mobile app improvement! (@CodeAllNight on Discord.) 189 | 190 | ## Chatpad Hardware 191 | 192 | ![Chatpad wiring](chatpad_wiring.png) 193 | 194 | The Xbox 360 Chatpad is a small keyboard that was originally designed for the Xbox 360. You can still find them on eBay for around $15USD. The model I used was **X814365-001**, which is a wired keypad. 195 | 196 | To take it apart you will need: 197 | - T6 screwdriver 198 | - A small phillips screwdriver 199 | - Something to pry the case apart (I used a small fork) 200 | 201 | I ordered a separate 7-pin, 1.25mm to Dupont 2.54mm adapter from Amazon. This allows me to connect the Chatpad to the Flipper Zero. You could also cut the existing cable and solder the wires to the Flipper Zero, but I wanted to keep the Chatpad intact. The adapter I used was the following: 202 | - [7-pin, 1.25mm to Dupont 2.54mm adapter](https://www.amazon.com/gp/product/B07PWZTC88) 203 | 204 | To take the Chatpad apart, follow these steps: 205 | - Remove the four T6 screws on the back of the Chatpad. 206 | - Carefully pry the case apart. 207 | - Remove the ribbon cable from the PCB. 208 | - Remove the 5 tiny phillips screws holding the PCB in place. 209 | - Carefully remove the PCB from the case. 210 | 211 | Connect the adapter to the Chatpad PCB: 212 | - Connect the 7-pin, 1.25mm connector to the Chatpad PCB. 213 | - Connect the Dupont 2.54mm connector to the Flipper Zero. 214 | - Pin 1 on the Chatpad has a little triangle on the PCB. 215 | - Connect Pin 1 on the Chatpad to Pin 9 `3V3` on the Flipper Zero. 216 | - Connect Pin 2 on the Chatpad to Pin 15 `C1` on the Flipper Zero. 217 | - Connect Pin 3 on the Chatpad to Pin 16 `C2` on the Flipper Zero. 218 | - Connect Pin 4 on the Chatpad to Pin 18 `GND` on the Flipper Zero. 219 | - Pins 5, 6, and 7 on the Chatpad are not used (they are for audio). 220 | 221 | ## Chatpad Setup 222 | 223 | Once you have installed the firmware using the [Quick Installation](#quick-installation) or [Firmware Overlay Installation](#firmware-overlay-installation) steps, you can connect the Chatpad to the Flipper Zero. **Every time you restart the Flipper Zero you will need to reconnect the Chatpad.** 224 | 225 | 1. Connect the Chatpad to the Flipper Zero (Flipper GPIO pins 9, 15, 16, 18 -- see [Chatpad Hardware](#chatpad-hardware) for more details). 226 | 2. Turn on the Flipper Zero. 227 | 3. Click "OK" and then select the "Settings" option. 228 | 4. Select the "Chatpad" option. 229 | 5. Click on the "Chatpad" menu item. Click "OK" to turn on the chatpad. 230 | 6. You should see "Chatpad is ON" and then "Chatpad is READY". 231 | 7. Press a key on the Chatpad and you should see the key pressed on the Flipper Zero screen. 232 | 233 | You can set Macros in the Chatpad Config. 234 | 235 | 1. Choose "Config" from the Chatpad menu. 236 | 2. For the "Macro" option, choose the letter you would like to assign a macro to. 237 | 3. Click "OK" and then type the text you would like to assign to the macro. (You can use the Chatpad to type the text.) 238 | 4. Cick "Save" 239 | 5. To use the macro, hold the "People" key (next to the green button on the chatpad) and then press the letter you assigned the macro to. 240 | 241 | ## Support 242 | 243 | If you have need help, I am here for you. Also, I would love your feedback on Flipper Zero input devices! The best way to get support is tag me **(@CodeAllNight)** on Discord in any of the Flipper Zero firmware servers. 244 | 245 | If you want to support my work, you can donate via [https://ko-fi.com/codeallnight](https://ko-fi.com/codeallnight) or you can [buy a FlipBoard](https://www.tindie.com/products/makeithackin/flipboard-macropad-keyboard-for-flipper-zero/) from MakeItHackin with software & tutorials from me (@CodeAllNight). -------------------------------------------------------------------------------- /firmware-overlay/ofw-1.1.2/applications/services/rpc/rpc.c: -------------------------------------------------------------------------------- 1 | #include "rpc_i.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | #include 20 | 21 | #define TAG "RpcSrv" 22 | 23 | typedef enum { 24 | RpcEvtNewData = (1 << 0), 25 | RpcEvtDisconnect = (1 << 1), 26 | } RpcEvtFlags; 27 | 28 | #define RPC_ALL_EVENTS (RpcEvtNewData | RpcEvtDisconnect) 29 | 30 | DICT_DEF2(RpcHandlerDict, pb_size_t, M_DEFAULT_OPLIST, RpcHandler, M_POD_OPLIST) 31 | 32 | typedef struct { 33 | RpcSystemAlloc alloc; 34 | RpcSystemFree free; 35 | void* context; 36 | } RpcSystemCallbacks; 37 | 38 | static RpcSystemCallbacks rpc_systems[] = { 39 | { 40 | .alloc = rpc_system_system_alloc, 41 | .free = NULL, 42 | }, 43 | { 44 | .alloc = rpc_system_storage_alloc, 45 | .free = rpc_system_storage_free, 46 | }, 47 | { 48 | .alloc = rpc_system_app_alloc, 49 | .free = rpc_system_app_free, 50 | }, 51 | { 52 | .alloc = rpc_system_gui_alloc, 53 | .free = rpc_system_gui_free, 54 | }, 55 | { 56 | .alloc = rpc_system_gpio_alloc, 57 | .free = NULL, 58 | }, 59 | { 60 | .alloc = rpc_system_property_alloc, 61 | .free = NULL, 62 | }, 63 | { 64 | .alloc = rpc_desktop_alloc, 65 | .free = rpc_desktop_free, 66 | }, 67 | }; 68 | 69 | struct RpcSession { 70 | Rpc* rpc; 71 | 72 | FuriThread* thread; 73 | 74 | RpcHandlerDict_t handlers; 75 | FuriStreamBuffer* stream; 76 | PB_Main* decoded_message; 77 | bool terminate; 78 | void** system_contexts; 79 | bool decode_error; 80 | 81 | FuriMutex* callbacks_mutex; 82 | RpcSendBytesCallback send_bytes_callback; 83 | RpcBufferIsEmptyCallback buffer_is_empty_callback; 84 | RpcSessionClosedCallback closed_callback; 85 | RpcSessionTerminatedCallback terminated_callback; 86 | RpcOwner owner; 87 | void* context; 88 | }; 89 | 90 | struct Rpc { 91 | FuriMutex* busy_mutex; 92 | }; 93 | 94 | RpcOwner rpc_session_get_owner(RpcSession* session) { 95 | furi_check(session); 96 | return session->owner; 97 | } 98 | 99 | static void rpc_close_session_process(const PB_Main* request, void* context) { 100 | furi_assert(request); 101 | furi_assert(context); 102 | 103 | RpcSession* session = (RpcSession*)context; 104 | 105 | rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); 106 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 107 | if(session->closed_callback) { 108 | session->closed_callback(session->context); 109 | } else { 110 | FURI_LOG_W(TAG, "Session stop isn't processed by transport layer"); 111 | } 112 | furi_mutex_release(session->callbacks_mutex); 113 | } 114 | 115 | void rpc_session_set_context(RpcSession* session, void* context) { 116 | furi_check(session); 117 | 118 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 119 | session->context = context; 120 | furi_mutex_release(session->callbacks_mutex); 121 | } 122 | 123 | void rpc_session_set_close_callback(RpcSession* session, RpcSessionClosedCallback callback) { 124 | furi_check(session); 125 | 126 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 127 | session->closed_callback = callback; 128 | furi_mutex_release(session->callbacks_mutex); 129 | } 130 | 131 | void rpc_session_set_send_bytes_callback(RpcSession* session, RpcSendBytesCallback callback) { 132 | furi_check(session); 133 | 134 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 135 | session->send_bytes_callback = callback; 136 | furi_mutex_release(session->callbacks_mutex); 137 | } 138 | 139 | void rpc_session_set_buffer_is_empty_callback( 140 | RpcSession* session, 141 | RpcBufferIsEmptyCallback callback) { 142 | furi_check(session); 143 | 144 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 145 | session->buffer_is_empty_callback = callback; 146 | furi_mutex_release(session->callbacks_mutex); 147 | } 148 | 149 | void rpc_session_set_terminated_callback( 150 | RpcSession* session, 151 | RpcSessionTerminatedCallback callback) { 152 | furi_check(session); 153 | 154 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 155 | session->terminated_callback = callback; 156 | furi_mutex_release(session->callbacks_mutex); 157 | } 158 | 159 | /* Doesn't forbid using rpc_feed_bytes() after session close - it's safe. 160 | * Because any bytes received in buffer will be flushed before next session. 161 | * If bytes get into stream buffer before it's get epmtified and this 162 | * command is gets processed - it's safe either. But case of it is quite 163 | * odd: client sends close request and sends command after. 164 | */ 165 | size_t rpc_session_feed( 166 | RpcSession* session, 167 | const uint8_t* encoded_bytes, 168 | size_t size, 169 | uint32_t timeout) { 170 | furi_check(session); 171 | furi_check(encoded_bytes); 172 | 173 | if(!size) return 0; 174 | 175 | size_t bytes_sent = furi_stream_buffer_send(session->stream, encoded_bytes, size, timeout); 176 | 177 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtNewData); 178 | 179 | return bytes_sent; 180 | } 181 | 182 | size_t rpc_session_get_available_size(RpcSession* session) { 183 | furi_check(session); 184 | return furi_stream_buffer_spaces_available(session->stream); 185 | } 186 | 187 | bool rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) { 188 | furi_assert(istream); 189 | furi_assert(buf); 190 | RpcSession* session = istream->state; 191 | furi_assert(session); 192 | furi_assert(istream->bytes_left); 193 | 194 | if(session->terminate) { 195 | return false; 196 | } 197 | 198 | uint32_t flags = 0; 199 | size_t bytes_received = 0; 200 | 201 | while(1) { 202 | bytes_received += furi_stream_buffer_receive( 203 | session->stream, buf + bytes_received, count - bytes_received, 0); 204 | if(furi_stream_buffer_is_empty(session->stream)) { 205 | if(session->buffer_is_empty_callback) { 206 | session->buffer_is_empty_callback(session->context); 207 | } 208 | } 209 | if(session->decode_error) { 210 | /* never go out till RPC_EVENT_DISCONNECT come */ 211 | bytes_received = 0; 212 | } 213 | if(count == bytes_received) { 214 | break; 215 | } else { 216 | flags = furi_thread_flags_wait(RPC_ALL_EVENTS, FuriFlagWaitAny, FuriWaitForever); 217 | if(flags & RpcEvtDisconnect) { 218 | if(furi_stream_buffer_is_empty(session->stream)) { 219 | session->terminate = true; 220 | istream->bytes_left = 0; 221 | bytes_received = 0; 222 | break; 223 | } else { 224 | /* Save disconnect flag and continue reading buffer */ 225 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); 226 | } 227 | } else if(flags & RpcEvtNewData) { 228 | // Just wake thread up 229 | } 230 | } 231 | } 232 | 233 | #ifdef SRV_RPC_DEBUG 234 | rpc_debug_print_data("INPUT", buf, bytes_received); 235 | #endif 236 | 237 | return count == bytes_received; 238 | } 239 | 240 | static bool rpc_pb_content_callback(pb_istream_t* stream, const pb_field_t* field, void** arg) { 241 | furi_assert(stream); 242 | RpcSession* session = stream->state; 243 | furi_assert(session); 244 | furi_assert(field); 245 | 246 | RpcHandler* handler = RpcHandlerDict_get(session->handlers, field->tag); 247 | 248 | if(handler && handler->decode_submessage) { 249 | handler->decode_submessage(stream, field, arg); 250 | } 251 | 252 | return true; 253 | } 254 | 255 | static int32_t rpc_session_worker(void* context) { 256 | furi_assert(context); 257 | RpcSession* session = (RpcSession*)context; 258 | Rpc* rpc = session->rpc; 259 | 260 | FURI_LOG_D(TAG, "Session started"); 261 | 262 | while(1) { 263 | pb_istream_t istream = { 264 | .callback = rpc_pb_stream_read, 265 | .state = session, 266 | .errmsg = NULL, 267 | .bytes_left = SIZE_MAX, 268 | }; 269 | 270 | bool message_decode_failed = false; 271 | 272 | if(pb_decode_ex(&istream, &PB_Main_msg, session->decoded_message, PB_DECODE_DELIMITED)) { 273 | #ifdef SRV_RPC_DEBUG 274 | FURI_LOG_I(TAG, "INPUT:"); 275 | rpc_debug_print_message(session->decoded_message); 276 | #endif 277 | RpcHandler* handler = 278 | RpcHandlerDict_get(session->handlers, session->decoded_message->which_content); 279 | 280 | if(handler && handler->message_handler) { 281 | furi_check(furi_mutex_acquire(rpc->busy_mutex, FuriWaitForever) == FuriStatusOk); 282 | handler->message_handler(session->decoded_message, handler->context); 283 | furi_check(furi_mutex_release(rpc->busy_mutex) == FuriStatusOk); 284 | } else if(session->decoded_message->which_content == 0) { 285 | /* Receiving zeroes means message is 0-length, which 286 | * is valid for proto3: all fields are filled with default values. 287 | * 0 - is default value for which_content field. 288 | * Mark it as decode error, because there is no content message 289 | * in Main message with tag 0. 290 | */ 291 | message_decode_failed = true; 292 | } else if(!handler && !session->terminate) { 293 | FURI_LOG_E( 294 | TAG, 295 | "Message(%d) decoded, but not implemented", 296 | session->decoded_message->which_content); 297 | rpc_send_and_release_empty( 298 | session, 299 | session->decoded_message->command_id, 300 | PB_CommandStatus_ERROR_NOT_IMPLEMENTED); 301 | } 302 | } else { 303 | message_decode_failed = true; 304 | } 305 | 306 | if(message_decode_failed) { 307 | furi_stream_buffer_reset(session->stream); 308 | if(!session->terminate) { 309 | /* Protobuf can't determine start and end of message. 310 | * Handle this by adding varint at beginning 311 | * of a message (PB_ENCODE_DELIMITED). But decoding fail 312 | * means we can't be sure next bytes are varint for next 313 | * message, so the only way to close session. 314 | * RPC itself can't make decision to close session. It has 315 | * to notify: 316 | * 1) down layer (transport) 317 | * 2) other side (companion app) 318 | * Who are responsible to handle RPC session lifecycle. 319 | * Companion receives 2 messages: ERROR_DECODE and session_closed. 320 | */ 321 | FURI_LOG_E(TAG, "Decode failed, error: \'%.128s\'", PB_GET_ERROR(&istream)); 322 | session->decode_error = true; 323 | rpc_send_and_release_empty(session, 0, PB_CommandStatus_ERROR_DECODE); 324 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 325 | if(session->closed_callback) { 326 | session->closed_callback(session->context); 327 | } 328 | furi_mutex_release(session->callbacks_mutex); 329 | 330 | if(session->owner == RpcOwnerBle) { 331 | // Disconnect BLE session 332 | FURI_LOG_E("RPC", "BLE session closed due to a decode error"); 333 | Bt* bt = furi_record_open(RECORD_BT); 334 | bt_profile_restore_default(bt); 335 | furi_record_close(RECORD_BT); 336 | FURI_LOG_E("RPC", "Finished disconnecting the BLE session"); 337 | } 338 | } 339 | } 340 | 341 | pb_release(&PB_Main_msg, session->decoded_message); 342 | 343 | if(session->terminate) { 344 | FURI_LOG_D(TAG, "Session terminated"); 345 | break; 346 | } 347 | } 348 | 349 | return 0; 350 | } 351 | 352 | static void rpc_session_thread_pending_callback(void* context, uint32_t arg) { 353 | UNUSED(arg); 354 | RpcSession* session = (RpcSession*)context; 355 | 356 | for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { 357 | if(rpc_systems[i].free) { 358 | (rpc_systems[i].free)(session->system_contexts[i]); 359 | } 360 | } 361 | free(session->system_contexts); 362 | free(session->decoded_message); 363 | RpcHandlerDict_clear(session->handlers); 364 | furi_stream_buffer_free(session->stream); 365 | 366 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 367 | if(session->terminated_callback) { 368 | session->terminated_callback(session->context); 369 | } 370 | furi_mutex_release(session->callbacks_mutex); 371 | 372 | furi_mutex_free(session->callbacks_mutex); 373 | furi_thread_join(session->thread); 374 | furi_thread_free(session->thread); 375 | free(session); 376 | } 377 | 378 | static void 379 | rpc_session_thread_state_callback(FuriThread* thread, FuriThreadState state, void* context) { 380 | UNUSED(thread); 381 | if(state == FuriThreadStateStopped) { 382 | furi_timer_pending_callback(rpc_session_thread_pending_callback, context, 0); 383 | } 384 | } 385 | 386 | RpcSession* rpc_session_open(Rpc* rpc, RpcOwner owner) { 387 | furi_check(rpc); 388 | 389 | RpcSession* session = malloc(sizeof(RpcSession)); 390 | session->callbacks_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 391 | session->stream = furi_stream_buffer_alloc(RPC_BUFFER_SIZE, 1); 392 | session->rpc = rpc; 393 | session->terminate = false; 394 | session->decode_error = false; 395 | session->owner = owner; 396 | RpcHandlerDict_init(session->handlers); 397 | 398 | session->decoded_message = malloc(sizeof(PB_Main)); 399 | session->decoded_message->cb_content.funcs.decode = rpc_pb_content_callback; 400 | session->decoded_message->cb_content.arg = session; 401 | 402 | session->system_contexts = malloc(COUNT_OF(rpc_systems) * sizeof(void*)); 403 | for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { 404 | session->system_contexts[i] = rpc_systems[i].alloc(session); 405 | } 406 | 407 | RpcHandler rpc_handler = { 408 | .message_handler = rpc_close_session_process, 409 | .decode_submessage = NULL, 410 | .context = session, 411 | }; 412 | rpc_add_handler(session, PB_Main_stop_session_tag, &rpc_handler); 413 | 414 | session->thread = furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session); 415 | 416 | furi_thread_set_state_context(session->thread, session); 417 | furi_thread_set_state_callback(session->thread, rpc_session_thread_state_callback); 418 | 419 | furi_thread_start(session->thread); 420 | 421 | return session; 422 | } 423 | 424 | void rpc_session_close(RpcSession* session) { 425 | furi_check(session); 426 | furi_check(session->rpc); 427 | 428 | rpc_session_set_send_bytes_callback(session, NULL); 429 | rpc_session_set_close_callback(session, NULL); 430 | rpc_session_set_buffer_is_empty_callback(session, NULL); 431 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); 432 | } 433 | 434 | void rpc_on_system_start(void* p) { 435 | UNUSED(p); 436 | Rpc* rpc = malloc(sizeof(Rpc)); 437 | 438 | rpc->busy_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 439 | 440 | Cli* cli = furi_record_open(RECORD_CLI); 441 | cli_add_command( 442 | cli, "start_rpc_session", CliCommandFlagParallelSafe, rpc_cli_command_start_session, rpc); 443 | 444 | furi_record_create(RECORD_RPC, rpc); 445 | rpc_keyboard_register(); 446 | } 447 | 448 | void rpc_add_handler(RpcSession* session, pb_size_t message_tag, RpcHandler* handler) { 449 | furi_assert(RpcHandlerDict_get(session->handlers, message_tag) == NULL); 450 | 451 | RpcHandlerDict_set_at(session->handlers, message_tag, *handler); 452 | } 453 | 454 | void rpc_send(RpcSession* session, PB_Main* message) { 455 | furi_assert(session); 456 | furi_assert(message); 457 | 458 | pb_ostream_t ostream = PB_OSTREAM_SIZING; 459 | 460 | #ifdef SRV_RPC_DEBUG 461 | FURI_LOG_I(TAG, "OUTPUT:"); 462 | rpc_debug_print_message(message); 463 | #endif 464 | 465 | bool result = pb_encode_ex(&ostream, &PB_Main_msg, message, PB_ENCODE_DELIMITED); 466 | furi_check(result && ostream.bytes_written); 467 | 468 | uint8_t* buffer = malloc(ostream.bytes_written); 469 | ostream = pb_ostream_from_buffer(buffer, ostream.bytes_written); 470 | 471 | pb_encode_ex(&ostream, &PB_Main_msg, message, PB_ENCODE_DELIMITED); 472 | 473 | #ifdef SRV_RPC_DEBUG 474 | rpc_debug_print_data("OUTPUT", buffer, ostream.bytes_written); 475 | #endif 476 | 477 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 478 | if(session->send_bytes_callback) { 479 | session->send_bytes_callback(session->context, buffer, ostream.bytes_written); 480 | } 481 | furi_mutex_release(session->callbacks_mutex); 482 | 483 | free(buffer); 484 | } 485 | 486 | void rpc_send_and_release(RpcSession* session, PB_Main* message) { 487 | rpc_send(session, message); 488 | pb_release(&PB_Main_msg, message); 489 | } 490 | 491 | void rpc_send_and_release_empty(RpcSession* session, uint32_t command_id, PB_CommandStatus status) { 492 | furi_assert(session); 493 | 494 | PB_Main message = { 495 | .command_id = command_id, 496 | .command_status = status, 497 | .has_next = false, 498 | .which_content = PB_Main_empty_tag, 499 | }; 500 | 501 | rpc_send_and_release(session, &message); 502 | pb_release(&PB_Main_msg, &message); 503 | } 504 | -------------------------------------------------------------------------------- /firmware-overlay/unl-079/applications/services/rpc/rpc.c: -------------------------------------------------------------------------------- 1 | #include "rpc_i.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | 19 | #include 20 | 21 | #define TAG "RpcSrv" 22 | 23 | typedef enum { 24 | RpcEvtNewData = (1 << 0), 25 | RpcEvtDisconnect = (1 << 1), 26 | } RpcEvtFlags; 27 | 28 | #define RPC_ALL_EVENTS (RpcEvtNewData | RpcEvtDisconnect) 29 | 30 | DICT_DEF2(RpcHandlerDict, pb_size_t, M_DEFAULT_OPLIST, RpcHandler, M_POD_OPLIST) 31 | 32 | typedef struct { 33 | RpcSystemAlloc alloc; 34 | RpcSystemFree free; 35 | void* context; 36 | } RpcSystemCallbacks; 37 | 38 | static RpcSystemCallbacks rpc_systems[] = { 39 | { 40 | .alloc = rpc_system_system_alloc, 41 | .free = NULL, 42 | }, 43 | { 44 | .alloc = rpc_system_storage_alloc, 45 | .free = rpc_system_storage_free, 46 | }, 47 | { 48 | .alloc = rpc_system_app_alloc, 49 | .free = rpc_system_app_free, 50 | }, 51 | { 52 | .alloc = rpc_system_gui_alloc, 53 | .free = rpc_system_gui_free, 54 | }, 55 | { 56 | .alloc = rpc_system_gpio_alloc, 57 | .free = NULL, 58 | }, 59 | { 60 | .alloc = rpc_system_property_alloc, 61 | .free = NULL, 62 | }, 63 | { 64 | .alloc = rpc_desktop_alloc, 65 | .free = rpc_desktop_free, 66 | }, 67 | }; 68 | 69 | struct RpcSession { 70 | Rpc* rpc; 71 | 72 | FuriThread* thread; 73 | 74 | RpcHandlerDict_t handlers; 75 | FuriStreamBuffer* stream; 76 | PB_Main* decoded_message; 77 | bool terminate; 78 | void** system_contexts; 79 | bool decode_error; 80 | 81 | FuriMutex* callbacks_mutex; 82 | RpcSendBytesCallback send_bytes_callback; 83 | RpcBufferIsEmptyCallback buffer_is_empty_callback; 84 | RpcSessionClosedCallback closed_callback; 85 | RpcSessionTerminatedCallback terminated_callback; 86 | RpcOwner owner; 87 | void* context; 88 | }; 89 | 90 | struct Rpc { 91 | FuriMutex* busy_mutex; 92 | }; 93 | 94 | RpcOwner rpc_session_get_owner(RpcSession* session) { 95 | furi_check(session); 96 | return session->owner; 97 | } 98 | 99 | static void rpc_close_session_process(const PB_Main* request, void* context) { 100 | furi_assert(request); 101 | furi_assert(context); 102 | 103 | RpcSession* session = (RpcSession*)context; 104 | 105 | rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); 106 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 107 | if(session->closed_callback) { 108 | session->closed_callback(session->context); 109 | } else { 110 | FURI_LOG_W(TAG, "Session stop isn't processed by transport layer"); 111 | } 112 | furi_mutex_release(session->callbacks_mutex); 113 | } 114 | 115 | void rpc_session_set_context(RpcSession* session, void* context) { 116 | furi_check(session); 117 | 118 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 119 | session->context = context; 120 | furi_mutex_release(session->callbacks_mutex); 121 | } 122 | 123 | void rpc_session_set_close_callback(RpcSession* session, RpcSessionClosedCallback callback) { 124 | furi_check(session); 125 | 126 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 127 | session->closed_callback = callback; 128 | furi_mutex_release(session->callbacks_mutex); 129 | } 130 | 131 | void rpc_session_set_send_bytes_callback(RpcSession* session, RpcSendBytesCallback callback) { 132 | furi_check(session); 133 | 134 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 135 | session->send_bytes_callback = callback; 136 | furi_mutex_release(session->callbacks_mutex); 137 | } 138 | 139 | void rpc_session_set_buffer_is_empty_callback( 140 | RpcSession* session, 141 | RpcBufferIsEmptyCallback callback) { 142 | furi_check(session); 143 | 144 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 145 | session->buffer_is_empty_callback = callback; 146 | furi_mutex_release(session->callbacks_mutex); 147 | } 148 | 149 | void rpc_session_set_terminated_callback( 150 | RpcSession* session, 151 | RpcSessionTerminatedCallback callback) { 152 | furi_check(session); 153 | 154 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 155 | session->terminated_callback = callback; 156 | furi_mutex_release(session->callbacks_mutex); 157 | } 158 | 159 | /* Doesn't forbid using rpc_feed_bytes() after session close - it's safe. 160 | * Because any bytes received in buffer will be flushed before next session. 161 | * If bytes get into stream buffer before it's get emptied and this 162 | * command is gets processed - it's safe either way. But case of it is quite 163 | * odd: client sends close request and sends command after. 164 | */ 165 | size_t rpc_session_feed( 166 | RpcSession* session, 167 | const uint8_t* encoded_bytes, 168 | size_t size, 169 | uint32_t timeout) { 170 | furi_check(session); 171 | furi_check(encoded_bytes); 172 | 173 | if(!size) return 0; 174 | 175 | size_t bytes_sent = furi_stream_buffer_send(session->stream, encoded_bytes, size, timeout); 176 | 177 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtNewData); 178 | 179 | return bytes_sent; 180 | } 181 | 182 | size_t rpc_session_get_available_size(RpcSession* session) { 183 | furi_check(session); 184 | return furi_stream_buffer_spaces_available(session->stream); 185 | } 186 | 187 | bool rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) { 188 | furi_assert(istream); 189 | furi_assert(buf); 190 | RpcSession* session = istream->state; 191 | furi_assert(session); 192 | furi_assert(istream->bytes_left); 193 | 194 | if(session->terminate) { 195 | return false; 196 | } 197 | 198 | uint32_t flags = 0; 199 | size_t bytes_received = 0; 200 | 201 | while(1) { 202 | bytes_received += furi_stream_buffer_receive( 203 | session->stream, buf + bytes_received, count - bytes_received, 0); 204 | if(furi_stream_buffer_is_empty(session->stream)) { 205 | if(session->buffer_is_empty_callback) { 206 | session->buffer_is_empty_callback(session->context); 207 | } 208 | } 209 | if(session->decode_error) { 210 | /* never go out till RPC_EVENT_DISCONNECT come */ 211 | bytes_received = 0; 212 | } 213 | if(count == bytes_received) { 214 | break; 215 | } else { 216 | flags = furi_thread_flags_wait(RPC_ALL_EVENTS, FuriFlagWaitAny, FuriWaitForever); 217 | if(flags & RpcEvtDisconnect) { 218 | if(furi_stream_buffer_is_empty(session->stream)) { 219 | session->terminate = true; 220 | istream->bytes_left = 0; 221 | bytes_received = 0; 222 | break; 223 | } else { 224 | /* Save disconnect flag and continue reading buffer */ 225 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); 226 | } 227 | } else if(flags & RpcEvtNewData) { 228 | // Just wake thread up 229 | } 230 | } 231 | } 232 | 233 | #ifdef SRV_RPC_DEBUG 234 | rpc_debug_print_data("INPUT", buf, bytes_received); 235 | #endif 236 | 237 | return count == bytes_received; 238 | } 239 | 240 | static bool rpc_pb_content_callback(pb_istream_t* stream, const pb_field_t* field, void** arg) { 241 | furi_assert(stream); 242 | RpcSession* session = stream->state; 243 | furi_assert(session); 244 | furi_assert(field); 245 | 246 | RpcHandler* handler = RpcHandlerDict_get(session->handlers, field->tag); 247 | 248 | if(handler && handler->decode_submessage) { 249 | handler->decode_submessage(stream, field, arg); 250 | } 251 | 252 | return true; 253 | } 254 | 255 | static int32_t rpc_session_worker(void* context) { 256 | furi_assert(context); 257 | RpcSession* session = (RpcSession*)context; 258 | Rpc* rpc = session->rpc; 259 | 260 | FURI_LOG_D(TAG, "Session started"); 261 | 262 | while(1) { 263 | pb_istream_t istream = { 264 | .callback = rpc_pb_stream_read, 265 | .state = session, 266 | .errmsg = NULL, 267 | .bytes_left = SIZE_MAX, 268 | }; 269 | 270 | bool message_decode_failed = false; 271 | 272 | if(pb_decode_ex(&istream, &PB_Main_msg, session->decoded_message, PB_DECODE_DELIMITED)) { 273 | #ifdef SRV_RPC_DEBUG 274 | FURI_LOG_I(TAG, "INPUT:"); 275 | rpc_debug_print_message(session->decoded_message); 276 | #endif 277 | RpcHandler* handler = 278 | RpcHandlerDict_get(session->handlers, session->decoded_message->which_content); 279 | 280 | if(handler && handler->message_handler) { 281 | furi_check(furi_mutex_acquire(rpc->busy_mutex, FuriWaitForever) == FuriStatusOk); 282 | handler->message_handler(session->decoded_message, handler->context); 283 | furi_check(furi_mutex_release(rpc->busy_mutex) == FuriStatusOk); 284 | } else if(session->decoded_message->which_content == 0) { 285 | /* Receiving zeroes means message is 0-length, which 286 | * is valid for proto3: all fields are filled with default values. 287 | * 0 - is default value for which_content field. 288 | * Mark it as decode error, because there is no content message 289 | * in Main message with tag 0. 290 | */ 291 | message_decode_failed = true; 292 | } else if(!handler && !session->terminate) { 293 | FURI_LOG_E( 294 | TAG, 295 | "Message(%d) decoded, but not implemented", 296 | session->decoded_message->which_content); 297 | rpc_send_and_release_empty( 298 | session, 299 | session->decoded_message->command_id, 300 | PB_CommandStatus_ERROR_NOT_IMPLEMENTED); 301 | } 302 | } else { 303 | message_decode_failed = true; 304 | } 305 | 306 | if(message_decode_failed) { 307 | furi_stream_buffer_reset(session->stream); 308 | if(!session->terminate) { 309 | /* Protobuf can't determine start and end of message. 310 | * Handle this by adding varint at beginning 311 | * of a message (PB_ENCODE_DELIMITED). But decoding fail 312 | * means we can't be sure next bytes are varint for next 313 | * message, so the only way to close session. 314 | * RPC itself can't make decision to close session. It has 315 | * to notify: 316 | * 1) down layer (transport) 317 | * 2) other side (companion app) 318 | * Who are responsible to handle RPC session lifecycle. 319 | * Companion receives 2 messages: ERROR_DECODE and session_closed. 320 | */ 321 | FURI_LOG_E(TAG, "Decode failed, error: \'%.128s\'", PB_GET_ERROR(&istream)); 322 | session->decode_error = true; 323 | rpc_send_and_release_empty(session, 0, PB_CommandStatus_ERROR_DECODE); 324 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 325 | if(session->closed_callback) { 326 | session->closed_callback(session->context); 327 | } 328 | furi_mutex_release(session->callbacks_mutex); 329 | 330 | if(session->owner == RpcOwnerBle) { 331 | // Disconnect BLE session 332 | FURI_LOG_E("RPC", "BLE session closed due to a decode error"); 333 | Bt* bt = furi_record_open(RECORD_BT); 334 | bt_profile_restore_default(bt); 335 | furi_record_close(RECORD_BT); 336 | FURI_LOG_E("RPC", "Finished disconnecting the BLE session"); 337 | } 338 | } 339 | } 340 | 341 | pb_release(&PB_Main_msg, session->decoded_message); 342 | 343 | if(session->terminate) { 344 | FURI_LOG_D(TAG, "Session terminated"); 345 | break; 346 | } 347 | } 348 | 349 | return 0; 350 | } 351 | 352 | static void rpc_session_thread_pending_callback(void* context, uint32_t arg) { 353 | UNUSED(arg); 354 | RpcSession* session = (RpcSession*)context; 355 | 356 | for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { 357 | if(rpc_systems[i].free) { 358 | (rpc_systems[i].free)(session->system_contexts[i]); 359 | } 360 | } 361 | free(session->system_contexts); 362 | free(session->decoded_message); 363 | RpcHandlerDict_clear(session->handlers); 364 | furi_stream_buffer_free(session->stream); 365 | 366 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 367 | if(session->terminated_callback) { 368 | session->terminated_callback(session->context); 369 | } 370 | furi_mutex_release(session->callbacks_mutex); 371 | 372 | furi_mutex_free(session->callbacks_mutex); 373 | furi_thread_join(session->thread); 374 | furi_thread_free(session->thread); 375 | free(session); 376 | } 377 | 378 | static void 379 | rpc_session_thread_state_callback(FuriThread* thread, FuriThreadState state, void* context) { 380 | UNUSED(thread); 381 | if(state == FuriThreadStateStopped) { 382 | furi_timer_pending_callback(rpc_session_thread_pending_callback, context, 0); 383 | } 384 | } 385 | 386 | RpcSession* rpc_session_open(Rpc* rpc, RpcOwner owner) { 387 | furi_check(rpc); 388 | 389 | RpcSession* session = malloc(sizeof(RpcSession)); 390 | session->callbacks_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 391 | session->stream = furi_stream_buffer_alloc(RPC_BUFFER_SIZE, 1); 392 | session->rpc = rpc; 393 | session->terminate = false; 394 | session->decode_error = false; 395 | session->owner = owner; 396 | RpcHandlerDict_init(session->handlers); 397 | 398 | session->decoded_message = malloc(sizeof(PB_Main)); 399 | session->decoded_message->cb_content.funcs.decode = rpc_pb_content_callback; 400 | session->decoded_message->cb_content.arg = session; 401 | 402 | session->system_contexts = malloc(COUNT_OF(rpc_systems) * sizeof(void*)); 403 | for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { 404 | session->system_contexts[i] = rpc_systems[i].alloc(session); 405 | } 406 | 407 | RpcHandler rpc_handler = { 408 | .message_handler = rpc_close_session_process, 409 | .decode_submessage = NULL, 410 | .context = session, 411 | }; 412 | rpc_add_handler(session, PB_Main_stop_session_tag, &rpc_handler); 413 | 414 | session->thread = furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session); 415 | 416 | furi_thread_set_state_context(session->thread, session); 417 | furi_thread_set_state_callback(session->thread, rpc_session_thread_state_callback); 418 | 419 | furi_thread_start(session->thread); 420 | 421 | return session; 422 | } 423 | 424 | void rpc_session_close(RpcSession* session) { 425 | furi_check(session); 426 | furi_check(session->rpc); 427 | 428 | rpc_session_set_send_bytes_callback(session, NULL); 429 | rpc_session_set_close_callback(session, NULL); 430 | rpc_session_set_buffer_is_empty_callback(session, NULL); 431 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); 432 | } 433 | 434 | void rpc_on_system_start(void* p) { 435 | UNUSED(p); 436 | Rpc* rpc = malloc(sizeof(Rpc)); 437 | 438 | rpc->busy_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 439 | 440 | Cli* cli = furi_record_open(RECORD_CLI); 441 | cli_add_command( 442 | cli, "start_rpc_session", CliCommandFlagParallelSafe, rpc_cli_command_start_session, rpc); 443 | 444 | furi_record_create(RECORD_RPC, rpc); 445 | rpc_keyboard_register(); 446 | } 447 | 448 | void rpc_add_handler(RpcSession* session, pb_size_t message_tag, RpcHandler* handler) { 449 | furi_assert(RpcHandlerDict_get(session->handlers, message_tag) == NULL); 450 | 451 | RpcHandlerDict_set_at(session->handlers, message_tag, *handler); 452 | } 453 | 454 | void rpc_send(RpcSession* session, PB_Main* message) { 455 | furi_assert(session); 456 | furi_assert(message); 457 | 458 | pb_ostream_t ostream = PB_OSTREAM_SIZING; 459 | 460 | #ifdef SRV_RPC_DEBUG 461 | FURI_LOG_I(TAG, "OUTPUT:"); 462 | rpc_debug_print_message(message); 463 | #endif 464 | 465 | bool result = pb_encode_ex(&ostream, &PB_Main_msg, message, PB_ENCODE_DELIMITED); 466 | furi_check(result && ostream.bytes_written); 467 | 468 | uint8_t* buffer = malloc(ostream.bytes_written); 469 | ostream = pb_ostream_from_buffer(buffer, ostream.bytes_written); 470 | 471 | pb_encode_ex(&ostream, &PB_Main_msg, message, PB_ENCODE_DELIMITED); 472 | 473 | #ifdef SRV_RPC_DEBUG 474 | rpc_debug_print_data("OUTPUT", buffer, ostream.bytes_written); 475 | #endif 476 | 477 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 478 | if(session->send_bytes_callback) { 479 | session->send_bytes_callback(session->context, buffer, ostream.bytes_written); 480 | } 481 | furi_mutex_release(session->callbacks_mutex); 482 | 483 | free(buffer); 484 | } 485 | 486 | void rpc_send_and_release(RpcSession* session, PB_Main* message) { 487 | rpc_send(session, message); 488 | pb_release(&PB_Main_msg, message); 489 | } 490 | 491 | void rpc_send_and_release_empty(RpcSession* session, uint32_t command_id, PB_CommandStatus status) { 492 | furi_assert(session); 493 | 494 | PB_Main message = { 495 | .command_id = command_id, 496 | .command_status = status, 497 | .has_next = false, 498 | .which_content = PB_Main_empty_tag, 499 | }; 500 | 501 | rpc_send_and_release(session, &message); 502 | pb_release(&PB_Main_msg, &message); 503 | } 504 | -------------------------------------------------------------------------------- /firmware-overlay/rm-1202-0837-0.420.0-6d10bad/applications/services/rpc/rpc.c: -------------------------------------------------------------------------------- 1 | #include "rpc_i.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | 20 | #include 21 | 22 | #define TAG "RpcSrv" 23 | 24 | typedef enum { 25 | RpcEvtNewData = (1 << 0), 26 | RpcEvtDisconnect = (1 << 1), 27 | } RpcEvtFlags; 28 | 29 | #define RPC_ALL_EVENTS (RpcEvtNewData | RpcEvtDisconnect) 30 | 31 | DICT_DEF2(RpcHandlerDict, pb_size_t, M_DEFAULT_OPLIST, RpcHandler, M_POD_OPLIST) 32 | 33 | typedef struct { 34 | RpcSystemAlloc alloc; 35 | RpcSystemFree free; 36 | void* context; 37 | } RpcSystemCallbacks; 38 | 39 | static RpcSystemCallbacks rpc_systems[] = { 40 | { 41 | .alloc = rpc_system_system_alloc, 42 | .free = NULL, 43 | }, 44 | { 45 | .alloc = rpc_system_storage_alloc, 46 | .free = rpc_system_storage_free, 47 | }, 48 | { 49 | .alloc = rpc_system_app_alloc, 50 | .free = rpc_system_app_free, 51 | }, 52 | { 53 | .alloc = rpc_system_gui_alloc, 54 | .free = rpc_system_gui_free, 55 | }, 56 | { 57 | .alloc = rpc_system_gpio_alloc, 58 | .free = NULL, 59 | }, 60 | { 61 | .alloc = rpc_system_property_alloc, 62 | .free = NULL, 63 | }, 64 | { 65 | .alloc = rpc_desktop_alloc, 66 | .free = rpc_desktop_free, 67 | }, 68 | }; 69 | 70 | struct RpcSession { 71 | Rpc* rpc; 72 | 73 | FuriThread* thread; 74 | 75 | RpcHandlerDict_t handlers; 76 | FuriStreamBuffer* stream; 77 | PB_Main* decoded_message; 78 | bool terminate; 79 | void** system_contexts; 80 | bool decode_error; 81 | 82 | FuriMutex* callbacks_mutex; 83 | RpcSendBytesCallback send_bytes_callback; 84 | RpcBufferIsEmptyCallback buffer_is_empty_callback; 85 | RpcSessionClosedCallback closed_callback; 86 | RpcSessionTerminatedCallback terminated_callback; 87 | RpcOwner owner; 88 | void* context; 89 | }; 90 | 91 | struct Rpc { 92 | FuriMutex* busy_mutex; 93 | size_t sessions_count; 94 | }; 95 | 96 | RpcOwner rpc_session_get_owner(RpcSession* session) { 97 | furi_check(session); 98 | return session->owner; 99 | } 100 | 101 | static void rpc_close_session_process(const PB_Main* request, void* context) { 102 | furi_assert(request); 103 | furi_assert(context); 104 | 105 | RpcSession* session = (RpcSession*)context; 106 | 107 | rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); 108 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 109 | if(session->closed_callback) { 110 | session->closed_callback(session->context); 111 | } else { 112 | FURI_LOG_W(TAG, "Session stop isn't processed by transport layer"); 113 | } 114 | furi_mutex_release(session->callbacks_mutex); 115 | } 116 | 117 | void rpc_session_set_context(RpcSession* session, void* context) { 118 | furi_check(session); 119 | 120 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 121 | session->context = context; 122 | furi_mutex_release(session->callbacks_mutex); 123 | } 124 | 125 | void rpc_session_set_close_callback(RpcSession* session, RpcSessionClosedCallback callback) { 126 | furi_check(session); 127 | 128 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 129 | session->closed_callback = callback; 130 | furi_mutex_release(session->callbacks_mutex); 131 | } 132 | 133 | void rpc_session_set_send_bytes_callback(RpcSession* session, RpcSendBytesCallback callback) { 134 | furi_check(session); 135 | 136 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 137 | session->send_bytes_callback = callback; 138 | furi_mutex_release(session->callbacks_mutex); 139 | } 140 | 141 | void rpc_session_set_buffer_is_empty_callback( 142 | RpcSession* session, 143 | RpcBufferIsEmptyCallback callback) { 144 | furi_check(session); 145 | 146 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 147 | session->buffer_is_empty_callback = callback; 148 | furi_mutex_release(session->callbacks_mutex); 149 | } 150 | 151 | void rpc_session_set_terminated_callback( 152 | RpcSession* session, 153 | RpcSessionTerminatedCallback callback) { 154 | furi_check(session); 155 | 156 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 157 | session->terminated_callback = callback; 158 | furi_mutex_release(session->callbacks_mutex); 159 | } 160 | 161 | /* Doesn't forbid using rpc_feed_bytes() after session close - it's safe. 162 | * Because any bytes received in buffer will be flushed before next session. 163 | * If bytes get into stream buffer before it's get emptied and this 164 | * command is gets processed - it's safe either way. But case of it is quite 165 | * odd: client sends close request and sends command after. 166 | */ 167 | size_t rpc_session_feed( 168 | RpcSession* session, 169 | const uint8_t* encoded_bytes, 170 | size_t size, 171 | uint32_t timeout) { 172 | furi_check(session); 173 | furi_check(encoded_bytes); 174 | 175 | if(!size) return 0; 176 | 177 | size_t bytes_sent = furi_stream_buffer_send(session->stream, encoded_bytes, size, timeout); 178 | 179 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtNewData); 180 | 181 | return bytes_sent; 182 | } 183 | 184 | size_t rpc_session_get_available_size(RpcSession* session) { 185 | furi_check(session); 186 | return furi_stream_buffer_spaces_available(session->stream); 187 | } 188 | 189 | bool rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) { 190 | furi_assert(istream); 191 | furi_assert(buf); 192 | RpcSession* session = istream->state; 193 | furi_assert(session); 194 | furi_assert(istream->bytes_left); 195 | 196 | if(session->terminate) { 197 | return false; 198 | } 199 | 200 | uint32_t flags = 0; 201 | size_t bytes_received = 0; 202 | 203 | while(1) { 204 | bytes_received += furi_stream_buffer_receive( 205 | session->stream, buf + bytes_received, count - bytes_received, 0); 206 | if(furi_stream_buffer_is_empty(session->stream)) { 207 | if(session->buffer_is_empty_callback) { 208 | session->buffer_is_empty_callback(session->context); 209 | } 210 | } 211 | if(session->decode_error) { 212 | /* never go out till RPC_EVENT_DISCONNECT come */ 213 | bytes_received = 0; 214 | } 215 | if(count == bytes_received) { 216 | break; 217 | } else { 218 | flags = furi_thread_flags_wait(RPC_ALL_EVENTS, FuriFlagWaitAny, FuriWaitForever); 219 | if(flags & RpcEvtDisconnect) { 220 | if(furi_stream_buffer_is_empty(session->stream)) { 221 | session->terminate = true; 222 | istream->bytes_left = 0; 223 | bytes_received = 0; 224 | break; 225 | } else { 226 | /* Save disconnect flag and continue reading buffer */ 227 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); 228 | } 229 | } else if(flags & RpcEvtNewData) { 230 | // Just wake thread up 231 | } 232 | } 233 | } 234 | 235 | #ifdef SRV_RPC_DEBUG 236 | rpc_debug_print_data("INPUT", buf, bytes_received); 237 | #endif 238 | 239 | return count == bytes_received; 240 | } 241 | 242 | static bool rpc_pb_content_callback(pb_istream_t* stream, const pb_field_t* field, void** arg) { 243 | furi_assert(stream); 244 | RpcSession* session = stream->state; 245 | furi_assert(session); 246 | furi_assert(field); 247 | 248 | RpcHandler* handler = RpcHandlerDict_get(session->handlers, field->tag); 249 | 250 | if(handler && handler->decode_submessage) { 251 | handler->decode_submessage(stream, field, arg); 252 | } 253 | 254 | return true; 255 | } 256 | 257 | static int32_t rpc_session_worker(void* context) { 258 | furi_assert(context); 259 | RpcSession* session = (RpcSession*)context; 260 | Rpc* rpc = session->rpc; 261 | 262 | FURI_LOG_D(TAG, "Session started"); 263 | 264 | while(1) { 265 | pb_istream_t istream = { 266 | .callback = rpc_pb_stream_read, 267 | .state = session, 268 | .errmsg = NULL, 269 | .bytes_left = SIZE_MAX, 270 | }; 271 | 272 | bool message_decode_failed = false; 273 | 274 | if(pb_decode_ex(&istream, &PB_Main_msg, session->decoded_message, PB_DECODE_DELIMITED)) { 275 | #ifdef SRV_RPC_DEBUG 276 | FURI_LOG_I(TAG, "INPUT:"); 277 | rpc_debug_print_message(session->decoded_message); 278 | #endif 279 | RpcHandler* handler = 280 | RpcHandlerDict_get(session->handlers, session->decoded_message->which_content); 281 | 282 | if(handler && handler->message_handler) { 283 | furi_check(furi_mutex_acquire(rpc->busy_mutex, FuriWaitForever) == FuriStatusOk); 284 | handler->message_handler(session->decoded_message, handler->context); 285 | furi_check(furi_mutex_release(rpc->busy_mutex) == FuriStatusOk); 286 | } else if(session->decoded_message->which_content == 0) { 287 | /* Receiving zeroes means message is 0-length, which 288 | * is valid for proto3: all fields are filled with default values. 289 | * 0 - is default value for which_content field. 290 | * Mark it as decode error, because there is no content message 291 | * in Main message with tag 0. 292 | */ 293 | message_decode_failed = true; 294 | } else if(!handler && !session->terminate) { 295 | FURI_LOG_E( 296 | TAG, 297 | "Message(%d) decoded, but not implemented", 298 | session->decoded_message->which_content); 299 | rpc_send_and_release_empty( 300 | session, 301 | session->decoded_message->command_id, 302 | PB_CommandStatus_ERROR_NOT_IMPLEMENTED); 303 | } 304 | } else { 305 | message_decode_failed = true; 306 | } 307 | 308 | if(message_decode_failed) { 309 | furi_stream_buffer_reset(session->stream); 310 | if(!session->terminate) { 311 | /* Protobuf can't determine start and end of message. 312 | * Handle this by adding varint at beginning 313 | * of a message (PB_ENCODE_DELIMITED). But decoding fail 314 | * means we can't be sure next bytes are varint for next 315 | * message, so the only way to close session. 316 | * RPC itself can't make decision to close session. It has 317 | * to notify: 318 | * 1) down layer (transport) 319 | * 2) other side (companion app) 320 | * Who are responsible to handle RPC session lifecycle. 321 | * Companion receives 2 messages: ERROR_DECODE and session_closed. 322 | */ 323 | FURI_LOG_E(TAG, "Decode failed, error: \'%.128s\'", PB_GET_ERROR(&istream)); 324 | session->decode_error = true; 325 | rpc_send_and_release_empty(session, 0, PB_CommandStatus_ERROR_DECODE); 326 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 327 | if(session->closed_callback) { 328 | session->closed_callback(session->context); 329 | } 330 | furi_mutex_release(session->callbacks_mutex); 331 | 332 | if(session->owner == RpcOwnerBle) { 333 | // Disconnect BLE session 334 | FURI_LOG_E("RPC", "BLE session closed due to a decode error"); 335 | Bt* bt = furi_record_open(RECORD_BT); 336 | bt_profile_restore_default(bt); 337 | furi_record_close(RECORD_BT); 338 | FURI_LOG_E("RPC", "Finished disconnecting the BLE session"); 339 | } 340 | } 341 | } 342 | 343 | pb_release(&PB_Main_msg, session->decoded_message); 344 | 345 | if(session->terminate) { 346 | FURI_LOG_D(TAG, "Session terminated"); 347 | break; 348 | } 349 | } 350 | 351 | return 0; 352 | } 353 | 354 | static void rpc_session_thread_pending_callback(void* context, uint32_t arg) { 355 | UNUSED(arg); 356 | RpcSession* session = (RpcSession*)context; 357 | 358 | for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { 359 | if(rpc_systems[i].free) { 360 | (rpc_systems[i].free)(session->system_contexts[i]); 361 | } 362 | } 363 | free(session->system_contexts); 364 | free(session->decoded_message); 365 | RpcHandlerDict_clear(session->handlers); 366 | furi_stream_buffer_free(session->stream); 367 | 368 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 369 | if(session->terminated_callback) { 370 | session->terminated_callback(session->context); 371 | } 372 | furi_mutex_release(session->callbacks_mutex); 373 | 374 | furi_mutex_free(session->callbacks_mutex); 375 | furi_thread_join(session->thread); 376 | furi_thread_free(session->thread); 377 | free(session); 378 | } 379 | 380 | static void rpc_session_thread_state_callback(FuriThreadState thread_state, void* context) { 381 | if(thread_state == FuriThreadStateStopped) { 382 | furi_timer_pending_callback(rpc_session_thread_pending_callback, context, 0); 383 | } 384 | } 385 | 386 | RpcSession* rpc_session_open(Rpc* rpc, RpcOwner owner) { 387 | furi_check(rpc); 388 | 389 | RpcSession* session = malloc(sizeof(RpcSession)); 390 | session->callbacks_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 391 | session->stream = furi_stream_buffer_alloc(RPC_BUFFER_SIZE, 1); 392 | session->rpc = rpc; 393 | session->terminate = false; 394 | session->decode_error = false; 395 | session->owner = owner; 396 | RpcHandlerDict_init(session->handlers); 397 | 398 | session->decoded_message = malloc(sizeof(PB_Main)); 399 | session->decoded_message->cb_content.funcs.decode = rpc_pb_content_callback; 400 | session->decoded_message->cb_content.arg = session; 401 | 402 | session->system_contexts = malloc(COUNT_OF(rpc_systems) * sizeof(void*)); 403 | for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { 404 | session->system_contexts[i] = rpc_systems[i].alloc(session); 405 | } 406 | 407 | RpcHandler rpc_handler = { 408 | .message_handler = rpc_close_session_process, 409 | .decode_submessage = NULL, 410 | .context = session, 411 | }; 412 | rpc_add_handler(session, PB_Main_stop_session_tag, &rpc_handler); 413 | 414 | session->thread = furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session); 415 | 416 | furi_thread_set_state_context(session->thread, session); 417 | furi_thread_set_state_callback(session->thread, rpc_session_thread_state_callback); 418 | 419 | furi_thread_start(session->thread); 420 | 421 | rpc->sessions_count++; 422 | 423 | return session; 424 | } 425 | 426 | void rpc_session_close(RpcSession* session) { 427 | furi_check(session); 428 | furi_check(session->rpc); 429 | 430 | session->rpc->sessions_count--; 431 | 432 | rpc_session_set_send_bytes_callback(session, NULL); 433 | rpc_session_set_close_callback(session, NULL); 434 | rpc_session_set_buffer_is_empty_callback(session, NULL); 435 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); 436 | } 437 | 438 | void rpc_on_system_start(void* p) { 439 | UNUSED(p); 440 | Rpc* rpc = malloc(sizeof(Rpc)); 441 | 442 | rpc->busy_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 443 | 444 | Cli* cli = furi_record_open(RECORD_CLI); 445 | cli_add_command( 446 | cli, "start_rpc_session", CliCommandFlagParallelSafe, rpc_cli_command_start_session, rpc); 447 | 448 | furi_record_create(RECORD_RPC, rpc); 449 | rpc_keyboard_register(); 450 | } 451 | 452 | void rpc_add_handler(RpcSession* session, pb_size_t message_tag, RpcHandler* handler) { 453 | furi_assert(RpcHandlerDict_get(session->handlers, message_tag) == NULL); 454 | 455 | RpcHandlerDict_set_at(session->handlers, message_tag, *handler); 456 | } 457 | 458 | void rpc_send(RpcSession* session, PB_Main* message) { 459 | furi_assert(session); 460 | furi_assert(message); 461 | 462 | pb_ostream_t ostream = PB_OSTREAM_SIZING; 463 | 464 | #ifdef SRV_RPC_DEBUG 465 | FURI_LOG_I(TAG, "OUTPUT:"); 466 | rpc_debug_print_message(message); 467 | #endif 468 | 469 | bool result = pb_encode_ex(&ostream, &PB_Main_msg, message, PB_ENCODE_DELIMITED); 470 | furi_check(result && ostream.bytes_written); 471 | 472 | uint8_t* buffer = malloc(ostream.bytes_written); 473 | ostream = pb_ostream_from_buffer(buffer, ostream.bytes_written); 474 | 475 | pb_encode_ex(&ostream, &PB_Main_msg, message, PB_ENCODE_DELIMITED); 476 | 477 | #ifdef SRV_RPC_DEBUG 478 | rpc_debug_print_data("OUTPUT", buffer, ostream.bytes_written); 479 | #endif 480 | 481 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 482 | if(session->send_bytes_callback) { 483 | session->send_bytes_callback(session->context, buffer, ostream.bytes_written); 484 | } 485 | furi_mutex_release(session->callbacks_mutex); 486 | 487 | free(buffer); 488 | } 489 | 490 | void rpc_send_and_release(RpcSession* session, PB_Main* message) { 491 | rpc_send(session, message); 492 | pb_release(&PB_Main_msg, message); 493 | } 494 | 495 | void rpc_send_and_release_empty(RpcSession* session, uint32_t command_id, PB_CommandStatus status) { 496 | furi_assert(session); 497 | 498 | PB_Main message = { 499 | .command_id = command_id, 500 | .command_status = status, 501 | .has_next = false, 502 | .which_content = PB_Main_empty_tag, 503 | }; 504 | 505 | rpc_send_and_release(session, &message); 506 | pb_release(&PB_Main_msg, &message); 507 | } 508 | 509 | size_t rpc_get_sessions_count(Rpc* rpc) { 510 | return rpc->sessions_count; 511 | } 512 | -------------------------------------------------------------------------------- /firmware-overlay/mntm-008/applications/services/rpc/rpc.c: -------------------------------------------------------------------------------- 1 | #include "rpc_i.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | #include 22 | 23 | #define TAG "RpcSrv" 24 | 25 | typedef enum { 26 | RpcEvtNewData = (1 << 0), 27 | RpcEvtDisconnect = (1 << 1), 28 | } RpcEvtFlags; 29 | 30 | #define RPC_ALL_EVENTS (RpcEvtNewData | RpcEvtDisconnect) 31 | 32 | DICT_DEF2(RpcHandlerDict, pb_size_t, M_DEFAULT_OPLIST, RpcHandler, M_POD_OPLIST) 33 | 34 | typedef struct { 35 | RpcSystemAlloc alloc; 36 | RpcSystemFree free; 37 | void* context; 38 | } RpcSystemCallbacks; 39 | 40 | static RpcSystemCallbacks rpc_systems[] = { 41 | { 42 | .alloc = rpc_system_system_alloc, 43 | .free = NULL, 44 | }, 45 | { 46 | .alloc = rpc_system_storage_alloc, 47 | .free = rpc_system_storage_free, 48 | }, 49 | { 50 | .alloc = rpc_system_app_alloc, 51 | .free = rpc_system_app_free, 52 | }, 53 | { 54 | .alloc = rpc_system_gui_alloc, 55 | .free = rpc_system_gui_free, 56 | }, 57 | { 58 | .alloc = rpc_system_gpio_alloc, 59 | .free = NULL, 60 | }, 61 | { 62 | .alloc = rpc_system_property_alloc, 63 | .free = NULL, 64 | }, 65 | { 66 | .alloc = rpc_desktop_alloc, 67 | .free = rpc_desktop_free, 68 | }, 69 | }; 70 | 71 | struct RpcSession { 72 | Rpc* rpc; 73 | 74 | FuriThread* thread; 75 | 76 | RpcHandlerDict_t handlers; 77 | FuriStreamBuffer* stream; 78 | PB_Main* decoded_message; 79 | bool terminate; 80 | void** system_contexts; 81 | bool decode_error; 82 | 83 | FuriMutex* callbacks_mutex; 84 | RpcSendBytesCallback send_bytes_callback; 85 | RpcBufferIsEmptyCallback buffer_is_empty_callback; 86 | RpcSessionClosedCallback closed_callback; 87 | RpcSessionTerminatedCallback terminated_callback; 88 | RpcOwner owner; 89 | void* context; 90 | }; 91 | 92 | struct Rpc { 93 | FuriMutex* busy_mutex; 94 | size_t sessions_count; 95 | }; 96 | 97 | RpcOwner rpc_session_get_owner(RpcSession* session) { 98 | furi_check(session); 99 | return session->owner; 100 | } 101 | 102 | static void rpc_close_session_process(const PB_Main* request, void* context) { 103 | furi_assert(request); 104 | furi_assert(context); 105 | 106 | RpcSession* session = (RpcSession*)context; 107 | 108 | rpc_send_and_release_empty(session, request->command_id, PB_CommandStatus_OK); 109 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 110 | if(session->closed_callback) { 111 | session->closed_callback(session->context); 112 | } else { 113 | FURI_LOG_W(TAG, "Session stop isn't processed by transport layer"); 114 | } 115 | furi_mutex_release(session->callbacks_mutex); 116 | } 117 | 118 | void rpc_session_set_context(RpcSession* session, void* context) { 119 | furi_check(session); 120 | 121 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 122 | session->context = context; 123 | furi_mutex_release(session->callbacks_mutex); 124 | } 125 | 126 | void rpc_session_set_close_callback(RpcSession* session, RpcSessionClosedCallback callback) { 127 | furi_check(session); 128 | 129 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 130 | session->closed_callback = callback; 131 | furi_mutex_release(session->callbacks_mutex); 132 | } 133 | 134 | void rpc_session_set_send_bytes_callback(RpcSession* session, RpcSendBytesCallback callback) { 135 | furi_check(session); 136 | 137 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 138 | session->send_bytes_callback = callback; 139 | furi_mutex_release(session->callbacks_mutex); 140 | } 141 | 142 | void rpc_session_set_buffer_is_empty_callback( 143 | RpcSession* session, 144 | RpcBufferIsEmptyCallback callback) { 145 | furi_check(session); 146 | 147 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 148 | session->buffer_is_empty_callback = callback; 149 | furi_mutex_release(session->callbacks_mutex); 150 | } 151 | 152 | void rpc_session_set_terminated_callback( 153 | RpcSession* session, 154 | RpcSessionTerminatedCallback callback) { 155 | furi_check(session); 156 | 157 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 158 | session->terminated_callback = callback; 159 | furi_mutex_release(session->callbacks_mutex); 160 | } 161 | 162 | /* Doesn't forbid using rpc_feed_bytes() after session close - it's safe. 163 | * Because any bytes received in buffer will be flushed before next session. 164 | * If bytes get into stream buffer before it's get emptied and this 165 | * command is gets processed - it's safe either way. But case of it is quite 166 | * odd: client sends close request and sends command after. 167 | */ 168 | size_t rpc_session_feed( 169 | RpcSession* session, 170 | const uint8_t* encoded_bytes, 171 | size_t size, 172 | uint32_t timeout) { 173 | furi_check(session); 174 | furi_check(encoded_bytes); 175 | 176 | if(!size) return 0; 177 | 178 | size_t bytes_sent = furi_stream_buffer_send(session->stream, encoded_bytes, size, timeout); 179 | 180 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtNewData); 181 | 182 | return bytes_sent; 183 | } 184 | 185 | size_t rpc_session_get_available_size(RpcSession* session) { 186 | furi_check(session); 187 | return furi_stream_buffer_spaces_available(session->stream); 188 | } 189 | 190 | bool rpc_pb_stream_read(pb_istream_t* istream, pb_byte_t* buf, size_t count) { 191 | furi_assert(istream); 192 | furi_assert(buf); 193 | RpcSession* session = istream->state; 194 | furi_assert(session); 195 | furi_assert(istream->bytes_left); 196 | 197 | if(session->terminate) { 198 | return false; 199 | } 200 | 201 | uint32_t flags = 0; 202 | size_t bytes_received = 0; 203 | 204 | while(1) { 205 | bytes_received += furi_stream_buffer_receive( 206 | session->stream, buf + bytes_received, count - bytes_received, 0); 207 | if(furi_stream_buffer_is_empty(session->stream)) { 208 | if(session->buffer_is_empty_callback) { 209 | session->buffer_is_empty_callback(session->context); 210 | } 211 | } 212 | if(session->decode_error) { 213 | /* never go out till RPC_EVENT_DISCONNECT come */ 214 | bytes_received = 0; 215 | } 216 | if(count == bytes_received) { 217 | break; 218 | } else { 219 | flags = furi_thread_flags_wait(RPC_ALL_EVENTS, FuriFlagWaitAny, FuriWaitForever); 220 | if(flags & RpcEvtDisconnect) { 221 | if(furi_stream_buffer_is_empty(session->stream)) { 222 | session->terminate = true; 223 | istream->bytes_left = 0; 224 | bytes_received = 0; 225 | break; 226 | } else { 227 | /* Save disconnect flag and continue reading buffer */ 228 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); 229 | } 230 | } else if(flags & RpcEvtNewData) { 231 | // Just wake thread up 232 | } 233 | } 234 | } 235 | 236 | #ifdef SRV_RPC_DEBUG 237 | rpc_debug_print_data("INPUT", buf, bytes_received); 238 | #endif 239 | 240 | return count == bytes_received; 241 | } 242 | 243 | static bool rpc_pb_content_callback(pb_istream_t* stream, const pb_field_t* field, void** arg) { 244 | furi_assert(stream); 245 | RpcSession* session = stream->state; 246 | furi_assert(session); 247 | furi_assert(field); 248 | 249 | RpcHandler* handler = RpcHandlerDict_get(session->handlers, field->tag); 250 | 251 | if(handler && handler->decode_submessage) { 252 | handler->decode_submessage(stream, field, arg); 253 | } 254 | 255 | return true; 256 | } 257 | 258 | static int32_t rpc_session_worker(void* context) { 259 | furi_assert(context); 260 | RpcSession* session = (RpcSession*)context; 261 | Rpc* rpc = session->rpc; 262 | 263 | FURI_LOG_D(TAG, "Session started"); 264 | 265 | while(1) { 266 | pb_istream_t istream = { 267 | .callback = rpc_pb_stream_read, 268 | .state = session, 269 | .errmsg = NULL, 270 | .bytes_left = SIZE_MAX, 271 | }; 272 | 273 | bool message_decode_failed = false; 274 | 275 | if(pb_decode_ex(&istream, &PB_Main_msg, session->decoded_message, PB_DECODE_DELIMITED)) { 276 | #ifdef SRV_RPC_DEBUG 277 | FURI_LOG_I(TAG, "INPUT:"); 278 | rpc_debug_print_message(session->decoded_message); 279 | #endif 280 | RpcHandler* handler = 281 | RpcHandlerDict_get(session->handlers, session->decoded_message->which_content); 282 | 283 | if(handler && handler->message_handler) { 284 | furi_check(furi_mutex_acquire(rpc->busy_mutex, FuriWaitForever) == FuriStatusOk); 285 | handler->message_handler(session->decoded_message, handler->context); 286 | furi_check(furi_mutex_release(rpc->busy_mutex) == FuriStatusOk); 287 | } else if(session->decoded_message->which_content == 0) { 288 | /* Receiving zeroes means message is 0-length, which 289 | * is valid for proto3: all fields are filled with default values. 290 | * 0 - is default value for which_content field. 291 | * Mark it as decode error, because there is no content message 292 | * in Main message with tag 0. 293 | */ 294 | message_decode_failed = true; 295 | } else if(!handler && !session->terminate) { 296 | FURI_LOG_E( 297 | TAG, 298 | "Message(%d) decoded, but not implemented", 299 | session->decoded_message->which_content); 300 | rpc_send_and_release_empty( 301 | session, 302 | session->decoded_message->command_id, 303 | PB_CommandStatus_ERROR_NOT_IMPLEMENTED); 304 | } 305 | } else { 306 | message_decode_failed = true; 307 | } 308 | 309 | if(message_decode_failed) { 310 | furi_stream_buffer_reset(session->stream); 311 | if(!session->terminate) { 312 | /* Protobuf can't determine start and end of message. 313 | * Handle this by adding varint at beginning 314 | * of a message (PB_ENCODE_DELIMITED). But decoding fail 315 | * means we can't be sure next bytes are varint for next 316 | * message, so the only way to close session. 317 | * RPC itself can't make decision to close session. It has 318 | * to notify: 319 | * 1) down layer (transport) 320 | * 2) other side (companion app) 321 | * Who are responsible to handle RPC session lifecycle. 322 | * Companion receives 2 messages: ERROR_DECODE and session_closed. 323 | */ 324 | FURI_LOG_E(TAG, "Decode failed, error: \'%.128s\'", PB_GET_ERROR(&istream)); 325 | session->decode_error = true; 326 | rpc_send_and_release_empty(session, 0, PB_CommandStatus_ERROR_DECODE); 327 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 328 | if(session->closed_callback) { 329 | session->closed_callback(session->context); 330 | } 331 | furi_mutex_release(session->callbacks_mutex); 332 | 333 | if(session->owner == RpcOwnerBle) { 334 | // Disconnect BLE session 335 | FURI_LOG_E("RPC", "BLE session closed due to a decode error"); 336 | Bt* bt = furi_record_open(RECORD_BT); 337 | bt_profile_restore_default(bt); 338 | furi_record_close(RECORD_BT); 339 | FURI_LOG_E("RPC", "Finished disconnecting the BLE session"); 340 | } 341 | } 342 | } 343 | 344 | pb_release(&PB_Main_msg, session->decoded_message); 345 | 346 | if(session->terminate) { 347 | FURI_LOG_D(TAG, "Session terminated"); 348 | break; 349 | } 350 | } 351 | 352 | return 0; 353 | } 354 | 355 | static void rpc_session_thread_pending_callback(void* context, uint32_t arg) { 356 | UNUSED(arg); 357 | RpcSession* session = (RpcSession*)context; 358 | 359 | for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { 360 | if(rpc_systems[i].free) { 361 | (rpc_systems[i].free)(session->system_contexts[i]); 362 | } 363 | } 364 | free(session->system_contexts); 365 | free(session->decoded_message); 366 | RpcHandlerDict_clear(session->handlers); 367 | furi_stream_buffer_free(session->stream); 368 | 369 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 370 | if(session->terminated_callback) { 371 | session->terminated_callback(session->context); 372 | } 373 | furi_mutex_release(session->callbacks_mutex); 374 | 375 | furi_mutex_free(session->callbacks_mutex); 376 | furi_thread_join(session->thread); 377 | furi_thread_free(session->thread); 378 | free(session); 379 | } 380 | 381 | static void 382 | rpc_session_thread_state_callback(FuriThread* thread, FuriThreadState state, void* context) { 383 | UNUSED(thread); 384 | if(state == FuriThreadStateStopped) { 385 | furi_timer_pending_callback(rpc_session_thread_pending_callback, context, 0); 386 | } 387 | } 388 | 389 | RpcSession* rpc_session_open(Rpc* rpc, RpcOwner owner) { 390 | if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagLock) && 391 | !momentum_settings.allow_locked_rpc_commands) 392 | return NULL; 393 | 394 | furi_check(rpc); 395 | 396 | RpcSession* session = malloc(sizeof(RpcSession)); 397 | session->callbacks_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 398 | session->stream = furi_stream_buffer_alloc(RPC_BUFFER_SIZE, 1); 399 | session->rpc = rpc; 400 | session->terminate = false; 401 | session->decode_error = false; 402 | session->owner = owner; 403 | RpcHandlerDict_init(session->handlers); 404 | 405 | session->decoded_message = malloc(sizeof(PB_Main)); 406 | session->decoded_message->cb_content.funcs.decode = rpc_pb_content_callback; 407 | session->decoded_message->cb_content.arg = session; 408 | 409 | session->system_contexts = malloc(COUNT_OF(rpc_systems) * sizeof(void*)); 410 | for(size_t i = 0; i < COUNT_OF(rpc_systems); ++i) { 411 | session->system_contexts[i] = rpc_systems[i].alloc(session); 412 | } 413 | 414 | RpcHandler rpc_handler = { 415 | .message_handler = rpc_close_session_process, 416 | .decode_submessage = NULL, 417 | .context = session, 418 | }; 419 | rpc_add_handler(session, PB_Main_stop_session_tag, &rpc_handler); 420 | 421 | session->thread = furi_thread_alloc_ex("RpcSessionWorker", 3072, rpc_session_worker, session); 422 | 423 | furi_thread_set_state_context(session->thread, session); 424 | furi_thread_set_state_callback(session->thread, rpc_session_thread_state_callback); 425 | 426 | furi_thread_start(session->thread); 427 | 428 | rpc->sessions_count++; 429 | 430 | return session; 431 | } 432 | 433 | void rpc_session_close(RpcSession* session) { 434 | furi_check(session); 435 | furi_check(session->rpc); 436 | 437 | session->rpc->sessions_count--; 438 | 439 | rpc_session_set_send_bytes_callback(session, NULL); 440 | rpc_session_set_close_callback(session, NULL); 441 | rpc_session_set_buffer_is_empty_callback(session, NULL); 442 | furi_thread_flags_set(furi_thread_get_id(session->thread), RpcEvtDisconnect); 443 | } 444 | 445 | void rpc_on_system_start(void* p) { 446 | UNUSED(p); 447 | Rpc* rpc = malloc(sizeof(Rpc)); 448 | 449 | rpc->busy_mutex = furi_mutex_alloc(FuriMutexTypeNormal); 450 | 451 | Cli* cli = furi_record_open(RECORD_CLI); 452 | cli_add_command( 453 | cli, "start_rpc_session", CliCommandFlagParallelSafe, rpc_cli_command_start_session, rpc); 454 | 455 | furi_record_create(RECORD_RPC, rpc); 456 | rpc_keyboard_register(); 457 | } 458 | 459 | void rpc_add_handler(RpcSession* session, pb_size_t message_tag, RpcHandler* handler) { 460 | furi_assert(RpcHandlerDict_get(session->handlers, message_tag) == NULL); 461 | 462 | RpcHandlerDict_set_at(session->handlers, message_tag, *handler); 463 | } 464 | 465 | void rpc_send(RpcSession* session, PB_Main* message) { 466 | furi_assert(session); 467 | furi_assert(message); 468 | 469 | pb_ostream_t ostream = PB_OSTREAM_SIZING; 470 | 471 | #ifdef SRV_RPC_DEBUG 472 | FURI_LOG_I(TAG, "OUTPUT:"); 473 | rpc_debug_print_message(message); 474 | #endif 475 | 476 | bool result = pb_encode_ex(&ostream, &PB_Main_msg, message, PB_ENCODE_DELIMITED); 477 | furi_check(result && ostream.bytes_written); 478 | 479 | uint8_t* buffer = malloc(ostream.bytes_written); 480 | ostream = pb_ostream_from_buffer(buffer, ostream.bytes_written); 481 | 482 | pb_encode_ex(&ostream, &PB_Main_msg, message, PB_ENCODE_DELIMITED); 483 | 484 | #ifdef SRV_RPC_DEBUG 485 | rpc_debug_print_data("OUTPUT", buffer, ostream.bytes_written); 486 | #endif 487 | 488 | furi_mutex_acquire(session->callbacks_mutex, FuriWaitForever); 489 | if(session->send_bytes_callback) { 490 | session->send_bytes_callback(session->context, buffer, ostream.bytes_written); 491 | } 492 | furi_mutex_release(session->callbacks_mutex); 493 | 494 | free(buffer); 495 | } 496 | 497 | void rpc_send_and_release(RpcSession* session, PB_Main* message) { 498 | rpc_send(session, message); 499 | pb_release(&PB_Main_msg, message); 500 | } 501 | 502 | void rpc_send_and_release_empty(RpcSession* session, uint32_t command_id, PB_CommandStatus status) { 503 | furi_assert(session); 504 | 505 | PB_Main message = { 506 | .command_id = command_id, 507 | .command_status = status, 508 | .has_next = false, 509 | .which_content = PB_Main_empty_tag, 510 | }; 511 | 512 | rpc_send_and_release(session, &message); 513 | pb_release(&PB_Main_msg, &message); 514 | } 515 | 516 | size_t rpc_get_sessions_count(Rpc* rpc) { 517 | return rpc->sessions_count; 518 | } 519 | --------------------------------------------------------------------------------