mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-05-11 06:09:08 -07:00
Merge branch 'ofw-dev' into xfw-dev
This commit is contained in:
@@ -5,6 +5,7 @@ App(
|
||||
provides=[
|
||||
"crypto_start",
|
||||
"rpc_start",
|
||||
"expansion_start",
|
||||
"bt",
|
||||
"desktop",
|
||||
"loader",
|
||||
|
||||
@@ -221,7 +221,12 @@ void cli_command_log(Cli* cli, FuriString* args, void* context) {
|
||||
furi_log_level_to_string(furi_log_get_level(), ¤t_level);
|
||||
printf("Current log level: %s\r\n", current_level);
|
||||
|
||||
furi_hal_console_set_tx_callback(cli_command_log_tx_callback, ring);
|
||||
FuriLogHandler log_handler = {
|
||||
.callback = cli_command_log_tx_callback,
|
||||
.context = ring,
|
||||
};
|
||||
|
||||
furi_log_add_handler(log_handler);
|
||||
|
||||
printf("Use <log ?> to list available log levels\r\n");
|
||||
printf("Press CTRL+C to stop...\r\n");
|
||||
@@ -230,7 +235,7 @@ void cli_command_log(Cli* cli, FuriString* args, void* context) {
|
||||
cli_write(cli, buffer, ret);
|
||||
}
|
||||
|
||||
furi_hal_console_set_tx_callback(NULL, NULL);
|
||||
furi_log_remove_handler(log_handler);
|
||||
|
||||
if(restore_log_level) {
|
||||
// There will be strange behaviour if log level is set from settings while log command is running
|
||||
|
||||
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"
|
||||
@@ -163,8 +163,11 @@ void rpc_session_set_terminated_callback(
|
||||
* command is gets processed - it's safe either way. But case of it is quite
|
||||
* odd: client sends close request and sends command after.
|
||||
*/
|
||||
size_t
|
||||
rpc_session_feed(RpcSession* session, uint8_t* encoded_bytes, size_t size, uint32_t timeout) {
|
||||
size_t rpc_session_feed(
|
||||
RpcSession* session,
|
||||
const uint8_t* encoded_bytes,
|
||||
size_t size,
|
||||
uint32_t timeout) {
|
||||
furi_assert(session);
|
||||
furi_assert(encoded_bytes);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ typedef enum {
|
||||
RpcOwnerUnknown = 0,
|
||||
RpcOwnerBle,
|
||||
RpcOwnerUsb,
|
||||
RpcOwnerUart,
|
||||
RpcOwnerCount,
|
||||
} RpcOwner;
|
||||
|
||||
@@ -124,7 +125,7 @@ void rpc_session_set_terminated_callback(
|
||||
*
|
||||
* @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
|
||||
*
|
||||
@@ -143,4 +144,4 @@ size_t rpc_get_sessions_count(Rpc* rpc);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -265,7 +265,7 @@ static void rpc_system_gui_virtual_display_input_callback(InputEvent* event, voi
|
||||
RpcGuiSystem* rpc_gui = context;
|
||||
RpcSession* session = rpc_gui->session;
|
||||
|
||||
FURI_LOG_D(TAG, "VirtulDisplay: SendInputEvent");
|
||||
FURI_LOG_D(TAG, "VirtualDisplay: SendInputEvent");
|
||||
|
||||
PB_Main rpc_message = {
|
||||
.command_id = 0,
|
||||
@@ -317,7 +317,7 @@ static void rpc_system_gui_start_virtual_display_process(const PB_Main* request,
|
||||
rpc_gui);
|
||||
|
||||
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(
|
||||
rpc_gui->virtual_display_view_port,
|
||||
rpc_system_gui_virtual_display_input_callback,
|
||||
@@ -464,4 +464,4 @@ void rpc_system_gui_free(void* context) {
|
||||
}
|
||||
furi_record_close(RECORD_GUI);
|
||||
free(rpc_gui);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user