mirror of
https://github.com/DarkFlippers/unleashed-firmware
synced 2024-11-30 00:10:30 +00:00
[FL-3669] Expansion module protocol (#3250)
* ApiSymbols: add furi_record_destroy * FuriHal: cleanup serial API, add logging configuration in RTC * FuriHal: hide private part in _i header. Toolbox: cleanup value index. SystemSettings: logging device and baudrate. * FuriHal: RTC logging method documentation * Synchronize API Symbols * Furi: mark HEAP_PRINT_DEBUG as broken * FuriHal: furi_hal_serial, add custom IRQ func * Fix PR review issues * Implement basic external module detection and echo * Update api symbols for f18 * Minimally working implementation (can create directory via rpc) * Make expansion protocol parser a header-only library * Rename a function * Improve thread syncronisation * Implement multi-packet transmissions * Improve test application * Clean up expansion worker code * Send heartbeat when host is ready * Update API symbols * Add draft documentation * Expansion worker: proper timeout and error handling * Expansion worker: correct TX, do not disable expansion callback * Expansion protocol: pc side test script * PC side expansion test: trying to change baudrate * Working comms between 2 flippers * Cleaner exit from expansion worker thread * Better checks * Add debug logs * Remove unneeded delays * Use USART as default expansion port * Refactor furi_hal_serial_control, fix crash * Improve furi_hal abstraction, wait for stable rx pin * Remove rogue include * Set proper exit reason on RPC error * Remove rogue comment * Remove RX stability check as potentially problematic * Improve expansion_test application * Remove rogue define * Give up on TODO * Implement expansion protocol checksum support * Update ExpansionModules.md * RPC: reverse input * Assets: sync protobuf * Fix typos * FuriHal: UART add reception DMA (#3220) * FuriHal: add DMA serial rx mode * usb_uart_bridge: switch to working with DMA * Sync api symbol versions * FuriHal: update serial docs and api * FuriHal: Selial added similar API for simple reception mode as with DMA * FuriHal: Update API target H18 * API: ver API H7 * FuriHal: Serial error processing * FuriHal: fix furi_hal_serial set baudrate * Sync api symbols * FuriHal: cleanup serial isr and various flag handling procedures * FuriHal: cleanup and simplify serial API * Debug: update UART Echo serial related flags * FuriHal: update serial API symbols naming * Make expansion_test compile * Remove unneeded file * Make PVS-studio happy * Optimise stack usage * Optimise heap usage, improve api signature * Fix typo * Clean up code * Update expansion_protocol.h * Fix unit tests * Add doxygen comments to expansion.h * Update/add doxygen comments * Update ExpansionModules.md * Github: new global code owner * FuriHal: naming in serial control * Expansion: check mutex acquire return result Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com> Co-authored-by: hedger <hedger@users.noreply.github.com> Co-authored-by: SkorP <skorpionm@yandex.ru> Co-authored-by: SG <who.just.the.doctor@gmail.com> Co-authored-by: Skorpionm <85568270+Skorpionm@users.noreply.github.com>
This commit is contained in:
parent
f9f67e6d54
commit
95737958ad
30 changed files with 2279 additions and 114 deletions
86
.github/CODEOWNERS
vendored
86
.github/CODEOWNERS
vendored
|
@ -1,68 +1,68 @@
|
||||||
# Who owns all the fish by default
|
# Who owns all the fish by default
|
||||||
* @skotopes @DrZlo13 @hedger
|
* @skotopes @DrZlo13 @hedger @gsurkov
|
||||||
|
|
||||||
# Apps
|
# Apps
|
||||||
/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gornekich
|
/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich
|
||||||
/applications/debug/accessor/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/debug/accessor/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/debug/battery_test_app/ @skotopes @DrZlo13 @hedger @gornekich
|
/applications/debug/battery_test_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich
|
||||||
/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gornekich
|
/applications/debug/bt_debug_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich
|
||||||
/applications/debug/file_browser_test/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/debug/file_browser_test/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/debug/lfrfid_debug/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/debug/lfrfid_debug/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/debug/text_box_test/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/debug/text_box_test/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/debug/uart_echo/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/debug/uart_echo/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/debug/usb_mouse/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/debug/usb_mouse/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/debug/usb_test/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/debug/usb_test/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
|
|
||||||
/applications/main/archive/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/main/archive/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/main/bad_usb/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/main/bad_usb/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/main/gpio/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/main/gpio/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/main/ibutton/ @skotopes @DrZlo13 @hedger @gsurkov
|
/applications/main/ibutton/ @skotopes @DrZlo13 @hedger @gsurkov
|
||||||
/applications/main/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
|
/applications/main/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
|
||||||
/applications/main/nfc/ @skotopes @DrZlo13 @hedger @gornekich @Astrrra
|
/applications/main/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich @Astrrra
|
||||||
/applications/main/subghz/ @skotopes @DrZlo13 @hedger @Skorpionm
|
/applications/main/subghz/ @skotopes @DrZlo13 @hedger @gsurkov @Skorpionm
|
||||||
/applications/main/u2f/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/main/u2f/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
|
|
||||||
/applications/services/bt/ @skotopes @DrZlo13 @hedger @gornekich
|
/applications/services/bt/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich
|
||||||
/applications/services/cli/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/services/cli/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/services/crypto/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/services/crypto/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/services/desktop/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/services/desktop/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/services/dolphin/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/services/dolphin/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/services/power/ @skotopes @DrZlo13 @hedger @gornekich
|
/applications/services/power/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich
|
||||||
/applications/services/rpc/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/services/rpc/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
|
|
||||||
/applications/services/bt_settings_app/ @skotopes @DrZlo13 @hedger @gornekich
|
/applications/services/bt_settings_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich
|
||||||
/applications/services/desktop_settings/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/services/desktop_settings/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/services/dolphin_passport/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/services/dolphin_passport/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/applications/services/power_settings_app/ @skotopes @DrZlo13 @hedger @gornekich
|
/applications/services/power_settings_app/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich
|
||||||
|
|
||||||
/applications/system/storage_move_to_sd/ @skotopes @DrZlo13 @hedger @nminaylov
|
/applications/system/storage_move_to_sd/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
|
|
||||||
/applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @nminaylov @gornekich @Astrrra @gsurkov @Skorpionm
|
/applications/debug/unit_tests/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov @gornekich @Astrrra @Skorpionm
|
||||||
|
|
||||||
/applications/examples/example_thermo/ @skotopes @DrZlo13 @hedger @gsurkov
|
/applications/examples/example_thermo/ @skotopes @DrZlo13 @hedger @gsurkov
|
||||||
|
|
||||||
# Firmware targets
|
# Firmware targets
|
||||||
/targets/ @skotopes @DrZlo13 @hedger @nminaylov
|
/targets/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
|
|
||||||
# Assets
|
# Assets
|
||||||
/applications/main/infrared/resources/ @skotopes @DrZlo13 @hedger @gsurkov
|
/applications/main/infrared/resources/ @skotopes @DrZlo13 @hedger @gsurkov
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
/documentation/ @skotopes @DrZlo13 @hedger @drunkbatya
|
/documentation/ @skotopes @DrZlo13 @hedger @gsurkov @drunkbatya
|
||||||
/scripts/toolchain/ @skotopes @DrZlo13 @hedger @drunkbatya
|
/scripts/toolchain/ @skotopes @DrZlo13 @hedger @gsurkov @drunkbatya
|
||||||
|
|
||||||
# Lib
|
# Lib
|
||||||
/lib/stm32wb_copro/ @skotopes @DrZlo13 @hedger @gornekich
|
/lib/stm32wb_copro/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich
|
||||||
/lib/digital_signal/ @skotopes @DrZlo13 @hedger @gornekich
|
/lib/digital_signal/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich
|
||||||
/lib/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
|
/lib/infrared/ @skotopes @DrZlo13 @hedger @gsurkov
|
||||||
/lib/lfrfid/ @skotopes @DrZlo13 @hedger @nminaylov
|
/lib/lfrfid/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/lib/libusb_stm32/ @skotopes @DrZlo13 @hedger @nminaylov
|
/lib/libusb_stm32/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/lib/mbedtls/ @skotopes @DrZlo13 @hedger @nminaylov
|
/lib/mbedtls/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/lib/micro-ecc/ @skotopes @DrZlo13 @hedger @nminaylov
|
/lib/micro-ecc/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/lib/nanopb/ @skotopes @DrZlo13 @hedger @nminaylov
|
/lib/nanopb/ @skotopes @DrZlo13 @hedger @gsurkov @nminaylov
|
||||||
/lib/nfc/ @skotopes @DrZlo13 @hedger @gornekich @Astrrra
|
/lib/nfc/ @skotopes @DrZlo13 @hedger @gsurkov @gornekich @Astrrra
|
||||||
/lib/one_wire/ @skotopes @DrZlo13 @hedger @gsurkov
|
/lib/one_wire/ @skotopes @DrZlo13 @hedger @gsurkov
|
||||||
/lib/subghz/ @skotopes @DrZlo13 @hedger @Skorpionm
|
/lib/subghz/ @skotopes @DrZlo13 @hedger @gsurkov @Skorpionm
|
||||||
|
|
||||||
# CI/CD
|
# CI/CD
|
||||||
/.github/workflows/ @skotopes @DrZlo13 @hedger @drunkbatya
|
/.github/workflows/ @skotopes @DrZlo13 @hedger @gsurkov @drunkbatya
|
||||||
|
|
12
applications/debug/expansion_test/application.fam
Normal file
12
applications/debug/expansion_test/application.fam
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
App(
|
||||||
|
appid="expansion_test",
|
||||||
|
name="Expansion Module Test",
|
||||||
|
apptype=FlipperAppType.DEBUG,
|
||||||
|
entry_point="expansion_test_app",
|
||||||
|
requires=["expansion_start"],
|
||||||
|
fap_libs=["assets"],
|
||||||
|
stack_size=1 * 1024,
|
||||||
|
order=20,
|
||||||
|
fap_category="Debug",
|
||||||
|
fap_file_assets="assets",
|
||||||
|
)
|
9
applications/debug/expansion_test/assets/test.txt
Normal file
9
applications/debug/expansion_test/assets/test.txt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
"Did you ever hear the tragedy of Darth Plagueis the Wise?"
|
||||||
|
"No."
|
||||||
|
"I thought not. It's not a story the Jedi would tell you. It's a Sith legend. Darth Plagueis... was a Dark Lord of the Sith so powerful and so wise, he could use the Force to influence the midi-chlorians... to create... life. He had such a knowledge of the dark side, he could even keep the ones he cared about... from dying."
|
||||||
|
"He could actually... save people from death?"
|
||||||
|
"The dark side of the Force is a pathway to many abilities... some consider to be unnatural."
|
||||||
|
"Wh– What happened to him?"
|
||||||
|
"He became so powerful, the only thing he was afraid of was... losing his power. Which eventually, of course, he did. Unfortunately, he taught his apprentice everything he knew. Then his apprentice killed him in his sleep. It's ironic. He could save others from death, but not himself."
|
||||||
|
"Is it possible to learn this power?"
|
||||||
|
"Not from a Jedi."
|
454
applications/debug/expansion_test/expansion_test.c
Normal file
454
applications/debug/expansion_test/expansion_test.c
Normal file
|
@ -0,0 +1,454 @@
|
||||||
|
/**
|
||||||
|
* @file expansion_test.c
|
||||||
|
* @brief Expansion module support testing application.
|
||||||
|
*
|
||||||
|
* Before running, connect pins using the following scheme:
|
||||||
|
* 13 -> 16 (USART TX to LPUART RX)
|
||||||
|
* 14 -> 15 (USART RX to LPUART TX)
|
||||||
|
*
|
||||||
|
* What this application does:
|
||||||
|
*
|
||||||
|
* - Enables module support and emulates the module on a single device
|
||||||
|
* (hence the above connection),
|
||||||
|
* - Connects to the expansion module service, sets baud rate,
|
||||||
|
* - Starts the RPC session,
|
||||||
|
* - Creates a directory at `/ext/ExpansionTest` and writes a file
|
||||||
|
* named `test.txt` under it,
|
||||||
|
* - Plays an audiovisual alert (sound and blinking display),
|
||||||
|
* - Waits 10 cycles of idle loop,
|
||||||
|
* - Stops the RPC session,
|
||||||
|
* - Waits another 10 cycles of idle loop,
|
||||||
|
* - Exits (plays a sound if any of the above steps failed).
|
||||||
|
*/
|
||||||
|
#include <furi.h>
|
||||||
|
|
||||||
|
#include <furi_hal_resources.h>
|
||||||
|
|
||||||
|
#include <furi_hal_serial.h>
|
||||||
|
#include <furi_hal_serial_control.h>
|
||||||
|
|
||||||
|
#include <pb.h>
|
||||||
|
#include <pb_decode.h>
|
||||||
|
#include <pb_encode.h>
|
||||||
|
|
||||||
|
#include <flipper.pb.h>
|
||||||
|
|
||||||
|
#include <storage/storage.h>
|
||||||
|
#include <expansion/expansion.h>
|
||||||
|
#include <notification/notification_messages.h>
|
||||||
|
#include <expansion/expansion_protocol.h>
|
||||||
|
|
||||||
|
#define TAG "ExpansionTest"
|
||||||
|
|
||||||
|
#define TEST_DIR_PATH EXT_PATH(TAG)
|
||||||
|
#define TEST_FILE_NAME "test.txt"
|
||||||
|
#define TEST_FILE_PATH EXT_PATH(TAG "/" TEST_FILE_NAME)
|
||||||
|
|
||||||
|
#define HOST_SERIAL_ID (FuriHalSerialIdLpuart)
|
||||||
|
#define MODULE_SERIAL_ID (FuriHalSerialIdUsart)
|
||||||
|
|
||||||
|
#define RECEIVE_BUFFER_SIZE (sizeof(ExpansionFrame) + sizeof(ExpansionFrameChecksum))
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ExpansionTestAppFlagData = 1U << 0,
|
||||||
|
ExpansionTestAppFlagExit = 1U << 1,
|
||||||
|
} ExpansionTestAppFlag;
|
||||||
|
|
||||||
|
#define EXPANSION_TEST_APP_ALL_FLAGS (ExpansionTestAppFlagData | ExpansionTestAppFlagExit)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
FuriThreadId thread_id;
|
||||||
|
Expansion* expansion;
|
||||||
|
FuriHalSerialHandle* handle;
|
||||||
|
FuriStreamBuffer* buf;
|
||||||
|
ExpansionFrame frame;
|
||||||
|
PB_Main msg;
|
||||||
|
Storage* storage;
|
||||||
|
} ExpansionTestApp;
|
||||||
|
|
||||||
|
static void expansion_test_app_serial_rx_callback(
|
||||||
|
FuriHalSerialHandle* handle,
|
||||||
|
FuriHalSerialRxEvent event,
|
||||||
|
void* context) {
|
||||||
|
furi_assert(handle);
|
||||||
|
furi_assert(context);
|
||||||
|
ExpansionTestApp* app = context;
|
||||||
|
|
||||||
|
if(event == FuriHalSerialRxEventData) {
|
||||||
|
const uint8_t data = furi_hal_serial_async_rx(handle);
|
||||||
|
furi_stream_buffer_send(app->buf, &data, sizeof(data), 0);
|
||||||
|
furi_thread_flags_set(app->thread_id, ExpansionTestAppFlagData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ExpansionTestApp* expansion_test_app_alloc() {
|
||||||
|
ExpansionTestApp* instance = malloc(sizeof(ExpansionTestApp));
|
||||||
|
instance->buf = furi_stream_buffer_alloc(RECEIVE_BUFFER_SIZE, 1);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void expansion_test_app_free(ExpansionTestApp* instance) {
|
||||||
|
furi_stream_buffer_free(instance->buf);
|
||||||
|
free(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void expansion_test_app_start(ExpansionTestApp* instance) {
|
||||||
|
instance->thread_id = furi_thread_get_current_id();
|
||||||
|
instance->expansion = furi_record_open(RECORD_EXPANSION);
|
||||||
|
instance->handle = furi_hal_serial_control_acquire(MODULE_SERIAL_ID);
|
||||||
|
// Configure the serial port
|
||||||
|
furi_hal_serial_init(instance->handle, EXPANSION_PROTOCOL_DEFAULT_BAUD_RATE);
|
||||||
|
// Start waiting for the initial pulse
|
||||||
|
expansion_enable(instance->expansion, HOST_SERIAL_ID);
|
||||||
|
|
||||||
|
furi_hal_serial_async_rx_start(
|
||||||
|
instance->handle, expansion_test_app_serial_rx_callback, instance, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void expansion_test_app_stop(ExpansionTestApp* instance) {
|
||||||
|
// Give back the module handle
|
||||||
|
furi_hal_serial_control_release(instance->handle);
|
||||||
|
// Turn expansion module support off
|
||||||
|
expansion_disable(instance->expansion);
|
||||||
|
furi_record_close(RECORD_EXPANSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool expansion_test_app_is_success_response(const ExpansionFrame* response) {
|
||||||
|
return response->header.type == ExpansionFrameTypeStatus &&
|
||||||
|
response->content.status.error == ExpansionFrameErrorNone;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool expansion_test_app_is_success_rpc_message(const PB_Main* message) {
|
||||||
|
return (message->command_status == PB_CommandStatus_OK ||
|
||||||
|
message->command_status == PB_CommandStatus_ERROR_STORAGE_EXIST) &&
|
||||||
|
(message->which_content == PB_Main_empty_tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t expansion_test_app_receive_callback(uint8_t* data, size_t data_size, void* context) {
|
||||||
|
ExpansionTestApp* instance = context;
|
||||||
|
|
||||||
|
size_t received_size = 0;
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
received_size += furi_stream_buffer_receive(
|
||||||
|
instance->buf, data + received_size, data_size - received_size, 0);
|
||||||
|
if(received_size == data_size) break;
|
||||||
|
|
||||||
|
const uint32_t flags = furi_thread_flags_wait(
|
||||||
|
EXPANSION_TEST_APP_ALL_FLAGS, FuriFlagWaitAny, EXPANSION_PROTOCOL_TIMEOUT_MS);
|
||||||
|
|
||||||
|
// Exit on any error
|
||||||
|
if(flags & FuriFlagError) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return received_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t
|
||||||
|
expansion_test_app_send_callback(const uint8_t* data, size_t data_size, void* context) {
|
||||||
|
ExpansionTestApp* instance = context;
|
||||||
|
|
||||||
|
furi_hal_serial_tx(instance->handle, data, data_size);
|
||||||
|
furi_hal_serial_tx_wait_complete(instance->handle);
|
||||||
|
|
||||||
|
return data_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_receive_frame(ExpansionTestApp* instance, ExpansionFrame* frame) {
|
||||||
|
return expansion_protocol_decode(frame, expansion_test_app_receive_callback, instance) ==
|
||||||
|
ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
expansion_test_app_send_status_response(ExpansionTestApp* instance, ExpansionFrameError error) {
|
||||||
|
ExpansionFrame frame = {
|
||||||
|
.header.type = ExpansionFrameTypeStatus,
|
||||||
|
.content.status.error = error,
|
||||||
|
};
|
||||||
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
||||||
|
ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_send_heartbeat(ExpansionTestApp* instance) {
|
||||||
|
ExpansionFrame frame = {
|
||||||
|
.header.type = ExpansionFrameTypeHeartbeat,
|
||||||
|
.content.heartbeat = {},
|
||||||
|
};
|
||||||
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
||||||
|
ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
expansion_test_app_send_baud_rate_request(ExpansionTestApp* instance, uint32_t baud_rate) {
|
||||||
|
ExpansionFrame frame = {
|
||||||
|
.header.type = ExpansionFrameTypeBaudRate,
|
||||||
|
.content.baud_rate.baud = baud_rate,
|
||||||
|
};
|
||||||
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
||||||
|
ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_send_control_request(
|
||||||
|
ExpansionTestApp* instance,
|
||||||
|
ExpansionFrameControlCommand command) {
|
||||||
|
ExpansionFrame frame = {
|
||||||
|
.header.type = ExpansionFrameTypeControl,
|
||||||
|
.content.control.command = command,
|
||||||
|
};
|
||||||
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
||||||
|
ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_send_data_request(
|
||||||
|
ExpansionTestApp* instance,
|
||||||
|
const uint8_t* data,
|
||||||
|
size_t data_size) {
|
||||||
|
furi_assert(data_size <= EXPANSION_PROTOCOL_MAX_DATA_SIZE);
|
||||||
|
|
||||||
|
ExpansionFrame frame = {
|
||||||
|
.header.type = ExpansionFrameTypeData,
|
||||||
|
.content.data.size = data_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
memcpy(frame.content.data.bytes, data, data_size);
|
||||||
|
return expansion_protocol_encode(&frame, expansion_test_app_send_callback, instance) ==
|
||||||
|
ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_rpc_encode_callback(
|
||||||
|
pb_ostream_t* stream,
|
||||||
|
const pb_byte_t* data,
|
||||||
|
size_t data_size) {
|
||||||
|
ExpansionTestApp* instance = stream->state;
|
||||||
|
|
||||||
|
size_t size_sent = 0;
|
||||||
|
|
||||||
|
while(size_sent < data_size) {
|
||||||
|
const size_t current_size = MIN(data_size - size_sent, EXPANSION_PROTOCOL_MAX_DATA_SIZE);
|
||||||
|
if(!expansion_test_app_send_data_request(instance, data + size_sent, current_size)) break;
|
||||||
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
||||||
|
if(!expansion_test_app_is_success_response(&instance->frame)) break;
|
||||||
|
size_sent += current_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size_sent == data_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_send_rpc_request(ExpansionTestApp* instance, PB_Main* message) {
|
||||||
|
pb_ostream_t stream = {
|
||||||
|
.callback = expansion_test_app_rpc_encode_callback,
|
||||||
|
.state = instance,
|
||||||
|
.max_size = SIZE_MAX,
|
||||||
|
.bytes_written = 0,
|
||||||
|
.errmsg = NULL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const bool success = pb_encode_ex(&stream, &PB_Main_msg, message, PB_ENCODE_DELIMITED);
|
||||||
|
pb_release(&PB_Main_msg, message);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_receive_rpc_request(ExpansionTestApp* instance, PB_Main* message) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
||||||
|
if(!expansion_test_app_send_status_response(instance, ExpansionFrameErrorNone)) break;
|
||||||
|
if(instance->frame.header.type != ExpansionFrameTypeData) break;
|
||||||
|
pb_istream_t stream = pb_istream_from_buffer(
|
||||||
|
instance->frame.content.data.bytes, instance->frame.content.data.size);
|
||||||
|
if(!pb_decode_ex(&stream, &PB_Main_msg, message, PB_DECODE_DELIMITED)) break;
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_send_presence(ExpansionTestApp* instance) {
|
||||||
|
// Send pulses to emulate module insertion
|
||||||
|
const uint8_t init = 0xAA;
|
||||||
|
furi_hal_serial_tx(instance->handle, &init, sizeof(init));
|
||||||
|
furi_hal_serial_tx_wait_complete(instance->handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_wait_ready(ExpansionTestApp* instance) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
||||||
|
if(instance->frame.header.type != ExpansionFrameTypeHeartbeat) break;
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_handshake(ExpansionTestApp* instance) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(!expansion_test_app_send_baud_rate_request(instance, 230400)) break;
|
||||||
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
||||||
|
if(!expansion_test_app_is_success_response(&instance->frame)) break;
|
||||||
|
furi_hal_serial_set_br(instance->handle, 230400);
|
||||||
|
furi_delay_ms(EXPANSION_PROTOCOL_BAUD_CHANGE_DT_MS);
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_start_rpc(ExpansionTestApp* instance) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(!expansion_test_app_send_control_request(instance, ExpansionFrameControlCommandStartRpc))
|
||||||
|
break;
|
||||||
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
||||||
|
if(!expansion_test_app_is_success_response(&instance->frame)) break;
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_rpc_mkdir(ExpansionTestApp* instance) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
instance->msg.command_id++;
|
||||||
|
instance->msg.command_status = PB_CommandStatus_OK;
|
||||||
|
instance->msg.which_content = PB_Main_storage_mkdir_request_tag;
|
||||||
|
instance->msg.has_next = false;
|
||||||
|
instance->msg.content.storage_mkdir_request.path = TEST_DIR_PATH;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(!expansion_test_app_send_rpc_request(instance, &instance->msg)) break;
|
||||||
|
if(!expansion_test_app_receive_rpc_request(instance, &instance->msg)) break;
|
||||||
|
if(!expansion_test_app_is_success_rpc_message(&instance->msg)) break;
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_rpc_write(ExpansionTestApp* instance) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||||
|
File* file = storage_file_alloc(storage);
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(!storage_file_open(file, APP_ASSETS_PATH(TEST_FILE_NAME), FSAM_READ, FSOM_OPEN_EXISTING))
|
||||||
|
break;
|
||||||
|
|
||||||
|
const uint64_t file_size = storage_file_size(file);
|
||||||
|
|
||||||
|
instance->msg.command_id++;
|
||||||
|
instance->msg.command_status = PB_CommandStatus_OK;
|
||||||
|
instance->msg.which_content = PB_Main_storage_write_request_tag;
|
||||||
|
instance->msg.has_next = false;
|
||||||
|
instance->msg.content.storage_write_request.path = TEST_FILE_PATH;
|
||||||
|
instance->msg.content.storage_write_request.has_file = true;
|
||||||
|
instance->msg.content.storage_write_request.file.data =
|
||||||
|
malloc(PB_BYTES_ARRAY_T_ALLOCSIZE(file_size));
|
||||||
|
instance->msg.content.storage_write_request.file.data->size = file_size;
|
||||||
|
|
||||||
|
const size_t bytes_read = storage_file_read(
|
||||||
|
file, instance->msg.content.storage_write_request.file.data->bytes, file_size);
|
||||||
|
|
||||||
|
if(bytes_read != file_size) {
|
||||||
|
pb_release(&PB_Main_msg, &instance->msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!expansion_test_app_send_rpc_request(instance, &instance->msg)) break;
|
||||||
|
if(!expansion_test_app_receive_rpc_request(instance, &instance->msg)) break;
|
||||||
|
if(!expansion_test_app_is_success_rpc_message(&instance->msg)) break;
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
storage_file_free(file);
|
||||||
|
furi_record_close(RECORD_STORAGE);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_rpc_alert(ExpansionTestApp* instance) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
instance->msg.command_id++;
|
||||||
|
instance->msg.command_status = PB_CommandStatus_OK;
|
||||||
|
instance->msg.which_content = PB_Main_system_play_audiovisual_alert_request_tag;
|
||||||
|
instance->msg.has_next = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(!expansion_test_app_send_rpc_request(instance, &instance->msg)) break;
|
||||||
|
if(!expansion_test_app_receive_rpc_request(instance, &instance->msg)) break;
|
||||||
|
if(instance->msg.which_content != PB_Main_empty_tag) break;
|
||||||
|
if(instance->msg.command_status != PB_CommandStatus_OK) break;
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_idle(ExpansionTestApp* instance, uint32_t num_cycles) {
|
||||||
|
uint32_t num_cycles_done;
|
||||||
|
for(num_cycles_done = 0; num_cycles_done < num_cycles; ++num_cycles_done) {
|
||||||
|
if(!expansion_test_app_send_heartbeat(instance)) break;
|
||||||
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
||||||
|
if(instance->frame.header.type != ExpansionFrameTypeHeartbeat) break;
|
||||||
|
furi_delay_ms(EXPANSION_PROTOCOL_TIMEOUT_MS - 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
return num_cycles_done == num_cycles;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_test_app_stop_rpc(ExpansionTestApp* instance) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(!expansion_test_app_send_control_request(instance, ExpansionFrameControlCommandStopRpc))
|
||||||
|
break;
|
||||||
|
if(!expansion_test_app_receive_frame(instance, &instance->frame)) break;
|
||||||
|
if(!expansion_test_app_is_success_response(&instance->frame)) break;
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t expansion_test_app(void* p) {
|
||||||
|
UNUSED(p);
|
||||||
|
|
||||||
|
ExpansionTestApp* instance = expansion_test_app_alloc();
|
||||||
|
expansion_test_app_start(instance);
|
||||||
|
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(!expansion_test_app_send_presence(instance)) break;
|
||||||
|
if(!expansion_test_app_wait_ready(instance)) break;
|
||||||
|
if(!expansion_test_app_handshake(instance)) break;
|
||||||
|
if(!expansion_test_app_start_rpc(instance)) break;
|
||||||
|
if(!expansion_test_app_rpc_mkdir(instance)) break;
|
||||||
|
if(!expansion_test_app_rpc_write(instance)) break;
|
||||||
|
if(!expansion_test_app_rpc_alert(instance)) break;
|
||||||
|
if(!expansion_test_app_idle(instance, 10)) break;
|
||||||
|
if(!expansion_test_app_stop_rpc(instance)) break;
|
||||||
|
if(!expansion_test_app_idle(instance, 10)) break;
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
expansion_test_app_stop(instance);
|
||||||
|
expansion_test_app_free(instance);
|
||||||
|
|
||||||
|
if(!success) {
|
||||||
|
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||||
|
notification_message(notification, &sequence_error);
|
||||||
|
furi_record_close(RECORD_NOTIFICATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
157
applications/debug/unit_tests/expansion/expansion_test.c
Normal file
157
applications/debug/unit_tests/expansion/expansion_test.c
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
#include "../minunit.h"
|
||||||
|
|
||||||
|
#include <furi.h>
|
||||||
|
#include <expansion/expansion_protocol.h>
|
||||||
|
|
||||||
|
MU_TEST(test_expansion_encoded_size) {
|
||||||
|
ExpansionFrame frame = {};
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeHeartbeat;
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_encoded_size(&frame));
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeStatus;
|
||||||
|
mu_assert_int_eq(2, expansion_frame_get_encoded_size(&frame));
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeBaudRate;
|
||||||
|
mu_assert_int_eq(5, expansion_frame_get_encoded_size(&frame));
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeControl;
|
||||||
|
mu_assert_int_eq(2, expansion_frame_get_encoded_size(&frame));
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeData;
|
||||||
|
for(size_t i = 0; i <= EXPANSION_PROTOCOL_MAX_DATA_SIZE; ++i) {
|
||||||
|
frame.content.data.size = i;
|
||||||
|
mu_assert_int_eq(i + 2, expansion_frame_get_encoded_size(&frame));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MU_TEST(test_expansion_remaining_size) {
|
||||||
|
ExpansionFrame frame = {};
|
||||||
|
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_remaining_size(&frame, 0));
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeHeartbeat;
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_remaining_size(&frame, 0));
|
||||||
|
mu_assert_int_eq(0, expansion_frame_get_remaining_size(&frame, 1));
|
||||||
|
mu_assert_int_eq(0, expansion_frame_get_remaining_size(&frame, 100));
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeStatus;
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_remaining_size(&frame, 0));
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_remaining_size(&frame, 1));
|
||||||
|
mu_assert_int_eq(0, expansion_frame_get_remaining_size(&frame, 2));
|
||||||
|
mu_assert_int_eq(0, expansion_frame_get_remaining_size(&frame, 100));
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeBaudRate;
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_remaining_size(&frame, 0));
|
||||||
|
mu_assert_int_eq(4, expansion_frame_get_remaining_size(&frame, 1));
|
||||||
|
mu_assert_int_eq(0, expansion_frame_get_remaining_size(&frame, 5));
|
||||||
|
mu_assert_int_eq(0, expansion_frame_get_remaining_size(&frame, 100));
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeControl;
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_remaining_size(&frame, 0));
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_remaining_size(&frame, 1));
|
||||||
|
mu_assert_int_eq(0, expansion_frame_get_remaining_size(&frame, 2));
|
||||||
|
mu_assert_int_eq(0, expansion_frame_get_remaining_size(&frame, 100));
|
||||||
|
|
||||||
|
frame.header.type = ExpansionFrameTypeData;
|
||||||
|
frame.content.data.size = EXPANSION_PROTOCOL_MAX_DATA_SIZE;
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_remaining_size(&frame, 0));
|
||||||
|
mu_assert_int_eq(1, expansion_frame_get_remaining_size(&frame, 1));
|
||||||
|
mu_assert_int_eq(
|
||||||
|
EXPANSION_PROTOCOL_MAX_DATA_SIZE, expansion_frame_get_remaining_size(&frame, 2));
|
||||||
|
for(size_t i = 0; i <= EXPANSION_PROTOCOL_MAX_DATA_SIZE; ++i) {
|
||||||
|
mu_assert_int_eq(
|
||||||
|
EXPANSION_PROTOCOL_MAX_DATA_SIZE - i,
|
||||||
|
expansion_frame_get_remaining_size(&frame, i + 2));
|
||||||
|
}
|
||||||
|
mu_assert_int_eq(0, expansion_frame_get_remaining_size(&frame, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void* data_out;
|
||||||
|
size_t size_available;
|
||||||
|
size_t size_sent;
|
||||||
|
} TestExpansionSendStream;
|
||||||
|
|
||||||
|
static size_t test_expansion_send_callback(const uint8_t* data, size_t data_size, void* context) {
|
||||||
|
TestExpansionSendStream* stream = context;
|
||||||
|
const size_t size_sent = MIN(data_size, stream->size_available);
|
||||||
|
|
||||||
|
memcpy(stream->data_out + stream->size_sent, data, size_sent);
|
||||||
|
|
||||||
|
stream->size_available -= size_sent;
|
||||||
|
stream->size_sent += size_sent;
|
||||||
|
|
||||||
|
return size_sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const void* data_in;
|
||||||
|
size_t size_available;
|
||||||
|
size_t size_received;
|
||||||
|
} TestExpansionReceiveStream;
|
||||||
|
|
||||||
|
static size_t test_expansion_receive_callback(uint8_t* data, size_t data_size, void* context) {
|
||||||
|
TestExpansionReceiveStream* stream = context;
|
||||||
|
const size_t size_received = MIN(data_size, stream->size_available);
|
||||||
|
|
||||||
|
memcpy(data, stream->data_in + stream->size_received, size_received);
|
||||||
|
|
||||||
|
stream->size_available -= size_received;
|
||||||
|
stream->size_received += size_received;
|
||||||
|
|
||||||
|
return size_received;
|
||||||
|
}
|
||||||
|
|
||||||
|
MU_TEST(test_expansion_encode_decode_frame) {
|
||||||
|
const ExpansionFrame frame_in = {
|
||||||
|
.header.type = ExpansionFrameTypeData,
|
||||||
|
.content.data.size = 8,
|
||||||
|
.content.data.bytes = {0xde, 0xad, 0xbe, 0xef, 0xfe, 0xed, 0xca, 0xfe},
|
||||||
|
};
|
||||||
|
|
||||||
|
uint8_t encoded_data[sizeof(ExpansionFrame) + sizeof(ExpansionFrameChecksum)];
|
||||||
|
memset(encoded_data, 0, sizeof(encoded_data));
|
||||||
|
|
||||||
|
TestExpansionSendStream send_stream = {
|
||||||
|
.data_out = &encoded_data,
|
||||||
|
.size_available = sizeof(encoded_data),
|
||||||
|
.size_sent = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const size_t encoded_size = expansion_frame_get_encoded_size(&frame_in);
|
||||||
|
|
||||||
|
mu_assert_int_eq(
|
||||||
|
expansion_protocol_encode(&frame_in, test_expansion_send_callback, &send_stream),
|
||||||
|
ExpansionProtocolStatusOk);
|
||||||
|
mu_assert_int_eq(encoded_size + sizeof(ExpansionFrameChecksum), send_stream.size_sent);
|
||||||
|
mu_assert_int_eq(
|
||||||
|
expansion_protocol_get_checksum((const uint8_t*)&frame_in, encoded_size),
|
||||||
|
encoded_data[encoded_size]);
|
||||||
|
mu_assert_mem_eq(&frame_in, &encoded_data, encoded_size);
|
||||||
|
|
||||||
|
TestExpansionReceiveStream stream = {
|
||||||
|
.data_in = encoded_data,
|
||||||
|
.size_available = send_stream.size_sent,
|
||||||
|
.size_received = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
ExpansionFrame frame_out;
|
||||||
|
|
||||||
|
mu_assert_int_eq(
|
||||||
|
expansion_protocol_decode(&frame_out, test_expansion_receive_callback, &stream),
|
||||||
|
ExpansionProtocolStatusOk);
|
||||||
|
mu_assert_int_eq(encoded_size + sizeof(ExpansionFrameChecksum), stream.size_received);
|
||||||
|
mu_assert_mem_eq(&frame_in, &frame_out, encoded_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
MU_TEST_SUITE(test_expansion_suite) {
|
||||||
|
MU_RUN_TEST(test_expansion_encoded_size);
|
||||||
|
MU_RUN_TEST(test_expansion_remaining_size);
|
||||||
|
MU_RUN_TEST(test_expansion_encode_decode_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
int run_minunit_test_expansion() {
|
||||||
|
MU_RUN_SUITE(test_expansion_suite);
|
||||||
|
return MU_EXIT_CODE;
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ int run_minunit_test_bit_lib();
|
||||||
int run_minunit_test_float_tools();
|
int run_minunit_test_float_tools();
|
||||||
int run_minunit_test_bt();
|
int run_minunit_test_bt();
|
||||||
int run_minunit_test_dialogs_file_browser_options();
|
int run_minunit_test_dialogs_file_browser_options();
|
||||||
|
int run_minunit_test_expansion();
|
||||||
|
|
||||||
typedef int (*UnitTestEntry)();
|
typedef int (*UnitTestEntry)();
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ const UnitTest unit_tests[] = {
|
||||||
{.name = "bt", .entry = run_minunit_test_bt},
|
{.name = "bt", .entry = run_minunit_test_bt},
|
||||||
{.name = "dialogs_file_browser_options",
|
{.name = "dialogs_file_browser_options",
|
||||||
.entry = run_minunit_test_dialogs_file_browser_options},
|
.entry = run_minunit_test_dialogs_file_browser_options},
|
||||||
|
{.name = "expansion", .entry = run_minunit_test_expansion},
|
||||||
};
|
};
|
||||||
|
|
||||||
void minunit_print_progress() {
|
void minunit_print_progress() {
|
||||||
|
|
|
@ -5,6 +5,7 @@ App(
|
||||||
provides=[
|
provides=[
|
||||||
"crypto_start",
|
"crypto_start",
|
||||||
"rpc_start",
|
"rpc_start",
|
||||||
|
"expansion_start",
|
||||||
"bt",
|
"bt",
|
||||||
"desktop",
|
"desktop",
|
||||||
"loader",
|
"loader",
|
||||||
|
|
12
applications/services/expansion/application.fam
Normal file
12
applications/services/expansion/application.fam
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
App(
|
||||||
|
appid="expansion_start",
|
||||||
|
apptype=FlipperAppType.STARTUP,
|
||||||
|
entry_point="expansion_on_system_start",
|
||||||
|
cdefines=["SRV_EXPANSION"],
|
||||||
|
sdk_headers=[
|
||||||
|
"expansion.h",
|
||||||
|
],
|
||||||
|
requires=["rpc_start"],
|
||||||
|
provides=["expansion_settings"],
|
||||||
|
order=10,
|
||||||
|
)
|
437
applications/services/expansion/expansion.c
Normal file
437
applications/services/expansion/expansion.c
Normal file
|
@ -0,0 +1,437 @@
|
||||||
|
#include "expansion.h"
|
||||||
|
|
||||||
|
#include <furi_hal_power.h>
|
||||||
|
#include <furi_hal_serial.h>
|
||||||
|
#include <furi_hal_serial_control.h>
|
||||||
|
|
||||||
|
#include <furi.h>
|
||||||
|
|
||||||
|
#include <rpc/rpc.h>
|
||||||
|
|
||||||
|
#include "expansion_settings.h"
|
||||||
|
#include "expansion_protocol.h"
|
||||||
|
|
||||||
|
#define TAG "ExpansionSrv"
|
||||||
|
|
||||||
|
#define EXPANSION_BUFFER_SIZE (sizeof(ExpansionFrame) + sizeof(ExpansionFrameChecksum))
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ExpansionStateDisabled,
|
||||||
|
ExpansionStateEnabled,
|
||||||
|
ExpansionStateRunning,
|
||||||
|
} ExpansionState;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ExpansionSessionStateHandShake,
|
||||||
|
ExpansionSessionStateConnected,
|
||||||
|
ExpansionSessionStateRpcActive,
|
||||||
|
} ExpansionSessionState;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ExpansionSessionExitReasonUnknown,
|
||||||
|
ExpansionSessionExitReasonUser,
|
||||||
|
ExpansionSessionExitReasonError,
|
||||||
|
ExpansionSessionExitReasonTimeout,
|
||||||
|
} ExpansionSessionExitReason;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ExpansionFlagStop = 1 << 0,
|
||||||
|
ExpansionFlagData = 1 << 1,
|
||||||
|
ExpansionFlagError = 1 << 2,
|
||||||
|
} ExpansionFlag;
|
||||||
|
|
||||||
|
#define EXPANSION_ALL_FLAGS (ExpansionFlagData | ExpansionFlagStop)
|
||||||
|
|
||||||
|
struct Expansion {
|
||||||
|
ExpansionState state;
|
||||||
|
ExpansionSessionState session_state;
|
||||||
|
ExpansionSessionExitReason exit_reason;
|
||||||
|
FuriStreamBuffer* rx_buf;
|
||||||
|
FuriSemaphore* tx_semaphore;
|
||||||
|
FuriMutex* state_mutex;
|
||||||
|
FuriThread* worker_thread;
|
||||||
|
FuriHalSerialId serial_id;
|
||||||
|
FuriHalSerialHandle* serial_handle;
|
||||||
|
RpcSession* rpc_session;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void expansion_detect_callback(void* context);
|
||||||
|
|
||||||
|
// Called in UART IRQ context
|
||||||
|
static void expansion_serial_rx_callback(
|
||||||
|
FuriHalSerialHandle* handle,
|
||||||
|
FuriHalSerialRxEvent event,
|
||||||
|
void* context) {
|
||||||
|
furi_assert(handle);
|
||||||
|
furi_assert(context);
|
||||||
|
|
||||||
|
Expansion* instance = context;
|
||||||
|
|
||||||
|
if(event == FuriHalSerialRxEventData) {
|
||||||
|
const uint8_t data = furi_hal_serial_async_rx(handle);
|
||||||
|
furi_stream_buffer_send(instance->rx_buf, &data, sizeof(data), 0);
|
||||||
|
furi_thread_flags_set(furi_thread_get_id(instance->worker_thread), ExpansionFlagData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t expansion_receive_callback(uint8_t* data, size_t data_size, void* context) {
|
||||||
|
Expansion* instance = context;
|
||||||
|
|
||||||
|
size_t received_size = 0;
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
received_size += furi_stream_buffer_receive(
|
||||||
|
instance->rx_buf, data + received_size, data_size - received_size, 0);
|
||||||
|
|
||||||
|
if(received_size == data_size) break;
|
||||||
|
|
||||||
|
const uint32_t flags = furi_thread_flags_wait(
|
||||||
|
EXPANSION_ALL_FLAGS, FuriFlagWaitAny, furi_ms_to_ticks(EXPANSION_PROTOCOL_TIMEOUT_MS));
|
||||||
|
|
||||||
|
if(flags & FuriFlagError) {
|
||||||
|
if(flags == (unsigned)FuriFlagErrorTimeout) {
|
||||||
|
// Exiting due to timeout
|
||||||
|
instance->exit_reason = ExpansionSessionExitReasonTimeout;
|
||||||
|
} else {
|
||||||
|
// Exiting due to an unspecified error
|
||||||
|
instance->exit_reason = ExpansionSessionExitReasonError;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} else if(flags & ExpansionFlagStop) {
|
||||||
|
// Exiting due to explicit request
|
||||||
|
instance->exit_reason = ExpansionSessionExitReasonUser;
|
||||||
|
break;
|
||||||
|
} else if(flags & ExpansionFlagError) {
|
||||||
|
// Exiting due to RPC error
|
||||||
|
instance->exit_reason = ExpansionSessionExitReasonError;
|
||||||
|
break;
|
||||||
|
} else if(flags & ExpansionFlagData) {
|
||||||
|
// Go to buffer reading
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return received_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool expansion_receive_frame(Expansion* instance, ExpansionFrame* frame) {
|
||||||
|
return expansion_protocol_decode(frame, expansion_receive_callback, instance) ==
|
||||||
|
ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t expansion_send_callback(const uint8_t* data, size_t data_size, void* context) {
|
||||||
|
Expansion* instance = context;
|
||||||
|
furi_hal_serial_tx(instance->serial_handle, data, data_size);
|
||||||
|
furi_hal_serial_tx_wait_complete(instance->serial_handle);
|
||||||
|
return data_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool expansion_send_frame(Expansion* instance, const ExpansionFrame* frame) {
|
||||||
|
return expansion_protocol_encode(frame, expansion_send_callback, instance) ==
|
||||||
|
ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_send_heartbeat(Expansion* instance) {
|
||||||
|
const ExpansionFrame frame = {
|
||||||
|
.header.type = ExpansionFrameTypeHeartbeat,
|
||||||
|
.content.heartbeat = {},
|
||||||
|
};
|
||||||
|
|
||||||
|
return expansion_send_frame(instance, &frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_send_status_response(Expansion* instance, ExpansionFrameError error) {
|
||||||
|
const ExpansionFrame frame = {
|
||||||
|
.header.type = ExpansionFrameTypeStatus,
|
||||||
|
.content.status.error = error,
|
||||||
|
};
|
||||||
|
|
||||||
|
return expansion_send_frame(instance, &frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
expansion_send_data_response(Expansion* instance, const uint8_t* data, size_t data_size) {
|
||||||
|
furi_assert(data_size <= EXPANSION_PROTOCOL_MAX_DATA_SIZE);
|
||||||
|
|
||||||
|
ExpansionFrame frame = {
|
||||||
|
.header.type = ExpansionFrameTypeData,
|
||||||
|
.content.data.size = data_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
memcpy(frame.content.data.bytes, data, data_size);
|
||||||
|
return expansion_send_frame(instance, &frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called in Rpc session thread context
|
||||||
|
static void expansion_rpc_send_callback(void* context, uint8_t* data, size_t data_size) {
|
||||||
|
Expansion* instance = context;
|
||||||
|
|
||||||
|
for(size_t sent_data_size = 0; sent_data_size < data_size;) {
|
||||||
|
if(furi_semaphore_acquire(
|
||||||
|
instance->tx_semaphore, furi_ms_to_ticks(EXPANSION_PROTOCOL_TIMEOUT_MS)) !=
|
||||||
|
FuriStatusOk) {
|
||||||
|
furi_thread_flags_set(furi_thread_get_id(instance->worker_thread), ExpansionFlagError);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t current_data_size =
|
||||||
|
MIN(data_size - sent_data_size, EXPANSION_PROTOCOL_MAX_DATA_SIZE);
|
||||||
|
if(!expansion_send_data_response(instance, data + sent_data_size, current_data_size))
|
||||||
|
break;
|
||||||
|
sent_data_size += current_data_size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool expansion_rpc_session_open(Expansion* instance) {
|
||||||
|
Rpc* rpc = furi_record_open(RECORD_RPC);
|
||||||
|
instance->rpc_session = rpc_session_open(rpc, RpcOwnerUart);
|
||||||
|
|
||||||
|
if(instance->rpc_session) {
|
||||||
|
instance->tx_semaphore = furi_semaphore_alloc(1, 1);
|
||||||
|
rpc_session_set_context(instance->rpc_session, instance);
|
||||||
|
rpc_session_set_send_bytes_callback(instance->rpc_session, expansion_rpc_send_callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance->rpc_session != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void expansion_rpc_session_close(Expansion* instance) {
|
||||||
|
if(instance->rpc_session) {
|
||||||
|
rpc_session_close(instance->rpc_session);
|
||||||
|
furi_semaphore_free(instance->tx_semaphore);
|
||||||
|
}
|
||||||
|
|
||||||
|
furi_record_close(RECORD_RPC);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
expansion_handle_session_state_handshake(Expansion* instance, const ExpansionFrame* rx_frame) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(rx_frame->header.type != ExpansionFrameTypeBaudRate) break;
|
||||||
|
const uint32_t baud_rate = rx_frame->content.baud_rate.baud;
|
||||||
|
|
||||||
|
FURI_LOG_D(TAG, "Proposed baud rate: %lu", baud_rate);
|
||||||
|
|
||||||
|
if(furi_hal_serial_is_baud_rate_supported(instance->serial_handle, baud_rate)) {
|
||||||
|
instance->session_state = ExpansionSessionStateConnected;
|
||||||
|
// Send response at previous baud rate
|
||||||
|
if(!expansion_send_status_response(instance, ExpansionFrameErrorNone)) break;
|
||||||
|
furi_hal_serial_set_br(instance->serial_handle, baud_rate);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if(!expansion_send_status_response(instance, ExpansionFrameErrorBaudRate)) break;
|
||||||
|
FURI_LOG_E(TAG, "Bad baud rate");
|
||||||
|
}
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
expansion_handle_session_state_connected(Expansion* instance, const ExpansionFrame* rx_frame) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(rx_frame->header.type == ExpansionFrameTypeControl) {
|
||||||
|
if(rx_frame->content.control.command != ExpansionFrameControlCommandStartRpc) break;
|
||||||
|
instance->session_state = ExpansionSessionStateRpcActive;
|
||||||
|
if(!expansion_rpc_session_open(instance)) break;
|
||||||
|
if(!expansion_send_status_response(instance, ExpansionFrameErrorNone)) break;
|
||||||
|
|
||||||
|
} else if(rx_frame->header.type == ExpansionFrameTypeHeartbeat) {
|
||||||
|
if(!expansion_send_heartbeat(instance)) break;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
expansion_handle_session_state_rpc_active(Expansion* instance, const ExpansionFrame* rx_frame) {
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if(rx_frame->header.type == ExpansionFrameTypeData) {
|
||||||
|
if(!expansion_send_status_response(instance, ExpansionFrameErrorNone)) break;
|
||||||
|
|
||||||
|
const size_t size_consumed = rpc_session_feed(
|
||||||
|
instance->rpc_session,
|
||||||
|
rx_frame->content.data.bytes,
|
||||||
|
rx_frame->content.data.size,
|
||||||
|
EXPANSION_PROTOCOL_TIMEOUT_MS);
|
||||||
|
if(size_consumed != rx_frame->content.data.size) break;
|
||||||
|
|
||||||
|
} else if(rx_frame->header.type == ExpansionFrameTypeControl) {
|
||||||
|
if(rx_frame->content.control.command != ExpansionFrameControlCommandStopRpc) break;
|
||||||
|
instance->session_state = ExpansionSessionStateConnected;
|
||||||
|
expansion_rpc_session_close(instance);
|
||||||
|
if(!expansion_send_status_response(instance, ExpansionFrameErrorNone)) break;
|
||||||
|
|
||||||
|
} else if(rx_frame->header.type == ExpansionFrameTypeStatus) {
|
||||||
|
if(rx_frame->content.status.error != ExpansionFrameErrorNone) break;
|
||||||
|
furi_semaphore_release(instance->tx_semaphore);
|
||||||
|
|
||||||
|
} else if(rx_frame->header.type == ExpansionFrameTypeHeartbeat) {
|
||||||
|
if(!expansion_send_heartbeat(instance)) break;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
success = true;
|
||||||
|
} while(false);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void expansion_state_machine(Expansion* instance) {
|
||||||
|
typedef bool (*ExpansionSessionStateHandler)(Expansion*, const ExpansionFrame*);
|
||||||
|
|
||||||
|
static const ExpansionSessionStateHandler expansion_handlers[] = {
|
||||||
|
[ExpansionSessionStateHandShake] = expansion_handle_session_state_handshake,
|
||||||
|
[ExpansionSessionStateConnected] = expansion_handle_session_state_connected,
|
||||||
|
[ExpansionSessionStateRpcActive] = expansion_handle_session_state_rpc_active,
|
||||||
|
};
|
||||||
|
|
||||||
|
ExpansionFrame rx_frame;
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
if(!expansion_receive_frame(instance, &rx_frame)) break;
|
||||||
|
if(!expansion_handlers[instance->session_state](instance, &rx_frame)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void expansion_worker_pending_callback(void* context, uint32_t arg) {
|
||||||
|
furi_assert(context);
|
||||||
|
UNUSED(arg);
|
||||||
|
|
||||||
|
Expansion* instance = context;
|
||||||
|
furi_thread_join(instance->worker_thread);
|
||||||
|
|
||||||
|
// Do not re-enable detection interrupt on user-requested exit
|
||||||
|
if(instance->exit_reason != ExpansionSessionExitReasonUser) {
|
||||||
|
furi_check(furi_mutex_acquire(instance->state_mutex, FuriWaitForever) == FuriStatusOk);
|
||||||
|
instance->state = ExpansionStateEnabled;
|
||||||
|
furi_hal_serial_control_set_expansion_callback(
|
||||||
|
instance->serial_id, expansion_detect_callback, instance);
|
||||||
|
furi_mutex_release(instance->state_mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t expansion_worker(void* context) {
|
||||||
|
furi_assert(context);
|
||||||
|
Expansion* instance = context;
|
||||||
|
|
||||||
|
furi_hal_power_insomnia_enter();
|
||||||
|
furi_hal_serial_control_set_expansion_callback(instance->serial_id, NULL, NULL);
|
||||||
|
|
||||||
|
instance->serial_handle = furi_hal_serial_control_acquire(instance->serial_id);
|
||||||
|
furi_check(instance->serial_handle);
|
||||||
|
|
||||||
|
FURI_LOG_D(TAG, "Service started");
|
||||||
|
|
||||||
|
instance->rx_buf = furi_stream_buffer_alloc(EXPANSION_BUFFER_SIZE, 1);
|
||||||
|
instance->session_state = ExpansionSessionStateHandShake;
|
||||||
|
instance->exit_reason = ExpansionSessionExitReasonUnknown;
|
||||||
|
|
||||||
|
furi_hal_serial_init(instance->serial_handle, EXPANSION_PROTOCOL_DEFAULT_BAUD_RATE);
|
||||||
|
|
||||||
|
furi_hal_serial_async_rx_start(
|
||||||
|
instance->serial_handle, expansion_serial_rx_callback, instance, false);
|
||||||
|
|
||||||
|
if(expansion_send_heartbeat(instance)) {
|
||||||
|
expansion_state_machine(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(instance->session_state == ExpansionSessionStateRpcActive) {
|
||||||
|
expansion_rpc_session_close(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
FURI_LOG_D(TAG, "Service stopped");
|
||||||
|
|
||||||
|
furi_hal_serial_control_release(instance->serial_handle);
|
||||||
|
furi_stream_buffer_free(instance->rx_buf);
|
||||||
|
|
||||||
|
furi_hal_power_insomnia_exit();
|
||||||
|
furi_timer_pending_callback(expansion_worker_pending_callback, instance, 0);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from the serial control thread
|
||||||
|
static void expansion_detect_callback(void* context) {
|
||||||
|
furi_assert(context);
|
||||||
|
Expansion* instance = context;
|
||||||
|
|
||||||
|
furi_check(furi_mutex_acquire(instance->state_mutex, FuriWaitForever) == FuriStatusOk);
|
||||||
|
|
||||||
|
if(instance->state == ExpansionStateEnabled) {
|
||||||
|
instance->state = ExpansionStateRunning;
|
||||||
|
furi_thread_start(instance->worker_thread);
|
||||||
|
}
|
||||||
|
|
||||||
|
furi_mutex_release(instance->state_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Expansion* expansion_alloc() {
|
||||||
|
Expansion* instance = malloc(sizeof(Expansion));
|
||||||
|
|
||||||
|
instance->state_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||||
|
instance->worker_thread = furi_thread_alloc_ex(TAG, 768, expansion_worker, instance);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void expansion_on_system_start(void* arg) {
|
||||||
|
UNUSED(arg);
|
||||||
|
|
||||||
|
Expansion* instance = expansion_alloc();
|
||||||
|
furi_record_create(RECORD_EXPANSION, instance);
|
||||||
|
|
||||||
|
ExpansionSettings settings = {};
|
||||||
|
if(!expansion_settings_load(&settings)) {
|
||||||
|
expansion_settings_save(&settings);
|
||||||
|
} else if(settings.uart_index < FuriHalSerialIdMax) {
|
||||||
|
expansion_enable(instance, settings.uart_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API functions
|
||||||
|
|
||||||
|
void expansion_enable(Expansion* instance, FuriHalSerialId serial_id) {
|
||||||
|
expansion_disable(instance);
|
||||||
|
|
||||||
|
furi_check(furi_mutex_acquire(instance->state_mutex, FuriWaitForever) == FuriStatusOk);
|
||||||
|
|
||||||
|
instance->serial_id = serial_id;
|
||||||
|
instance->state = ExpansionStateEnabled;
|
||||||
|
|
||||||
|
furi_hal_serial_control_set_expansion_callback(
|
||||||
|
instance->serial_id, expansion_detect_callback, instance);
|
||||||
|
|
||||||
|
furi_mutex_release(instance->state_mutex);
|
||||||
|
|
||||||
|
FURI_LOG_D(TAG, "Detection enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
void expansion_disable(Expansion* instance) {
|
||||||
|
furi_check(furi_mutex_acquire(instance->state_mutex, FuriWaitForever) == FuriStatusOk);
|
||||||
|
|
||||||
|
if(instance->state == ExpansionStateRunning) {
|
||||||
|
furi_thread_flags_set(furi_thread_get_id(instance->worker_thread), ExpansionFlagStop);
|
||||||
|
furi_thread_join(instance->worker_thread);
|
||||||
|
} else if(instance->state == ExpansionStateEnabled) {
|
||||||
|
FURI_LOG_D(TAG, "Detection disabled");
|
||||||
|
furi_hal_serial_control_set_expansion_callback(instance->serial_id, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
instance->state = ExpansionStateDisabled;
|
||||||
|
|
||||||
|
furi_mutex_release(instance->state_mutex);
|
||||||
|
}
|
50
applications/services/expansion/expansion.h
Normal file
50
applications/services/expansion/expansion.h
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* @file expansion.h
|
||||||
|
* @brief Expansion module support library.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <furi_hal_serial_types.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief FURI record key to access the expansion object.
|
||||||
|
*/
|
||||||
|
#define RECORD_EXPANSION "expansion"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Expansion opaque type declaration.
|
||||||
|
*/
|
||||||
|
typedef struct Expansion Expansion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enable support for expansion modules on designated serial port.
|
||||||
|
*
|
||||||
|
* Only one serial port can be used to communicate with an expansion
|
||||||
|
* module at a time.
|
||||||
|
*
|
||||||
|
* Calling this function when expansion module support is already enabled
|
||||||
|
* will first disable the previous setting, then enable the current one.
|
||||||
|
*
|
||||||
|
* @param[in,out] instance pointer to the Expansion instance.
|
||||||
|
* @param[in] serial_id numerical identifier of the serial.
|
||||||
|
*/
|
||||||
|
void expansion_enable(Expansion* instance, FuriHalSerialId serial_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Disable support for expansion modules.
|
||||||
|
*
|
||||||
|
* Calling this function will cease all communications with the
|
||||||
|
* expansion module (if any), release the serial handle and
|
||||||
|
* reset the respective pins to the default state.
|
||||||
|
*
|
||||||
|
* @param[in,out] instance pointer to the Expansion instance.
|
||||||
|
*/
|
||||||
|
void expansion_disable(Expansion* instance);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
338
applications/services/expansion/expansion_protocol.h
Normal file
338
applications/services/expansion/expansion_protocol.h
Normal file
|
@ -0,0 +1,338 @@
|
||||||
|
/**
|
||||||
|
* @file expansion_protocol.h
|
||||||
|
* @brief Flipper Expansion Protocol parser reference implementation.
|
||||||
|
*
|
||||||
|
* This file is licensed separately under The Unlicense.
|
||||||
|
* See https://unlicense.org/ for more details.
|
||||||
|
*
|
||||||
|
* This parser is written with low-spec hardware in mind. It does not use
|
||||||
|
* dynamic memory allocation or Flipper-specific libraries and can be
|
||||||
|
* included directly into any module's firmware's sources.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Default baud rate to start all communications at.
|
||||||
|
*/
|
||||||
|
#define EXPANSION_PROTOCOL_DEFAULT_BAUD_RATE (9600UL)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Maximum data size per frame, in bytes.
|
||||||
|
*/
|
||||||
|
#define EXPANSION_PROTOCOL_MAX_DATA_SIZE (64U)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Maximum allowed inactivity period, in milliseconds.
|
||||||
|
*/
|
||||||
|
#define EXPANSION_PROTOCOL_TIMEOUT_MS (250U)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Dead time after changing connection baud rate.
|
||||||
|
*/
|
||||||
|
#define EXPANSION_PROTOCOL_BAUD_CHANGE_DT_MS (25U)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enumeration of supported frame types.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
ExpansionFrameTypeHeartbeat = 1, /**< Heartbeat frame. */
|
||||||
|
ExpansionFrameTypeStatus = 2, /**< Status report frame. */
|
||||||
|
ExpansionFrameTypeBaudRate = 3, /**< Baud rate negotiation frame. */
|
||||||
|
ExpansionFrameTypeControl = 4, /**< Control frame. */
|
||||||
|
ExpansionFrameTypeData = 5, /**< Data frame. */
|
||||||
|
ExpansionFrameTypeReserved, /**< Special value. */
|
||||||
|
} ExpansionFrameType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enumeration of possible error types.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
ExpansionFrameErrorNone = 0x00, /**< No error occurred. */
|
||||||
|
ExpansionFrameErrorUnknown = 0x01, /**< An unknown error has occurred (generic response). */
|
||||||
|
ExpansionFrameErrorBaudRate = 0x02, /**< Requested baud rate is not supported. */
|
||||||
|
} ExpansionFrameError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enumeration of suported control commands.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
ExpansionFrameControlCommandStartRpc = 0x00, /**< Start an RPC session. */
|
||||||
|
ExpansionFrameControlCommandStopRpc = 0x01, /**< Stop an open RPC session. */
|
||||||
|
} ExpansionFrameControlCommand;
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Frame header structure.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t type; /**< Type of the frame. @see ExpansionFrameType. */
|
||||||
|
} ExpansionFrameHeader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Heartbeat frame contents.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/** Empty. */
|
||||||
|
} ExpansionFrameHeartbeat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Status frame contents.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t error; /**< Reported error code. @see ExpansionFrameError. */
|
||||||
|
} ExpansionFrameStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Baud rate frame contents.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint32_t baud; /**< Requested baud rate. */
|
||||||
|
} ExpansionFrameBaudRate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Control frame contents.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t command; /**< Control command number. @see ExpansionFrameControlCommand. */
|
||||||
|
} ExpansionFrameControl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Data frame contents.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/** Size of the data. Must be less than EXPANSION_PROTOCOL_MAX_DATA_SIZE. */
|
||||||
|
uint8_t size;
|
||||||
|
/** Data bytes. Valid only up to ExpansionFrameData::size bytes. */
|
||||||
|
uint8_t bytes[EXPANSION_PROTOCOL_MAX_DATA_SIZE];
|
||||||
|
} ExpansionFrameData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Expansion protocol frame structure.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
ExpansionFrameHeader header; /**< Header of the frame. Required. */
|
||||||
|
union {
|
||||||
|
ExpansionFrameHeartbeat heartbeat; /**< Heartbeat frame contents. */
|
||||||
|
ExpansionFrameStatus status; /**< Status frame contents. */
|
||||||
|
ExpansionFrameBaudRate baud_rate; /**< Baud rate frame contents. */
|
||||||
|
ExpansionFrameControl control; /**< Control frame contents. */
|
||||||
|
ExpansionFrameData data; /**< Data frame contents. */
|
||||||
|
} content; /**< Contents of the frame. */
|
||||||
|
} ExpansionFrame;
|
||||||
|
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Expansion checksum type.
|
||||||
|
*/
|
||||||
|
typedef uint8_t ExpansionFrameChecksum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Receive function type declaration.
|
||||||
|
*
|
||||||
|
* @see expansion_frame_decode().
|
||||||
|
*
|
||||||
|
* @param[out] data pointer to the buffer to reveive the data into.
|
||||||
|
* @param[in] data_size maximum output buffer capacity, in bytes.
|
||||||
|
* @param[in,out] context pointer to a user-defined context object.
|
||||||
|
* @returns number of bytes written into the output buffer.
|
||||||
|
*/
|
||||||
|
typedef size_t (*ExpansionFrameReceiveCallback)(uint8_t* data, size_t data_size, void* context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send function type declaration.
|
||||||
|
*
|
||||||
|
* @see expansion_frame_encode().
|
||||||
|
*
|
||||||
|
* @param[in] data pointer to the buffer containing the data to be sent.
|
||||||
|
* @param[in] data_size size of the data to send, in bytes.
|
||||||
|
* @param[in,out] context pointer to a user-defined context object.
|
||||||
|
* @returns number of bytes actually sent.
|
||||||
|
*/
|
||||||
|
typedef size_t (*ExpansionFrameSendCallback)(const uint8_t* data, size_t data_size, void* context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get encoded frame size.
|
||||||
|
*
|
||||||
|
* The frame MUST be complete and properly formed.
|
||||||
|
*
|
||||||
|
* @param[in] frame pointer to the frame to be evaluated.
|
||||||
|
* @returns encoded frame size, in bytes.
|
||||||
|
*/
|
||||||
|
static inline size_t expansion_frame_get_encoded_size(const ExpansionFrame* frame) {
|
||||||
|
switch(frame->header.type) {
|
||||||
|
case ExpansionFrameTypeHeartbeat:
|
||||||
|
return sizeof(frame->header);
|
||||||
|
case ExpansionFrameTypeStatus:
|
||||||
|
return sizeof(frame->header) + sizeof(frame->content.status);
|
||||||
|
case ExpansionFrameTypeBaudRate:
|
||||||
|
return sizeof(frame->header) + sizeof(frame->content.baud_rate);
|
||||||
|
case ExpansionFrameTypeControl:
|
||||||
|
return sizeof(frame->header) + sizeof(frame->content.control);
|
||||||
|
case ExpansionFrameTypeData:
|
||||||
|
return sizeof(frame->header) + sizeof(frame->content.data.size) + frame->content.data.size;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get remaining number of bytes needed to properly decode a frame.
|
||||||
|
*
|
||||||
|
* The return value will vary depending on the received_size parameter value.
|
||||||
|
* The frame is considered complete when the function returns 0.
|
||||||
|
*
|
||||||
|
* @param[in] frame pointer to the frame to be evaluated.
|
||||||
|
* @param[in] received_size number of bytes currently availabe for evaluation.
|
||||||
|
* @returns number of bytes needed for a complete frame.
|
||||||
|
*/
|
||||||
|
static inline size_t
|
||||||
|
expansion_frame_get_remaining_size(const ExpansionFrame* frame, size_t received_size) {
|
||||||
|
if(received_size < sizeof(ExpansionFrameHeader)) return sizeof(ExpansionFrameHeader);
|
||||||
|
|
||||||
|
const size_t received_content_size = received_size - sizeof(ExpansionFrameHeader);
|
||||||
|
size_t content_size;
|
||||||
|
|
||||||
|
switch(frame->header.type) {
|
||||||
|
case ExpansionFrameTypeHeartbeat:
|
||||||
|
content_size = 0;
|
||||||
|
break;
|
||||||
|
case ExpansionFrameTypeStatus:
|
||||||
|
content_size = sizeof(frame->content.status);
|
||||||
|
break;
|
||||||
|
case ExpansionFrameTypeBaudRate:
|
||||||
|
content_size = sizeof(frame->content.baud_rate);
|
||||||
|
break;
|
||||||
|
case ExpansionFrameTypeControl:
|
||||||
|
content_size = sizeof(frame->content.control);
|
||||||
|
break;
|
||||||
|
case ExpansionFrameTypeData:
|
||||||
|
if(received_content_size < sizeof(frame->content.data.size)) {
|
||||||
|
content_size = sizeof(frame->content.data.size);
|
||||||
|
} else {
|
||||||
|
content_size = sizeof(frame->content.data.size) + frame->content.data.size;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return SIZE_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content_size > received_content_size ? content_size - received_content_size : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enumeration of protocol parser statuses.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
ExpansionProtocolStatusOk, /**< No error has occurred. */
|
||||||
|
ExpansionProtocolStatusErrorFormat, /**< Invalid frame type. */
|
||||||
|
ExpansionProtocolStatusErrorChecksum, /**< Checksum mismatch. */
|
||||||
|
ExpansionProtocolStatusErrorCommunication, /**< Input/output error. */
|
||||||
|
} ExpansionProtocolStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the checksum byte corresponding to the frame
|
||||||
|
*
|
||||||
|
* Lightweight XOR checksum algorithm for basic error detection.
|
||||||
|
*
|
||||||
|
* @param[in] data pointer to a byte buffer containing the data.
|
||||||
|
* @param[in] data_size size of the data buffer.
|
||||||
|
* @returns checksum byte of the frame.
|
||||||
|
*/
|
||||||
|
static inline ExpansionFrameChecksum
|
||||||
|
expansion_protocol_get_checksum(const uint8_t* data, size_t data_size) {
|
||||||
|
ExpansionFrameChecksum checksum = 0;
|
||||||
|
for(size_t i = 0; i < data_size; ++i) {
|
||||||
|
checksum ^= data[i];
|
||||||
|
}
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Receive and decode a frame.
|
||||||
|
*
|
||||||
|
* Will repeatedly call the receive callback function until enough data is received.
|
||||||
|
*
|
||||||
|
* @param[out] frame pointer to the frame to contain decoded data.
|
||||||
|
* @param[in] receive pointer to the function used to receive data.
|
||||||
|
* @param[in,out] context pointer to a user-defined context object. Will be passed to the receive callback function.
|
||||||
|
* @returns ExpansionProtocolStatusOk on success, any other error code on failure.
|
||||||
|
*/
|
||||||
|
static inline ExpansionProtocolStatus expansion_protocol_decode(
|
||||||
|
ExpansionFrame* frame,
|
||||||
|
ExpansionFrameReceiveCallback receive,
|
||||||
|
void* context) {
|
||||||
|
size_t total_size = 0;
|
||||||
|
size_t remaining_size;
|
||||||
|
|
||||||
|
while(true) {
|
||||||
|
remaining_size = expansion_frame_get_remaining_size(frame, total_size);
|
||||||
|
|
||||||
|
if(remaining_size == SIZE_MAX) {
|
||||||
|
return ExpansionProtocolStatusErrorFormat;
|
||||||
|
} else if(remaining_size == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t received_size =
|
||||||
|
receive((uint8_t*)frame + total_size, remaining_size, context);
|
||||||
|
|
||||||
|
if(received_size == 0) {
|
||||||
|
return ExpansionProtocolStatusErrorCommunication;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_size += received_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpansionFrameChecksum checksum;
|
||||||
|
const size_t received_size = receive(&checksum, sizeof(checksum), context);
|
||||||
|
|
||||||
|
if(received_size != sizeof(checksum)) {
|
||||||
|
return ExpansionProtocolStatusErrorCommunication;
|
||||||
|
} else if(checksum != expansion_protocol_get_checksum((const uint8_t*)frame, total_size)) {
|
||||||
|
return ExpansionProtocolStatusErrorChecksum;
|
||||||
|
} else {
|
||||||
|
return ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Encode and send a frame.
|
||||||
|
*
|
||||||
|
* @param[in] frame pointer to the frame to be encoded and sent.
|
||||||
|
* @param[in] send pointer to the function used to send data.
|
||||||
|
* @param[in,out] context pointer to a user-defined context object. Will be passed to the send callback function.
|
||||||
|
* @returns ExpansionProtocolStatusOk on success, any other error code on failure.
|
||||||
|
*/
|
||||||
|
static inline ExpansionProtocolStatus expansion_protocol_encode(
|
||||||
|
const ExpansionFrame* frame,
|
||||||
|
ExpansionFrameSendCallback send,
|
||||||
|
void* context) {
|
||||||
|
const size_t encoded_size = expansion_frame_get_encoded_size(frame);
|
||||||
|
if(encoded_size == 0) {
|
||||||
|
return ExpansionProtocolStatusErrorFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpansionFrameChecksum checksum =
|
||||||
|
expansion_protocol_get_checksum((const uint8_t*)frame, encoded_size);
|
||||||
|
|
||||||
|
if((send((const uint8_t*)frame, encoded_size, context) != encoded_size) ||
|
||||||
|
(send(&checksum, sizeof(checksum), context) != sizeof(checksum))) {
|
||||||
|
return ExpansionProtocolStatusErrorCommunication;
|
||||||
|
} else {
|
||||||
|
return ExpansionProtocolStatusOk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
30
applications/services/expansion/expansion_settings.c
Normal file
30
applications/services/expansion/expansion_settings.c
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#include "expansion_settings.h"
|
||||||
|
|
||||||
|
#include <storage/storage.h>
|
||||||
|
#include <toolbox/saved_struct.h>
|
||||||
|
|
||||||
|
#include "expansion_settings_filename.h"
|
||||||
|
|
||||||
|
#define EXPANSION_SETTINGS_PATH INT_PATH(EXPANSION_SETTINGS_FILE_NAME)
|
||||||
|
#define EXPANSION_SETTINGS_VERSION (0)
|
||||||
|
#define EXPANSION_SETTINGS_MAGIC (0xEA)
|
||||||
|
|
||||||
|
bool expansion_settings_load(ExpansionSettings* settings) {
|
||||||
|
furi_assert(settings);
|
||||||
|
return saved_struct_load(
|
||||||
|
EXPANSION_SETTINGS_PATH,
|
||||||
|
settings,
|
||||||
|
sizeof(ExpansionSettings),
|
||||||
|
EXPANSION_SETTINGS_MAGIC,
|
||||||
|
EXPANSION_SETTINGS_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool expansion_settings_save(ExpansionSettings* settings) {
|
||||||
|
furi_assert(settings);
|
||||||
|
return saved_struct_save(
|
||||||
|
EXPANSION_SETTINGS_PATH,
|
||||||
|
settings,
|
||||||
|
sizeof(ExpansionSettings),
|
||||||
|
EXPANSION_SETTINGS_MAGIC,
|
||||||
|
EXPANSION_SETTINGS_VERSION);
|
||||||
|
}
|
43
applications/services/expansion/expansion_settings.h
Normal file
43
applications/services/expansion/expansion_settings.h
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* @file expansion_settings.h
|
||||||
|
* @brief Expansion module support settings.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Expansion module support settings storage type.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
/**
|
||||||
|
* Numerical index of serial port used to communicate
|
||||||
|
* with expansion modules.
|
||||||
|
*/
|
||||||
|
uint8_t uart_index;
|
||||||
|
} ExpansionSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load expansion module support settings from file.
|
||||||
|
*
|
||||||
|
* @param[out] settings pointer to an ExpansionSettings instance to load settings into.
|
||||||
|
* @returns true if the settings were successfully loaded, false otherwise.
|
||||||
|
*/
|
||||||
|
bool expansion_settings_load(ExpansionSettings* settings);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Save expansion module support settings to file.
|
||||||
|
*
|
||||||
|
* @param[in] settings pointer to an ExpansionSettings instance to save settings from.
|
||||||
|
* @returns true if the settings were successfully saved, false otherwise.
|
||||||
|
*/
|
||||||
|
bool expansion_settings_save(ExpansionSettings* settings);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* @file expansion_settings_filename.h
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief File name used for expansion settings.
|
||||||
|
*/
|
||||||
|
#define EXPANSION_SETTINGS_FILE_NAME ".expansion.settings"
|
|
@ -160,8 +160,11 @@ void rpc_session_set_terminated_callback(
|
||||||
* command is gets processed - it's safe either. But case of it is quite
|
* command is gets processed - it's safe either. But case of it is quite
|
||||||
* odd: client sends close request and sends command after.
|
* odd: client sends close request and sends command after.
|
||||||
*/
|
*/
|
||||||
size_t
|
size_t rpc_session_feed(
|
||||||
rpc_session_feed(RpcSession* session, uint8_t* encoded_bytes, size_t size, uint32_t timeout) {
|
RpcSession* session,
|
||||||
|
const uint8_t* encoded_bytes,
|
||||||
|
size_t size,
|
||||||
|
uint32_t timeout) {
|
||||||
furi_assert(session);
|
furi_assert(session);
|
||||||
furi_assert(encoded_bytes);
|
furi_assert(encoded_bytes);
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ typedef enum {
|
||||||
RpcOwnerUnknown = 0,
|
RpcOwnerUnknown = 0,
|
||||||
RpcOwnerBle,
|
RpcOwnerBle,
|
||||||
RpcOwnerUsb,
|
RpcOwnerUsb,
|
||||||
|
RpcOwnerUart,
|
||||||
RpcOwnerCount,
|
RpcOwnerCount,
|
||||||
} RpcOwner;
|
} RpcOwner;
|
||||||
|
|
||||||
|
@ -124,7 +125,7 @@ void rpc_session_set_terminated_callback(
|
||||||
*
|
*
|
||||||
* @return actually consumed bytes
|
* @return actually consumed bytes
|
||||||
*/
|
*/
|
||||||
size_t rpc_session_feed(RpcSession* session, uint8_t* buffer, size_t size, uint32_t timeout);
|
size_t rpc_session_feed(RpcSession* session, const uint8_t* buffer, size_t size, uint32_t timeout);
|
||||||
|
|
||||||
/** Get available size of RPC buffer
|
/** Get available size of RPC buffer
|
||||||
*
|
*
|
||||||
|
|
|
@ -265,7 +265,7 @@ static void rpc_system_gui_virtual_display_input_callback(InputEvent* event, voi
|
||||||
RpcGuiSystem* rpc_gui = context;
|
RpcGuiSystem* rpc_gui = context;
|
||||||
RpcSession* session = rpc_gui->session;
|
RpcSession* session = rpc_gui->session;
|
||||||
|
|
||||||
FURI_LOG_D(TAG, "VirtulDisplay: SendInputEvent");
|
FURI_LOG_D(TAG, "VirtualDisplay: SendInputEvent");
|
||||||
|
|
||||||
PB_Main rpc_message = {
|
PB_Main rpc_message = {
|
||||||
.command_id = 0,
|
.command_id = 0,
|
||||||
|
@ -317,7 +317,7 @@ static void rpc_system_gui_start_virtual_display_process(const PB_Main* request,
|
||||||
rpc_gui);
|
rpc_gui);
|
||||||
|
|
||||||
if(request->content.gui_start_virtual_display_request.send_input) {
|
if(request->content.gui_start_virtual_display_request.send_input) {
|
||||||
FURI_LOG_D(TAG, "VirtulDisplay: input forwarding requested");
|
FURI_LOG_D(TAG, "VirtualDisplay: input forwarding requested");
|
||||||
view_port_input_callback_set(
|
view_port_input_callback_set(
|
||||||
rpc_gui->virtual_display_view_port,
|
rpc_gui->virtual_display_view_port,
|
||||||
rpc_system_gui_virtual_display_input_callback,
|
rpc_system_gui_virtual_display_input_callback,
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
App(
|
||||||
|
appid="expansion_settings",
|
||||||
|
name="Expansion Modules",
|
||||||
|
apptype=FlipperAppType.SETTINGS,
|
||||||
|
entry_point="expansion_settings_app",
|
||||||
|
requires=["gui"],
|
||||||
|
stack_size=1 * 1024,
|
||||||
|
order=80,
|
||||||
|
)
|
|
@ -0,0 +1,91 @@
|
||||||
|
#include "expansion_settings_app.h"
|
||||||
|
|
||||||
|
static const char* const expansion_uart_text[] = {
|
||||||
|
"USART",
|
||||||
|
"LPUART",
|
||||||
|
"None",
|
||||||
|
};
|
||||||
|
|
||||||
|
static void expansion_settings_app_uart_changed(VariableItem* item) {
|
||||||
|
ExpansionSettingsApp* app = variable_item_get_context(item);
|
||||||
|
const uint8_t index = variable_item_get_current_value_index(item);
|
||||||
|
variable_item_set_current_value_text(item, expansion_uart_text[index]);
|
||||||
|
app->settings.uart_index = index;
|
||||||
|
|
||||||
|
if(index < FuriHalSerialIdMax) {
|
||||||
|
expansion_enable(app->expansion, index);
|
||||||
|
} else {
|
||||||
|
expansion_disable(app->expansion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t expansion_settings_app_exit(void* context) {
|
||||||
|
UNUSED(context);
|
||||||
|
return VIEW_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ExpansionSettingsApp* expansion_settings_app_alloc() {
|
||||||
|
ExpansionSettingsApp* app = malloc(sizeof(ExpansionSettingsApp));
|
||||||
|
|
||||||
|
if(!expansion_settings_load(&app->settings)) {
|
||||||
|
expansion_settings_save(&app->settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
app->gui = furi_record_open(RECORD_GUI);
|
||||||
|
app->expansion = furi_record_open(RECORD_EXPANSION);
|
||||||
|
|
||||||
|
app->view_dispatcher = view_dispatcher_alloc();
|
||||||
|
view_dispatcher_enable_queue(app->view_dispatcher);
|
||||||
|
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||||
|
|
||||||
|
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||||
|
|
||||||
|
app->var_item_list = variable_item_list_alloc();
|
||||||
|
|
||||||
|
VariableItem* item;
|
||||||
|
uint8_t value_index;
|
||||||
|
|
||||||
|
item = variable_item_list_add(
|
||||||
|
app->var_item_list,
|
||||||
|
"Listen UART",
|
||||||
|
COUNT_OF(expansion_uart_text),
|
||||||
|
expansion_settings_app_uart_changed,
|
||||||
|
app);
|
||||||
|
value_index = app->settings.uart_index;
|
||||||
|
variable_item_set_current_value_index(item, value_index);
|
||||||
|
variable_item_set_current_value_text(item, expansion_uart_text[value_index]);
|
||||||
|
|
||||||
|
view_set_previous_callback(
|
||||||
|
variable_item_list_get_view(app->var_item_list), expansion_settings_app_exit);
|
||||||
|
view_dispatcher_add_view(
|
||||||
|
app->view_dispatcher,
|
||||||
|
ExpansionSettingsViewVarItemList,
|
||||||
|
variable_item_list_get_view(app->var_item_list));
|
||||||
|
|
||||||
|
view_dispatcher_switch_to_view(app->view_dispatcher, ExpansionSettingsViewVarItemList);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void expansion_settings_app_free(ExpansionSettingsApp* app) {
|
||||||
|
furi_assert(app);
|
||||||
|
|
||||||
|
expansion_settings_save(&app->settings);
|
||||||
|
|
||||||
|
view_dispatcher_remove_view(app->view_dispatcher, ExpansionSettingsViewVarItemList);
|
||||||
|
variable_item_list_free(app->var_item_list);
|
||||||
|
view_dispatcher_free(app->view_dispatcher);
|
||||||
|
|
||||||
|
furi_record_close(RECORD_EXPANSION);
|
||||||
|
furi_record_close(RECORD_GUI);
|
||||||
|
|
||||||
|
free(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t expansion_settings_app(void* p) {
|
||||||
|
UNUSED(p);
|
||||||
|
ExpansionSettingsApp* app = expansion_settings_app_alloc();
|
||||||
|
view_dispatcher_run(app->view_dispatcher);
|
||||||
|
expansion_settings_app_free(app);
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <furi.h>
|
||||||
|
#include <furi_hal.h>
|
||||||
|
|
||||||
|
#include <gui/gui.h>
|
||||||
|
#include <gui/view_dispatcher.h>
|
||||||
|
#include <gui/modules/variable_item_list.h>
|
||||||
|
|
||||||
|
#include <expansion/expansion.h>
|
||||||
|
#include <expansion/expansion_settings.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Gui* gui;
|
||||||
|
ViewDispatcher* view_dispatcher;
|
||||||
|
VariableItemList* var_item_list;
|
||||||
|
Expansion* expansion;
|
||||||
|
ExpansionSettings settings;
|
||||||
|
} ExpansionSettingsApp;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
ExpansionSettingsViewVarItemList,
|
||||||
|
} ExpansionSettingsView;
|
164
documentation/ExpansionModules.md
Normal file
164
documentation/ExpansionModules.md
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
# Expansion Module Protocol - Draft
|
||||||
|
|
||||||
|
## Terms and definitions
|
||||||
|
|
||||||
|
- Expansion Module: A third-party hardware unit meant for use with Flipper Zero by connecting it to its GPIO header.
|
||||||
|
- Expansion Module Protocol: A serial-based, byte-oriented, synchronous communication protocol described in this document.
|
||||||
|
- Host: Hardware unit tasked with serving requests. Used interchangeably with Flipper, Server, Host etc. throughout this document.
|
||||||
|
- Device: Used interchangeably with Expansion Module, Module, Client, etc.
|
||||||
|
- RPC: Remote Procedure Call, a protobuf-based communication protocol widely used by Flipper Zero companion applications.
|
||||||
|
- Timeout Interval: Period of inactivity to be treated as a loss of connection, also denoted as Tto. Equals to 250 ms.
|
||||||
|
- Baud Rate Switch Dead Time: Period of time after baud rate change during which no communication is allowed, also denoted Tdt. Equals to 25 ms.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Automatic expansion module detection
|
||||||
|
- Baud rate negotiation
|
||||||
|
- Basic error detection
|
||||||
|
- Request-response communication flow
|
||||||
|
- Integration with Flipper RPC protocol
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
Depending on the UART selected for communication, the following pins area available for the expansion modules to connect to:
|
||||||
|
|
||||||
|
| UART | Tx pin | Rx pin |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| USART | 13 | 14 |
|
||||||
|
| LPUART | 15 | 16 |
|
||||||
|
|
||||||
|
## Frame structure
|
||||||
|
|
||||||
|
Each frame consists of a header (1 byte), contents (size depends of frame type) and checksum (1 byte) fields:
|
||||||
|
|
||||||
|
| Header (1 byte) | Contents (0 or more bytes) | Checksum (1 byte) |
|
||||||
|
|-----------------|----------------------------|-------------------|
|
||||||
|
| Frame type | Frame payload | XOR checksum |
|
||||||
|
|
||||||
|
### Heartbeat frame
|
||||||
|
|
||||||
|
HEARTBEAT frames are used to maintain an idle connection. In the event of not receiving any frames within Tto, either side must cease all communications and be ready to initiate the connection again.
|
||||||
|
|
||||||
|
| Header (1 byte) | Checksum (1 byte) |
|
||||||
|
|-----------------|-------------------|
|
||||||
|
| 0x01 | XOR checksum |
|
||||||
|
|
||||||
|
Note that the contents field is not present (0 bytes length).
|
||||||
|
|
||||||
|
### Status frame
|
||||||
|
|
||||||
|
STATUS frames are used to report the status of a transaction. Every received frame MUST be confirmed by a matching STATUS response.
|
||||||
|
|
||||||
|
| Header (1 byte) | Contents (1 byte) | Checksum (1 byte) |
|
||||||
|
|-----------------|-------------------|-------------------|
|
||||||
|
| 0x02 | Error code | XOR checksum |
|
||||||
|
|
||||||
|
The `Error code` field SHALL have one of the following values:
|
||||||
|
|
||||||
|
| Error code | Meaning |
|
||||||
|
|------------|-------------------------|
|
||||||
|
| 0x00 | OK (No error) |
|
||||||
|
| 0x01 | Unknown error |
|
||||||
|
| 0x02 | Baud rate not supported |
|
||||||
|
|
||||||
|
### Baud rate frame
|
||||||
|
|
||||||
|
BAUD RATE frames are used to negotiate communication speed. The initial connection SHALL always happen at 9600 baud. The first message sent by the module MUST be a BAUD RATE frame, even if a different speed is not required.
|
||||||
|
|
||||||
|
| Header (1 byte) | Contents (4 bytes) | Checksum (1 byte) |
|
||||||
|
|-----------------|--------------------|-------------------|
|
||||||
|
| 0x03 | Baud rate | XOR checksum |
|
||||||
|
|
||||||
|
If the requested baud rate is supported by the host, it SHALL respond with a STATUS frame with an OK error code, otherwise the error code SHALL be 0x02 (Baud rate not supported). Until the negotiation succeeds, the speed SHALL remain at 9600 baud. The module MAY send additional BAUD RATE frames with alternative speeds in case the initial request was refused. No other frames are allowed until the speed negotiation succeeds.
|
||||||
|
|
||||||
|
### Control frame
|
||||||
|
|
||||||
|
CONTROL frames are used to control various aspects of the communication. As of now, the sole purpose of CONTROL frames is to start and stop the RPC session.
|
||||||
|
|
||||||
|
| Header (1 byte) | Contents (1 byte) | Checksum (1 byte) |
|
||||||
|
|-----------------|-------------------|-------------------|
|
||||||
|
| 0x04 | Command | XOR checksum |
|
||||||
|
|
||||||
|
The `Command` field SHALL have one of the followind values:
|
||||||
|
|
||||||
|
| Command | Meaning |
|
||||||
|
|---------|-------------------|
|
||||||
|
| 0x00 | Start RPC session |
|
||||||
|
| 0x01 | Stop RPC session |
|
||||||
|
|
||||||
|
### Data frame
|
||||||
|
|
||||||
|
DATA frames are used to transmit arbitrary data in either direction. Each DATA frame can hold up to 64 bytes. If an RPC session is curretly open, all received bytes are forwarded to it.
|
||||||
|
|
||||||
|
| Header (1 byte) | Contents (1 to 65 byte(s)) | Checksum (1 byte) |
|
||||||
|
|-----------------|----------------------------|-------------------|
|
||||||
|
| 0x05 | Data | XOR checksum |
|
||||||
|
|
||||||
|
The `Data` field SHALL have the following structure:
|
||||||
|
|
||||||
|
| Data size (1 byte) | Data (0 to 64 bytes) |
|
||||||
|
|--------------------|----------------------|
|
||||||
|
| 0x00 ... 0x40 | Arbitrary data |
|
||||||
|
|
||||||
|
## Communication flow
|
||||||
|
|
||||||
|
In order for the host to be able to detect the module, the respective feature must be enabled first. This can be done via the GUI by going to `Settings -> Expansion Modules` and selecting the required `Listen UART` or programmatically by calling `expansion_enable()`. Likewise, disabling this feature via the same GUI or by calling `expansion_disable()` will result in ceasing all communications and not being able to detect any connected modules.
|
||||||
|
|
||||||
|
The communication is always initiated by the module by the means of shortly pulling the RX pin down. The host SHALL respond with a HEARTBEAT frame indicating that it is ready to receive requests. The module then MUST issue a BAUDRATE request within Tto. Failure to do so will result in the host dropping the connection and returning to its initial state.
|
||||||
|
|
||||||
|
```
|
||||||
|
MODULE | FLIPPER
|
||||||
|
-----------------------------+---------------------------
|
||||||
|
| (Start)
|
||||||
|
Pull down RX -->
|
||||||
|
<-- Heartbeat
|
||||||
|
Baud Rate -->
|
||||||
|
<-- Status [OK | Error]
|
||||||
|
|
|
||||||
|
(Module changes baud rate | (Flipper changes
|
||||||
|
and waits for Tdt) | baud rate)
|
||||||
|
|
|
||||||
|
Control [Start RPC] -->
|
||||||
|
<-- Status [OK | Error]
|
||||||
|
-----------------------------+--------------------------- (1)
|
||||||
|
Data [RPC Request] -->
|
||||||
|
<-- Status [OK | Error]
|
||||||
|
<-- Data [RPC Response]
|
||||||
|
Status [OK | Error] -->
|
||||||
|
-----------------------------+--------------------------- (2)
|
||||||
|
Data [RPC Request pt.1] -->
|
||||||
|
<-- Status [OK | Error]
|
||||||
|
Data [RPC Request pt.2] -->
|
||||||
|
<-- Status [OK | Error]
|
||||||
|
Data [RPC Request pt.3] -->
|
||||||
|
<-- Status [OK | Error]
|
||||||
|
<-- Data [RPC Response]
|
||||||
|
Status [OK | Error] -->
|
||||||
|
-----------------------------+--------------------------- (3)
|
||||||
|
Heartbeat -->
|
||||||
|
<-- Heartbeat
|
||||||
|
Heartbeat -->
|
||||||
|
<-- Heartbeat
|
||||||
|
-----------------------------+---------------------------
|
||||||
|
Control [Stop RPC] -->
|
||||||
|
<-- Status [OK | Error]
|
||||||
|
(Module disconnected) |
|
||||||
|
| (No activity within Tto
|
||||||
|
| return to start)
|
||||||
|
|
||||||
|
(1) The module MUST confirm all implicitly requested frames (e.g. DATA frames containing RPC responses) with a STATUS frame.
|
||||||
|
(2) RPC requests larger than 64 bytes are split into multiple frames. Every DATA frame MUST be confirmed with a STATUS frame.
|
||||||
|
(3) When the module has no data to send, it MUST send HEARTBEAT frames with a period < Tto in order to maintain the connection.
|
||||||
|
The host SHALL respond with a HEARTBEAT frame each time.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error detection
|
||||||
|
|
||||||
|
Error detection is implemented via adding an extra checksum byte to every frame (see above).
|
||||||
|
|
||||||
|
The checksum is calculated by bitwise XOR-ing every byte in the frame (excluding the checksum byte itself), with an initial value of 0.
|
||||||
|
|
||||||
|
### Error recovery behaviour
|
||||||
|
|
||||||
|
In the event of a detected error, the concerned side MUST cease all communications and reset to initial state. The other side will then experience
|
||||||
|
a communication timeout and the connection will be re-established automatically.
|
|
@ -1,10 +1,11 @@
|
||||||
entry,status,name,type,params
|
entry,status,name,type,params
|
||||||
Version,+,51.0,,
|
Version,+,52.0,,
|
||||||
Header,+,applications/services/bt/bt_service/bt.h,,
|
Header,+,applications/services/bt/bt_service/bt.h,,
|
||||||
Header,+,applications/services/cli/cli.h,,
|
Header,+,applications/services/cli/cli.h,,
|
||||||
Header,+,applications/services/cli/cli_vcp.h,,
|
Header,+,applications/services/cli/cli_vcp.h,,
|
||||||
Header,+,applications/services/dialogs/dialogs.h,,
|
Header,+,applications/services/dialogs/dialogs.h,,
|
||||||
Header,+,applications/services/dolphin/dolphin.h,,
|
Header,+,applications/services/dolphin/dolphin.h,,
|
||||||
|
Header,+,applications/services/expansion/expansion.h,,
|
||||||
Header,+,applications/services/gui/elements.h,,
|
Header,+,applications/services/gui/elements.h,,
|
||||||
Header,+,applications/services/gui/gui.h,,
|
Header,+,applications/services/gui/gui.h,,
|
||||||
Header,+,applications/services/gui/icon_i.h,,
|
Header,+,applications/services/gui/icon_i.h,,
|
||||||
|
@ -788,6 +789,8 @@ Function,-,exp10f,float,float
|
||||||
Function,-,exp2,double,double
|
Function,-,exp2,double,double
|
||||||
Function,-,exp2f,float,float
|
Function,-,exp2f,float,float
|
||||||
Function,-,exp2l,long double,long double
|
Function,-,exp2l,long double,long double
|
||||||
|
Function,+,expansion_disable,void,Expansion*
|
||||||
|
Function,+,expansion_enable,void,"Expansion*, FuriHalSerialId"
|
||||||
Function,-,expf,float,float
|
Function,-,expf,float,float
|
||||||
Function,-,expl,long double,long double
|
Function,-,expl,long double,long double
|
||||||
Function,-,explicit_bzero,void,"void*, size_t"
|
Function,-,explicit_bzero,void,"void*, size_t"
|
||||||
|
@ -1268,22 +1271,27 @@ Function,+,furi_hal_sd_max_mount_retry_count,uint8_t,
|
||||||
Function,+,furi_hal_sd_presence_init,void,
|
Function,+,furi_hal_sd_presence_init,void,
|
||||||
Function,+,furi_hal_sd_read_blocks,FuriStatus,"uint32_t*, uint32_t, uint32_t"
|
Function,+,furi_hal_sd_read_blocks,FuriStatus,"uint32_t*, uint32_t, uint32_t"
|
||||||
Function,+,furi_hal_sd_write_blocks,FuriStatus,"const uint32_t*, uint32_t, uint32_t"
|
Function,+,furi_hal_sd_write_blocks,FuriStatus,"const uint32_t*, uint32_t, uint32_t"
|
||||||
|
Function,+,furi_hal_serial_async_rx,uint8_t,FuriHalSerialHandle*
|
||||||
|
Function,+,furi_hal_serial_async_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialAsyncRxCallback, void*, _Bool"
|
||||||
|
Function,+,furi_hal_serial_async_rx_stop,void,FuriHalSerialHandle*
|
||||||
Function,+,furi_hal_serial_control_acquire,FuriHalSerialHandle*,FuriHalSerialId
|
Function,+,furi_hal_serial_control_acquire,FuriHalSerialHandle*,FuriHalSerialId
|
||||||
Function,+,furi_hal_serial_control_deinit,void,
|
Function,+,furi_hal_serial_control_deinit,void,
|
||||||
Function,+,furi_hal_serial_control_init,void,
|
Function,+,furi_hal_serial_control_init,void,
|
||||||
Function,+,furi_hal_serial_control_release,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_control_release,void,FuriHalSerialHandle*
|
||||||
Function,+,furi_hal_serial_control_resume,void,
|
Function,+,furi_hal_serial_control_resume,void,
|
||||||
|
Function,+,furi_hal_serial_control_set_expansion_callback,void,"FuriHalSerialId, FuriHalSerialControlExpansionCallback, void*"
|
||||||
Function,+,furi_hal_serial_control_set_logging_config,void,"FuriHalSerialId, uint32_t"
|
Function,+,furi_hal_serial_control_set_logging_config,void,"FuriHalSerialId, uint32_t"
|
||||||
Function,+,furi_hal_serial_control_suspend,void,
|
Function,+,furi_hal_serial_control_suspend,void,
|
||||||
Function,+,furi_hal_serial_deinit,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_deinit,void,FuriHalSerialHandle*
|
||||||
|
Function,+,furi_hal_serial_disable_direction,void,"FuriHalSerialHandle*, FuriHalSerialDirection"
|
||||||
Function,+,furi_hal_serial_dma_rx,size_t,"FuriHalSerialHandle*, uint8_t*, size_t"
|
Function,+,furi_hal_serial_dma_rx,size_t,"FuriHalSerialHandle*, uint8_t*, size_t"
|
||||||
Function,+,furi_hal_serial_dma_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialDmaRxCallback, void*, _Bool"
|
Function,+,furi_hal_serial_dma_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialDmaRxCallback, void*, _Bool"
|
||||||
Function,+,furi_hal_serial_dma_rx_stop,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_dma_rx_stop,void,FuriHalSerialHandle*
|
||||||
|
Function,+,furi_hal_serial_enable_direction,void,"FuriHalSerialHandle*, FuriHalSerialDirection"
|
||||||
|
Function,+,furi_hal_serial_get_gpio_pin,const GpioPin*,"FuriHalSerialHandle*, FuriHalSerialDirection"
|
||||||
Function,+,furi_hal_serial_init,void,"FuriHalSerialHandle*, uint32_t"
|
Function,+,furi_hal_serial_init,void,"FuriHalSerialHandle*, uint32_t"
|
||||||
|
Function,+,furi_hal_serial_is_baud_rate_supported,_Bool,"FuriHalSerialHandle*, uint32_t"
|
||||||
Function,+,furi_hal_serial_resume,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_resume,void,FuriHalSerialHandle*
|
||||||
Function,+,furi_hal_serial_async_rx,uint8_t,FuriHalSerialHandle*
|
|
||||||
Function,+,furi_hal_serial_async_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialAsyncRxCallback, void*, _Bool"
|
|
||||||
Function,+,furi_hal_serial_async_rx_stop,void,FuriHalSerialHandle*
|
|
||||||
Function,+,furi_hal_serial_set_br,void,"FuriHalSerialHandle*, uint32_t"
|
Function,+,furi_hal_serial_set_br,void,"FuriHalSerialHandle*, uint32_t"
|
||||||
Function,+,furi_hal_serial_suspend,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_suspend,void,FuriHalSerialHandle*
|
||||||
Function,+,furi_hal_serial_tx,void,"FuriHalSerialHandle*, const uint8_t*, size_t"
|
Function,+,furi_hal_serial_tx,void,"FuriHalSerialHandle*, const uint8_t*, size_t"
|
||||||
|
@ -2098,7 +2106,7 @@ Function,-,round,double,double
|
||||||
Function,+,roundf,float,float
|
Function,+,roundf,float,float
|
||||||
Function,-,roundl,long double,long double
|
Function,-,roundl,long double,long double
|
||||||
Function,+,rpc_session_close,void,RpcSession*
|
Function,+,rpc_session_close,void,RpcSession*
|
||||||
Function,+,rpc_session_feed,size_t,"RpcSession*, uint8_t*, size_t, uint32_t"
|
Function,+,rpc_session_feed,size_t,"RpcSession*, const uint8_t*, size_t, uint32_t"
|
||||||
Function,+,rpc_session_get_available_size,size_t,RpcSession*
|
Function,+,rpc_session_get_available_size,size_t,RpcSession*
|
||||||
Function,+,rpc_session_get_owner,RpcOwner,RpcSession*
|
Function,+,rpc_session_get_owner,RpcOwner,RpcSession*
|
||||||
Function,+,rpc_session_open,RpcSession*,"Rpc*, RpcOwner"
|
Function,+,rpc_session_open,RpcSession*,"Rpc*, RpcOwner"
|
||||||
|
|
|
|
@ -1,11 +1,12 @@
|
||||||
entry,status,name,type,params
|
entry,status,name,type,params
|
||||||
Version,+,51.0,,
|
Version,+,52.0,,
|
||||||
Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,,
|
Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,,
|
||||||
Header,+,applications/services/bt/bt_service/bt.h,,
|
Header,+,applications/services/bt/bt_service/bt.h,,
|
||||||
Header,+,applications/services/cli/cli.h,,
|
Header,+,applications/services/cli/cli.h,,
|
||||||
Header,+,applications/services/cli/cli_vcp.h,,
|
Header,+,applications/services/cli/cli_vcp.h,,
|
||||||
Header,+,applications/services/dialogs/dialogs.h,,
|
Header,+,applications/services/dialogs/dialogs.h,,
|
||||||
Header,+,applications/services/dolphin/dolphin.h,,
|
Header,+,applications/services/dolphin/dolphin.h,,
|
||||||
|
Header,+,applications/services/expansion/expansion.h,,
|
||||||
Header,+,applications/services/gui/elements.h,,
|
Header,+,applications/services/gui/elements.h,,
|
||||||
Header,+,applications/services/gui/gui.h,,
|
Header,+,applications/services/gui/gui.h,,
|
||||||
Header,+,applications/services/gui/icon_i.h,,
|
Header,+,applications/services/gui/icon_i.h,,
|
||||||
|
@ -877,6 +878,8 @@ Function,-,exp10f,float,float
|
||||||
Function,-,exp2,double,double
|
Function,-,exp2,double,double
|
||||||
Function,-,exp2f,float,float
|
Function,-,exp2f,float,float
|
||||||
Function,-,exp2l,long double,long double
|
Function,-,exp2l,long double,long double
|
||||||
|
Function,+,expansion_disable,void,Expansion*
|
||||||
|
Function,+,expansion_enable,void,"Expansion*, FuriHalSerialId"
|
||||||
Function,-,expf,float,float
|
Function,-,expf,float,float
|
||||||
Function,-,expl,long double,long double
|
Function,-,expl,long double,long double
|
||||||
Function,-,explicit_bzero,void,"void*, size_t"
|
Function,-,explicit_bzero,void,"void*, size_t"
|
||||||
|
@ -1434,22 +1437,27 @@ Function,+,furi_hal_sd_max_mount_retry_count,uint8_t,
|
||||||
Function,+,furi_hal_sd_presence_init,void,
|
Function,+,furi_hal_sd_presence_init,void,
|
||||||
Function,+,furi_hal_sd_read_blocks,FuriStatus,"uint32_t*, uint32_t, uint32_t"
|
Function,+,furi_hal_sd_read_blocks,FuriStatus,"uint32_t*, uint32_t, uint32_t"
|
||||||
Function,+,furi_hal_sd_write_blocks,FuriStatus,"const uint32_t*, uint32_t, uint32_t"
|
Function,+,furi_hal_sd_write_blocks,FuriStatus,"const uint32_t*, uint32_t, uint32_t"
|
||||||
|
Function,+,furi_hal_serial_async_rx,uint8_t,FuriHalSerialHandle*
|
||||||
|
Function,+,furi_hal_serial_async_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialAsyncRxCallback, void*, _Bool"
|
||||||
|
Function,+,furi_hal_serial_async_rx_stop,void,FuriHalSerialHandle*
|
||||||
Function,+,furi_hal_serial_control_acquire,FuriHalSerialHandle*,FuriHalSerialId
|
Function,+,furi_hal_serial_control_acquire,FuriHalSerialHandle*,FuriHalSerialId
|
||||||
Function,+,furi_hal_serial_control_deinit,void,
|
Function,+,furi_hal_serial_control_deinit,void,
|
||||||
Function,+,furi_hal_serial_control_init,void,
|
Function,+,furi_hal_serial_control_init,void,
|
||||||
Function,+,furi_hal_serial_control_release,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_control_release,void,FuriHalSerialHandle*
|
||||||
Function,+,furi_hal_serial_control_resume,void,
|
Function,+,furi_hal_serial_control_resume,void,
|
||||||
|
Function,+,furi_hal_serial_control_set_expansion_callback,void,"FuriHalSerialId, FuriHalSerialControlExpansionCallback, void*"
|
||||||
Function,+,furi_hal_serial_control_set_logging_config,void,"FuriHalSerialId, uint32_t"
|
Function,+,furi_hal_serial_control_set_logging_config,void,"FuriHalSerialId, uint32_t"
|
||||||
Function,+,furi_hal_serial_control_suspend,void,
|
Function,+,furi_hal_serial_control_suspend,void,
|
||||||
Function,+,furi_hal_serial_deinit,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_deinit,void,FuriHalSerialHandle*
|
||||||
|
Function,+,furi_hal_serial_disable_direction,void,"FuriHalSerialHandle*, FuriHalSerialDirection"
|
||||||
Function,+,furi_hal_serial_dma_rx,size_t,"FuriHalSerialHandle*, uint8_t*, size_t"
|
Function,+,furi_hal_serial_dma_rx,size_t,"FuriHalSerialHandle*, uint8_t*, size_t"
|
||||||
Function,+,furi_hal_serial_dma_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialDmaRxCallback, void*, _Bool"
|
Function,+,furi_hal_serial_dma_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialDmaRxCallback, void*, _Bool"
|
||||||
Function,+,furi_hal_serial_dma_rx_stop,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_dma_rx_stop,void,FuriHalSerialHandle*
|
||||||
|
Function,+,furi_hal_serial_enable_direction,void,"FuriHalSerialHandle*, FuriHalSerialDirection"
|
||||||
|
Function,+,furi_hal_serial_get_gpio_pin,const GpioPin*,"FuriHalSerialHandle*, FuriHalSerialDirection"
|
||||||
Function,+,furi_hal_serial_init,void,"FuriHalSerialHandle*, uint32_t"
|
Function,+,furi_hal_serial_init,void,"FuriHalSerialHandle*, uint32_t"
|
||||||
|
Function,+,furi_hal_serial_is_baud_rate_supported,_Bool,"FuriHalSerialHandle*, uint32_t"
|
||||||
Function,+,furi_hal_serial_resume,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_resume,void,FuriHalSerialHandle*
|
||||||
Function,+,furi_hal_serial_async_rx,uint8_t,FuriHalSerialHandle*
|
|
||||||
Function,+,furi_hal_serial_async_rx_start,void,"FuriHalSerialHandle*, FuriHalSerialAsyncRxCallback, void*, _Bool"
|
|
||||||
Function,+,furi_hal_serial_async_rx_stop,void,FuriHalSerialHandle*
|
|
||||||
Function,+,furi_hal_serial_set_br,void,"FuriHalSerialHandle*, uint32_t"
|
Function,+,furi_hal_serial_set_br,void,"FuriHalSerialHandle*, uint32_t"
|
||||||
Function,+,furi_hal_serial_suspend,void,FuriHalSerialHandle*
|
Function,+,furi_hal_serial_suspend,void,FuriHalSerialHandle*
|
||||||
Function,+,furi_hal_serial_tx,void,"FuriHalSerialHandle*, const uint8_t*, size_t"
|
Function,+,furi_hal_serial_tx,void,"FuriHalSerialHandle*, const uint8_t*, size_t"
|
||||||
|
@ -2682,7 +2690,7 @@ Function,-,round,double,double
|
||||||
Function,+,roundf,float,float
|
Function,+,roundf,float,float
|
||||||
Function,-,roundl,long double,long double
|
Function,-,roundl,long double,long double
|
||||||
Function,+,rpc_session_close,void,RpcSession*
|
Function,+,rpc_session_close,void,RpcSession*
|
||||||
Function,+,rpc_session_feed,size_t,"RpcSession*, uint8_t*, size_t, uint32_t"
|
Function,+,rpc_session_feed,size_t,"RpcSession*, const uint8_t*, size_t, uint32_t"
|
||||||
Function,+,rpc_session_get_available_size,size_t,RpcSession*
|
Function,+,rpc_session_get_available_size,size_t,RpcSession*
|
||||||
Function,+,rpc_session_get_owner,RpcOwner,RpcSession*
|
Function,+,rpc_session_get_owner,RpcOwner,RpcSession*
|
||||||
Function,+,rpc_session_open,RpcSession*,"Rpc*, RpcOwner"
|
Function,+,rpc_session_open,RpcSession*,"Rpc*, RpcOwner"
|
||||||
|
|
|
|
@ -31,6 +31,59 @@ typedef struct {
|
||||||
void* context;
|
void* context;
|
||||||
} FuriHalSerial;
|
} FuriHalSerial;
|
||||||
|
|
||||||
|
typedef void (*FuriHalSerialControlFunc)(USART_TypeDef*);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
USART_TypeDef* periph;
|
||||||
|
GpioAltFn alt_fn;
|
||||||
|
const GpioPin* gpio[FuriHalSerialDirectionMax];
|
||||||
|
FuriHalSerialControlFunc enable[FuriHalSerialDirectionMax];
|
||||||
|
FuriHalSerialControlFunc disable[FuriHalSerialDirectionMax];
|
||||||
|
} FuriHalSerialConfig;
|
||||||
|
|
||||||
|
static const FuriHalSerialConfig furi_hal_serial_config[FuriHalSerialIdMax] = {
|
||||||
|
[FuriHalSerialIdUsart] =
|
||||||
|
{
|
||||||
|
.periph = USART1,
|
||||||
|
.alt_fn = GpioAltFn7USART1,
|
||||||
|
.gpio =
|
||||||
|
{
|
||||||
|
[FuriHalSerialDirectionTx] = &gpio_usart_tx,
|
||||||
|
[FuriHalSerialDirectionRx] = &gpio_usart_rx,
|
||||||
|
},
|
||||||
|
.enable =
|
||||||
|
{
|
||||||
|
[FuriHalSerialDirectionTx] = LL_USART_EnableDirectionTx,
|
||||||
|
[FuriHalSerialDirectionRx] = LL_USART_EnableDirectionRx,
|
||||||
|
},
|
||||||
|
.disable =
|
||||||
|
{
|
||||||
|
[FuriHalSerialDirectionTx] = LL_USART_DisableDirectionTx,
|
||||||
|
[FuriHalSerialDirectionRx] = LL_USART_DisableDirectionRx,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[FuriHalSerialIdLpuart] =
|
||||||
|
{
|
||||||
|
.periph = LPUART1,
|
||||||
|
.alt_fn = GpioAltFn8LPUART1,
|
||||||
|
.gpio =
|
||||||
|
{
|
||||||
|
[FuriHalSerialDirectionTx] = &gpio_ext_pc1,
|
||||||
|
[FuriHalSerialDirectionRx] = &gpio_ext_pc0,
|
||||||
|
},
|
||||||
|
.enable =
|
||||||
|
{
|
||||||
|
[FuriHalSerialDirectionTx] = LL_LPUART_EnableDirectionTx,
|
||||||
|
[FuriHalSerialDirectionRx] = LL_LPUART_EnableDirectionRx,
|
||||||
|
},
|
||||||
|
.disable =
|
||||||
|
{
|
||||||
|
[FuriHalSerialDirectionTx] = LL_LPUART_DisableDirectionTx,
|
||||||
|
[FuriHalSerialDirectionRx] = LL_LPUART_DisableDirectionRx,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
static FuriHalSerial furi_hal_serial[FuriHalSerialIdMax] = {0};
|
static FuriHalSerial furi_hal_serial[FuriHalSerialIdMax] = {0};
|
||||||
|
|
||||||
static size_t furi_hal_serial_dma_bytes_available(FuriHalSerialId ch);
|
static size_t furi_hal_serial_dma_bytes_available(FuriHalSerialId ch);
|
||||||
|
@ -451,6 +504,11 @@ void furi_hal_serial_init(FuriHalSerialHandle* handle, uint32_t baud) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool furi_hal_serial_is_baud_rate_supported(FuriHalSerialHandle* handle, uint32_t baud) {
|
||||||
|
furi_check(handle);
|
||||||
|
return baud >= 9600UL && baud <= 4000000UL;
|
||||||
|
}
|
||||||
|
|
||||||
static uint32_t furi_hal_serial_get_prescaler(FuriHalSerialHandle* handle, uint32_t baud) {
|
static uint32_t furi_hal_serial_get_prescaler(FuriHalSerialHandle* handle, uint32_t baud) {
|
||||||
uint32_t uartclk = LL_RCC_GetUSARTClockFreq(LL_RCC_USART1_CLKSOURCE);
|
uint32_t uartclk = LL_RCC_GetUSARTClockFreq(LL_RCC_USART1_CLKSOURCE);
|
||||||
uint32_t divisor = (uartclk / baud);
|
uint32_t divisor = (uartclk / baud);
|
||||||
|
@ -836,3 +894,44 @@ void furi_hal_serial_dma_rx_stop(FuriHalSerialHandle* handle) {
|
||||||
furi_hal_serial_event_deinit(handle);
|
furi_hal_serial_event_deinit(handle);
|
||||||
furi_hal_serial_dma_configure(handle, NULL, NULL);
|
furi_hal_serial_dma_configure(handle, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void furi_hal_serial_enable_direction(
|
||||||
|
FuriHalSerialHandle* handle,
|
||||||
|
FuriHalSerialDirection direction) {
|
||||||
|
furi_check(handle);
|
||||||
|
furi_check(handle->id < FuriHalSerialIdMax);
|
||||||
|
furi_check(direction < FuriHalSerialDirectionMax);
|
||||||
|
|
||||||
|
USART_TypeDef* periph = furi_hal_serial_config[handle->id].periph;
|
||||||
|
furi_hal_serial_config[handle->id].enable[direction](periph);
|
||||||
|
|
||||||
|
const GpioPin* gpio = furi_hal_serial_config[handle->id].gpio[direction];
|
||||||
|
const GpioAltFn alt_fn = furi_hal_serial_config[handle->id].alt_fn;
|
||||||
|
|
||||||
|
furi_hal_gpio_init_ex(
|
||||||
|
gpio, GpioModeAltFunctionPushPull, GpioPullUp, GpioSpeedVeryHigh, alt_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
void furi_hal_serial_disable_direction(
|
||||||
|
FuriHalSerialHandle* handle,
|
||||||
|
FuriHalSerialDirection direction) {
|
||||||
|
furi_check(handle);
|
||||||
|
furi_check(handle->id < FuriHalSerialIdMax);
|
||||||
|
furi_check(direction < FuriHalSerialDirectionMax);
|
||||||
|
|
||||||
|
USART_TypeDef* periph = furi_hal_serial_config[handle->id].periph;
|
||||||
|
furi_hal_serial_config[handle->id].disable[direction](periph);
|
||||||
|
|
||||||
|
const GpioPin* gpio = furi_hal_serial_config[handle->id].gpio[direction];
|
||||||
|
|
||||||
|
furi_hal_gpio_init(gpio, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GpioPin*
|
||||||
|
furi_hal_serial_get_gpio_pin(FuriHalSerialHandle* handle, FuriHalSerialDirection direction) {
|
||||||
|
furi_check(handle);
|
||||||
|
furi_check(handle->id < FuriHalSerialIdMax);
|
||||||
|
furi_check(direction < FuriHalSerialDirectionMax);
|
||||||
|
|
||||||
|
return furi_hal_serial_config[handle->id].gpio[direction];
|
||||||
|
}
|
||||||
|
|
|
@ -48,6 +48,15 @@ void furi_hal_serial_suspend(FuriHalSerialHandle* handle);
|
||||||
*/
|
*/
|
||||||
void furi_hal_serial_resume(FuriHalSerialHandle* handle);
|
void furi_hal_serial_resume(FuriHalSerialHandle* handle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Determine whether a certain baud rate is supported
|
||||||
|
*
|
||||||
|
* @param handle Serial handle
|
||||||
|
* @param baud baud rate to be checked
|
||||||
|
* @returns true if baud rate is supported, false otherwise.
|
||||||
|
*/
|
||||||
|
bool furi_hal_serial_is_baud_rate_supported(FuriHalSerialHandle* handle, uint32_t baud);
|
||||||
|
|
||||||
/** Changes baud rate
|
/** Changes baud rate
|
||||||
*
|
*
|
||||||
* @param handle Serial handle
|
* @param handle Serial handle
|
||||||
|
@ -152,6 +161,42 @@ typedef void (*FuriHalSerialDmaRxCallback)(
|
||||||
size_t data_len,
|
size_t data_len,
|
||||||
void* context);
|
void* context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enable an input/output directon
|
||||||
|
*
|
||||||
|
* Takes over the respective pin by reconfiguring it to
|
||||||
|
* the appropriate alternative function.
|
||||||
|
*
|
||||||
|
* @param handle Serial handle
|
||||||
|
* @param direction Direction to enable
|
||||||
|
*/
|
||||||
|
void furi_hal_serial_enable_direction(
|
||||||
|
FuriHalSerialHandle* handle,
|
||||||
|
FuriHalSerialDirection direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Disable an input/output directon
|
||||||
|
*
|
||||||
|
* Releases the respective pin by reconfiguring it to
|
||||||
|
* initial state, making possible its use for other purposes.
|
||||||
|
*
|
||||||
|
* @param handle Serial handle
|
||||||
|
* @param direction Direction to disable
|
||||||
|
*/
|
||||||
|
void furi_hal_serial_disable_direction(
|
||||||
|
FuriHalSerialHandle* handle,
|
||||||
|
FuriHalSerialDirection direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the GPIO pin associated with a serial
|
||||||
|
*
|
||||||
|
* @param handle Serial handle
|
||||||
|
* @param direction Direction to query
|
||||||
|
* @returns pointer to the respective pin instance
|
||||||
|
*/
|
||||||
|
const GpioPin*
|
||||||
|
furi_hal_serial_get_gpio_pin(FuriHalSerialHandle* handle, FuriHalSerialDirection direction);
|
||||||
|
|
||||||
/** Start and sets Serial event callback receive DMA
|
/** Start and sets Serial event callback receive DMA
|
||||||
*
|
*
|
||||||
* @param handle Serial handle
|
* @param handle Serial handle
|
||||||
|
|
|
@ -14,6 +14,8 @@ typedef enum {
|
||||||
FuriHalSerialControlMessageTypeAcquire,
|
FuriHalSerialControlMessageTypeAcquire,
|
||||||
FuriHalSerialControlMessageTypeRelease,
|
FuriHalSerialControlMessageTypeRelease,
|
||||||
FuriHalSerialControlMessageTypeLogging,
|
FuriHalSerialControlMessageTypeLogging,
|
||||||
|
FuriHalSerialControlMessageTypeExpansionSetCallback,
|
||||||
|
FuriHalSerialControlMessageTypeExpansionIrq,
|
||||||
} FuriHalSerialControlMessageType;
|
} FuriHalSerialControlMessageType;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
@ -28,6 +30,12 @@ typedef struct {
|
||||||
const uint32_t baud_rate;
|
const uint32_t baud_rate;
|
||||||
} FuriHalSerialControlMessageInputLogging;
|
} FuriHalSerialControlMessageInputLogging;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const FuriHalSerialId id;
|
||||||
|
const FuriHalSerialControlExpansionCallback callback;
|
||||||
|
void* context;
|
||||||
|
} FuriHalSerialControlMessageExpCallback;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
FuriHalSerialHandle handles[FuriHalSerialIdMax];
|
FuriHalSerialHandle handles[FuriHalSerialIdMax];
|
||||||
FuriMessageQueue* queue;
|
FuriMessageQueue* queue;
|
||||||
|
@ -38,6 +46,10 @@ typedef struct {
|
||||||
uint32_t log_config_serial_baud_rate;
|
uint32_t log_config_serial_baud_rate;
|
||||||
FuriLogHandler log_handler;
|
FuriLogHandler log_handler;
|
||||||
FuriHalSerialHandle* log_serial;
|
FuriHalSerialHandle* log_serial;
|
||||||
|
|
||||||
|
// Expansion detection
|
||||||
|
FuriHalSerialControlExpansionCallback expansion_cb;
|
||||||
|
void* expansion_ctx;
|
||||||
} FuriHalSerialControl;
|
} FuriHalSerialControl;
|
||||||
|
|
||||||
FuriHalSerialControl* furi_hal_serial_control = NULL;
|
FuriHalSerialControl* furi_hal_serial_control = NULL;
|
||||||
|
@ -65,6 +77,150 @@ static void furi_hal_serial_control_log_set_handle(FuriHalSerialHandle* handle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void furi_hal_serial_control_expansion_irq_callback(void* context) {
|
||||||
|
UNUSED(context);
|
||||||
|
|
||||||
|
FuriHalSerialControlMessage message;
|
||||||
|
message.type = FuriHalSerialControlMessageTypeExpansionIrq;
|
||||||
|
message.api_lock = NULL;
|
||||||
|
furi_message_queue_put(furi_hal_serial_control->queue, &message, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool furi_hal_serial_control_handler_stop(void* input, void* output) {
|
||||||
|
UNUSED(input);
|
||||||
|
UNUSED(output);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool furi_hal_serial_control_handler_suspend(void* input, void* output) {
|
||||||
|
UNUSED(input);
|
||||||
|
UNUSED(output);
|
||||||
|
|
||||||
|
for(size_t i = 0; i < FuriHalSerialIdMax; i++) {
|
||||||
|
furi_hal_serial_tx_wait_complete(&furi_hal_serial_control->handles[i]);
|
||||||
|
furi_hal_serial_suspend(&furi_hal_serial_control->handles[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool furi_hal_serial_control_handler_resume(void* input, void* output) {
|
||||||
|
UNUSED(input);
|
||||||
|
UNUSED(output);
|
||||||
|
|
||||||
|
for(size_t i = 0; i < FuriHalSerialIdMax; i++) {
|
||||||
|
furi_hal_serial_resume(&furi_hal_serial_control->handles[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool furi_hal_serial_control_handler_acquire(void* input, void* output) {
|
||||||
|
FuriHalSerialId serial_id = *(FuriHalSerialId*)input;
|
||||||
|
if(furi_hal_serial_control->handles[serial_id].in_use) {
|
||||||
|
*(FuriHalSerialHandle**)output = NULL;
|
||||||
|
} else {
|
||||||
|
// Logging
|
||||||
|
if(furi_hal_serial_control->log_config_serial_id == serial_id) {
|
||||||
|
furi_hal_serial_control_log_set_handle(NULL);
|
||||||
|
}
|
||||||
|
// Return handle
|
||||||
|
furi_hal_serial_control->handles[serial_id].in_use = true;
|
||||||
|
*(FuriHalSerialHandle**)output = &furi_hal_serial_control->handles[serial_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool furi_hal_serial_control_handler_release(void* input, void* output) {
|
||||||
|
UNUSED(output);
|
||||||
|
|
||||||
|
FuriHalSerialHandle* handle = *(FuriHalSerialHandle**)input;
|
||||||
|
furi_assert(handle->in_use);
|
||||||
|
furi_hal_serial_deinit(handle);
|
||||||
|
handle->in_use = false;
|
||||||
|
|
||||||
|
// Return back logging
|
||||||
|
if(furi_hal_serial_control->log_config_serial_id == handle->id) {
|
||||||
|
furi_hal_serial_control_log_set_handle(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool furi_hal_serial_control_handler_logging(void* input, void* output) {
|
||||||
|
UNUSED(output);
|
||||||
|
|
||||||
|
// Set new configuration
|
||||||
|
FuriHalSerialControlMessageInputLogging* message_input = input;
|
||||||
|
furi_hal_serial_control->log_config_serial_id = message_input->id;
|
||||||
|
furi_hal_serial_control->log_config_serial_baud_rate = message_input->baud_rate;
|
||||||
|
// Apply new configuration
|
||||||
|
FuriHalSerialHandle* handle = NULL;
|
||||||
|
if(furi_hal_serial_control->log_config_serial_id < FuriHalSerialIdMax) {
|
||||||
|
if(!furi_hal_serial_control->handles[furi_hal_serial_control->log_config_serial_id].in_use) {
|
||||||
|
handle =
|
||||||
|
&furi_hal_serial_control->handles[furi_hal_serial_control->log_config_serial_id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
furi_hal_serial_control_log_set_handle(handle);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool furi_hal_serial_control_handler_expansion_set_callback(void* input, void* output) {
|
||||||
|
UNUSED(output);
|
||||||
|
|
||||||
|
FuriHalSerialControlMessageExpCallback* message_input = input;
|
||||||
|
FuriHalSerialHandle* handle = &furi_hal_serial_control->handles[message_input->id];
|
||||||
|
const GpioPin* gpio = furi_hal_serial_get_gpio_pin(handle, FuriHalSerialDirectionRx);
|
||||||
|
|
||||||
|
if(message_input->callback) {
|
||||||
|
furi_check(furi_hal_serial_control->expansion_cb == NULL);
|
||||||
|
|
||||||
|
furi_hal_serial_disable_direction(handle, FuriHalSerialDirectionRx);
|
||||||
|
furi_hal_gpio_add_int_callback(gpio, furi_hal_serial_control_expansion_irq_callback, NULL);
|
||||||
|
furi_hal_gpio_init(gpio, GpioModeInterruptFall, GpioPullUp, GpioSpeedLow);
|
||||||
|
} else {
|
||||||
|
furi_check(furi_hal_serial_control->expansion_cb != NULL);
|
||||||
|
|
||||||
|
furi_hal_gpio_remove_int_callback(gpio);
|
||||||
|
furi_hal_serial_enable_direction(handle, FuriHalSerialDirectionRx);
|
||||||
|
}
|
||||||
|
|
||||||
|
furi_hal_serial_control->expansion_cb = message_input->callback;
|
||||||
|
furi_hal_serial_control->expansion_ctx = message_input->context;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool furi_hal_serial_control_handler_expansion_irq(void* input, void* output) {
|
||||||
|
UNUSED(input);
|
||||||
|
UNUSED(output);
|
||||||
|
|
||||||
|
if(furi_hal_serial_control->expansion_cb) {
|
||||||
|
void* context = furi_hal_serial_control->expansion_ctx;
|
||||||
|
furi_hal_serial_control->expansion_cb(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef bool (*FuriHalSerialControlCommandHandler)(void* input, void* output);
|
||||||
|
|
||||||
|
static const FuriHalSerialControlCommandHandler furi_hal_serial_control_handlers[] = {
|
||||||
|
[FuriHalSerialControlMessageTypeStop] = furi_hal_serial_control_handler_stop,
|
||||||
|
[FuriHalSerialControlMessageTypeSuspend] = furi_hal_serial_control_handler_suspend,
|
||||||
|
[FuriHalSerialControlMessageTypeResume] = furi_hal_serial_control_handler_resume,
|
||||||
|
[FuriHalSerialControlMessageTypeAcquire] = furi_hal_serial_control_handler_acquire,
|
||||||
|
[FuriHalSerialControlMessageTypeRelease] = furi_hal_serial_control_handler_release,
|
||||||
|
[FuriHalSerialControlMessageTypeLogging] = furi_hal_serial_control_handler_logging,
|
||||||
|
[FuriHalSerialControlMessageTypeExpansionSetCallback] =
|
||||||
|
furi_hal_serial_control_handler_expansion_set_callback,
|
||||||
|
[FuriHalSerialControlMessageTypeExpansionIrq] = furi_hal_serial_control_handler_expansion_irq,
|
||||||
|
};
|
||||||
|
|
||||||
static int32_t furi_hal_serial_control_thread(void* args) {
|
static int32_t furi_hal_serial_control_thread(void* args) {
|
||||||
UNUSED(args);
|
UNUSED(args);
|
||||||
|
|
||||||
|
@ -74,61 +230,13 @@ static int32_t furi_hal_serial_control_thread(void* args) {
|
||||||
FuriStatus status =
|
FuriStatus status =
|
||||||
furi_message_queue_get(furi_hal_serial_control->queue, &message, FuriWaitForever);
|
furi_message_queue_get(furi_hal_serial_control->queue, &message, FuriWaitForever);
|
||||||
furi_check(status == FuriStatusOk);
|
furi_check(status == FuriStatusOk);
|
||||||
|
furi_check(message.type < COUNT_OF(furi_hal_serial_control_handlers));
|
||||||
|
|
||||||
if(message.type == FuriHalSerialControlMessageTypeStop) {
|
should_continue =
|
||||||
should_continue = false;
|
furi_hal_serial_control_handlers[message.type](message.input, message.output);
|
||||||
} else if(message.type == FuriHalSerialControlMessageTypeSuspend) {
|
|
||||||
for(size_t i = 0; i < FuriHalSerialIdMax; i++) {
|
|
||||||
furi_hal_serial_tx_wait_complete(&furi_hal_serial_control->handles[i]);
|
|
||||||
furi_hal_serial_suspend(&furi_hal_serial_control->handles[i]);
|
|
||||||
}
|
|
||||||
api_lock_unlock(message.api_lock);
|
|
||||||
} else if(message.type == FuriHalSerialControlMessageTypeResume) {
|
|
||||||
for(size_t i = 0; i < FuriHalSerialIdMax; i++) {
|
|
||||||
furi_hal_serial_resume(&furi_hal_serial_control->handles[i]);
|
|
||||||
}
|
|
||||||
api_lock_unlock(message.api_lock);
|
|
||||||
} else if(message.type == FuriHalSerialControlMessageTypeAcquire) {
|
|
||||||
FuriHalSerialId serial_id = *(FuriHalSerialId*)message.input;
|
|
||||||
if(furi_hal_serial_control->handles[serial_id].in_use) {
|
|
||||||
*(FuriHalSerialHandle**)message.output = NULL;
|
|
||||||
} else {
|
|
||||||
// Logging
|
|
||||||
if(furi_hal_serial_control->log_config_serial_id == serial_id) {
|
|
||||||
furi_hal_serial_control_log_set_handle(NULL);
|
|
||||||
}
|
|
||||||
// Return handle
|
|
||||||
furi_hal_serial_control->handles[serial_id].in_use = true;
|
|
||||||
*(FuriHalSerialHandle**)message.output =
|
|
||||||
&furi_hal_serial_control->handles[serial_id];
|
|
||||||
}
|
|
||||||
api_lock_unlock(message.api_lock);
|
|
||||||
} else if(message.type == FuriHalSerialControlMessageTypeRelease) {
|
|
||||||
FuriHalSerialHandle* handle = *(FuriHalSerialHandle**)message.input;
|
|
||||||
furi_assert(handle->in_use);
|
|
||||||
furi_hal_serial_deinit(handle);
|
|
||||||
handle->in_use = false;
|
|
||||||
|
|
||||||
// Return back logging
|
if(message.api_lock != NULL) {
|
||||||
if(furi_hal_serial_control->log_config_serial_id == handle->id) {
|
|
||||||
furi_hal_serial_control_log_set_handle(handle);
|
|
||||||
}
|
|
||||||
api_lock_unlock(message.api_lock);
|
api_lock_unlock(message.api_lock);
|
||||||
} else if(message.type == FuriHalSerialControlMessageTypeLogging) {
|
|
||||||
// Set new configuration
|
|
||||||
FuriHalSerialControlMessageInputLogging* message_input = message.input;
|
|
||||||
furi_hal_serial_control->log_config_serial_id = message_input->id;
|
|
||||||
furi_hal_serial_control->log_config_serial_baud_rate = message_input->baud_rate;
|
|
||||||
// Apply new configuration
|
|
||||||
FuriHalSerialHandle* handle = NULL;
|
|
||||||
if(furi_hal_serial_control->log_config_serial_id < FuriHalSerialIdMax) {
|
|
||||||
handle = &furi_hal_serial_control
|
|
||||||
->handles[furi_hal_serial_control->log_config_serial_id];
|
|
||||||
}
|
|
||||||
furi_hal_serial_control_log_set_handle(handle);
|
|
||||||
api_lock_unlock(message.api_lock);
|
|
||||||
} else {
|
|
||||||
furi_crash("Invalid parameter");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,6 +265,7 @@ void furi_hal_serial_control_deinit(void) {
|
||||||
// Stop control plane thread
|
// Stop control plane thread
|
||||||
FuriHalSerialControlMessage message;
|
FuriHalSerialControlMessage message;
|
||||||
message.type = FuriHalSerialControlMessageTypeStop;
|
message.type = FuriHalSerialControlMessageTypeStop;
|
||||||
|
message.api_lock = NULL;
|
||||||
furi_message_queue_put(furi_hal_serial_control->queue, &message, FuriWaitForever);
|
furi_message_queue_put(furi_hal_serial_control->queue, &message, FuriWaitForever);
|
||||||
furi_thread_join(furi_hal_serial_control->thread);
|
furi_thread_join(furi_hal_serial_control->thread);
|
||||||
// Release resources
|
// Release resources
|
||||||
|
@ -220,6 +329,9 @@ void furi_hal_serial_control_set_logging_config(FuriHalSerialId serial_id, uint3
|
||||||
// Very special case of updater, where RTC initialized before kernel start
|
// Very special case of updater, where RTC initialized before kernel start
|
||||||
if(!furi_hal_serial_control) return;
|
if(!furi_hal_serial_control) return;
|
||||||
|
|
||||||
|
furi_check(furi_hal_serial_is_baud_rate_supported(
|
||||||
|
&furi_hal_serial_control->handles[serial_id], baud_rate));
|
||||||
|
|
||||||
FuriHalSerialControlMessageInputLogging message_input = {
|
FuriHalSerialControlMessageInputLogging message_input = {
|
||||||
.id = serial_id,
|
.id = serial_id,
|
||||||
.baud_rate = baud_rate,
|
.baud_rate = baud_rate,
|
||||||
|
@ -231,3 +343,23 @@ void furi_hal_serial_control_set_logging_config(FuriHalSerialId serial_id, uint3
|
||||||
furi_message_queue_put(furi_hal_serial_control->queue, &message, FuriWaitForever);
|
furi_message_queue_put(furi_hal_serial_control->queue, &message, FuriWaitForever);
|
||||||
api_lock_wait_unlock_and_free(message.api_lock);
|
api_lock_wait_unlock_and_free(message.api_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void furi_hal_serial_control_set_expansion_callback(
|
||||||
|
FuriHalSerialId serial_id,
|
||||||
|
FuriHalSerialControlExpansionCallback callback,
|
||||||
|
void* context) {
|
||||||
|
furi_check(serial_id <= FuriHalSerialIdMax);
|
||||||
|
furi_check(furi_hal_serial_control);
|
||||||
|
|
||||||
|
FuriHalSerialControlMessageExpCallback message_input = {
|
||||||
|
.id = serial_id,
|
||||||
|
.callback = callback,
|
||||||
|
.context = context,
|
||||||
|
};
|
||||||
|
FuriHalSerialControlMessage message;
|
||||||
|
message.type = FuriHalSerialControlMessageTypeExpansionSetCallback;
|
||||||
|
message.api_lock = api_lock_alloc_locked();
|
||||||
|
message.input = &message_input;
|
||||||
|
furi_message_queue_put(furi_hal_serial_control->queue, &message, FuriWaitForever);
|
||||||
|
api_lock_wait_unlock_and_free(message.api_lock);
|
||||||
|
}
|
||||||
|
|
|
@ -41,6 +41,27 @@ void furi_hal_serial_control_release(FuriHalSerialHandle* handle);
|
||||||
*/
|
*/
|
||||||
void furi_hal_serial_control_set_logging_config(FuriHalSerialId serial_id, uint32_t baud_rate);
|
void furi_hal_serial_control_set_logging_config(FuriHalSerialId serial_id, uint32_t baud_rate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Expansion module detection callback type.
|
||||||
|
*
|
||||||
|
* @param[in,out] context Pointer to the user-defined context object.
|
||||||
|
*/
|
||||||
|
typedef void (*FuriHalSerialControlExpansionCallback)(void* context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enable expansion module detection for a given serial interface.
|
||||||
|
*
|
||||||
|
* Passing NULL as the callback parameter disables external module detection.
|
||||||
|
*
|
||||||
|
* @param[in] serial_id Identifier of the serial interface to be used.
|
||||||
|
* @param[in] callback Pointer to the callback function to be called upon module detection.
|
||||||
|
* @param[in,out] context Pointer to the user-defined context object. Will be passed to the callback function.
|
||||||
|
*/
|
||||||
|
void furi_hal_serial_control_set_expansion_callback(
|
||||||
|
FuriHalSerialId serial_id,
|
||||||
|
FuriHalSerialControlExpansionCallback callback,
|
||||||
|
void* context);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -12,4 +12,11 @@ typedef enum {
|
||||||
FuriHalSerialIdMax,
|
FuriHalSerialIdMax,
|
||||||
} FuriHalSerialId;
|
} FuriHalSerialId;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
FuriHalSerialDirectionTx,
|
||||||
|
FuriHalSerialDirectionRx,
|
||||||
|
|
||||||
|
FuriHalSerialDirectionMax,
|
||||||
|
} FuriHalSerialDirection;
|
||||||
|
|
||||||
typedef struct FuriHalSerialHandle FuriHalSerialHandle;
|
typedef struct FuriHalSerialHandle FuriHalSerialHandle;
|
||||||
|
|
Loading…
Reference in a new issue