Move some "system" apps to apps repo
Because: - They aren't fundamental to firmware function - They are not in firmware repo for other CFW either - Easier to keep updated Moved: - Hex Viewer - IR Remote - Mass Storage - Nightstand Clock - SubGHz Playlist - SubGHz Remote - Text Viewer HID app (USB/BT Remote) could be moved too, but UL and OFW have it here Makes sense to keep HID app in FW repo for updating FindMy Flipper integrates with firmware for startup, better kept in FW
@@ -1,9 +1,9 @@
|
||||
#include "archive_files.h"
|
||||
#include "archive_apps.h"
|
||||
#include "archive_browser.h"
|
||||
#include <applications/system/subghz_playlist/playlist_file.h>
|
||||
#include <applications/system/subghz_remote/subghz_remote_app_i.h>
|
||||
#include <applications/system/ir_remote/infrared_remote.h>
|
||||
#include <applications/external/subghz_playlist/playlist_file.h>
|
||||
#include <applications/external/subghz_remote/subghz_remote_app_i.h>
|
||||
#include <applications/external/ir_remote/infrared_remote.h>
|
||||
|
||||
#define TAG "Archive"
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Roman Shchekin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,17 +0,0 @@
|
||||
App(
|
||||
appid="hex_viewer",
|
||||
name="HEX Viewer",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="hex_viewer_app",
|
||||
requires=[
|
||||
"gui",
|
||||
"dialogs",
|
||||
],
|
||||
stack_size=2 * 1024,
|
||||
fap_icon="icons/hex_10px.bmp",
|
||||
fap_icon_assets="icons",
|
||||
fap_category="Tools",
|
||||
fap_author="@QtRoS",
|
||||
fap_version="2.0",
|
||||
fap_description="App allows to view various files as HEX",
|
||||
)
|
||||
@@ -1,62 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
typedef enum {
|
||||
HexViewerCustomEventStartscreenUp,
|
||||
HexViewerCustomEventStartscreenDown,
|
||||
HexViewerCustomEventStartscreenLeft,
|
||||
HexViewerCustomEventStartscreenRight,
|
||||
HexViewerCustomEventStartscreenOk,
|
||||
HexViewerCustomEventStartscreenBack,
|
||||
HexViewerCustomEventScene1Up,
|
||||
HexViewerCustomEventScene1Down,
|
||||
HexViewerCustomEventScene1Left,
|
||||
HexViewerCustomEventScene1Right,
|
||||
HexViewerCustomEventScene1Ok,
|
||||
HexViewerCustomEventScene1Back,
|
||||
HexViewerCustomEventScene2Up,
|
||||
HexViewerCustomEventScene2Down,
|
||||
HexViewerCustomEventScene2Left,
|
||||
HexViewerCustomEventScene2Right,
|
||||
HexViewerCustomEventScene2Ok,
|
||||
HexViewerCustomEventScene2Back,
|
||||
} HexViewerCustomEvent;
|
||||
|
||||
enum HexViewerCustomEventType {
|
||||
// Reserve first 100 events for button types and indexes, starting from 0
|
||||
HexViewerCustomEventMenuVoid,
|
||||
HexViewerCustomEventMenuSelected,
|
||||
HexViewerCustomEventMenuPercentEntered,
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef union {
|
||||
uint32_t packed_value;
|
||||
struct {
|
||||
uint16_t type;
|
||||
int16_t value;
|
||||
} content;
|
||||
} HexViewerCustomEventMenu;
|
||||
#pragma pack(pop)
|
||||
|
||||
static inline uint32_t hex_viewer_custom_menu_event_pack(uint16_t type, int16_t value) {
|
||||
HexViewerCustomEventMenu event = {.content = {.type = type, .value = value}};
|
||||
return event.packed_value;
|
||||
}
|
||||
static inline void
|
||||
hex_viewer_custom_menu_event_unpack(uint32_t packed_value, uint16_t* type, int16_t* value) {
|
||||
HexViewerCustomEventMenu event = {.packed_value = packed_value};
|
||||
if(type) *type = event.content.type;
|
||||
if(value) *value = event.content.value;
|
||||
}
|
||||
|
||||
static inline uint16_t hex_viewer_custom_menu_event_get_type(uint32_t packed_value) {
|
||||
uint16_t type;
|
||||
hex_viewer_custom_menu_event_unpack(packed_value, &type, NULL);
|
||||
return type;
|
||||
}
|
||||
|
||||
static inline int16_t hex_viewer_custom_menu_event_get_value(uint32_t packed_value) {
|
||||
int16_t value;
|
||||
hex_viewer_custom_menu_event_unpack(packed_value, NULL, &value);
|
||||
return value;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#include "hex_viewer_haptic.h"
|
||||
#include "../hex_viewer.h"
|
||||
|
||||
void hex_viewer_play_happy_bump(void* context) {
|
||||
HexViewer* app = context;
|
||||
if(app->haptic != 1) {
|
||||
return;
|
||||
}
|
||||
notification_message(app->notification, &sequence_set_vibro_on);
|
||||
furi_thread_flags_wait(0, FuriFlagWaitAny, 20);
|
||||
notification_message(app->notification, &sequence_reset_vibro);
|
||||
}
|
||||
|
||||
void hex_viewer_play_bad_bump(void* context) {
|
||||
HexViewer* app = context;
|
||||
if(app->haptic != 1) {
|
||||
return;
|
||||
}
|
||||
notification_message(app->notification, &sequence_set_vibro_on);
|
||||
furi_thread_flags_wait(0, FuriFlagWaitAny, 100);
|
||||
notification_message(app->notification, &sequence_reset_vibro);
|
||||
}
|
||||
|
||||
void hex_viewer_play_long_bump(void* context) {
|
||||
HexViewer* app = context;
|
||||
if(app->haptic != 1) {
|
||||
return;
|
||||
}
|
||||
for(int i = 0; i < 4; i++) {
|
||||
notification_message(app->notification, &sequence_set_vibro_on);
|
||||
furi_thread_flags_wait(0, FuriFlagWaitAny, 50);
|
||||
notification_message(app->notification, &sequence_reset_vibro);
|
||||
furi_thread_flags_wait(0, FuriFlagWaitAny, 100);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#include <notification/notification_messages.h>
|
||||
|
||||
void hex_viewer_play_happy_bump(void* context);
|
||||
|
||||
void hex_viewer_play_bad_bump(void* context);
|
||||
|
||||
void hex_viewer_play_long_bump(void* context);
|
||||
@@ -1,39 +0,0 @@
|
||||
#include "hex_viewer_led.h"
|
||||
#include "../hex_viewer.h"
|
||||
|
||||
void hex_viewer_led_set_rgb(void* context, int red, int green, int blue) {
|
||||
HexViewer* app = context;
|
||||
if(app->led != 1) {
|
||||
return;
|
||||
}
|
||||
NotificationMessage notification_led_message_1;
|
||||
notification_led_message_1.type = NotificationMessageTypeLedRed;
|
||||
NotificationMessage notification_led_message_2;
|
||||
notification_led_message_2.type = NotificationMessageTypeLedGreen;
|
||||
NotificationMessage notification_led_message_3;
|
||||
notification_led_message_3.type = NotificationMessageTypeLedBlue;
|
||||
|
||||
notification_led_message_1.data.led.value = red;
|
||||
notification_led_message_2.data.led.value = green;
|
||||
notification_led_message_3.data.led.value = blue;
|
||||
const NotificationSequence notification_sequence = {
|
||||
¬ification_led_message_1,
|
||||
¬ification_led_message_2,
|
||||
¬ification_led_message_3,
|
||||
&message_do_not_reset,
|
||||
NULL,
|
||||
};
|
||||
notification_message(app->notification, ¬ification_sequence);
|
||||
furi_thread_flags_wait(
|
||||
0, FuriFlagWaitAny, 10); //Delay, prevent removal from RAM before LED value set
|
||||
}
|
||||
|
||||
void hex_viewer_led_reset(void* context) {
|
||||
HexViewer* app = context;
|
||||
notification_message(app->notification, &sequence_reset_red);
|
||||
notification_message(app->notification, &sequence_reset_green);
|
||||
notification_message(app->notification, &sequence_reset_blue);
|
||||
|
||||
furi_thread_flags_wait(
|
||||
0, FuriFlagWaitAny, 300); //Delay, prevent removal from RAM before LED value set
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
|
||||
void hex_viewer_led_set_rgb(void* context, int red, int green, int blue);
|
||||
|
||||
void hex_viewer_led_reset(void* context);
|
||||
@@ -1,26 +0,0 @@
|
||||
#include "hex_viewer_speaker.h"
|
||||
#include "../hex_viewer.h"
|
||||
|
||||
#define NOTE_INPUT 587.33f
|
||||
|
||||
void hex_viewer_play_input_sound(void* context) {
|
||||
HexViewer* app = context;
|
||||
if(app->speaker != 1) {
|
||||
return;
|
||||
}
|
||||
float volume = 1.0f;
|
||||
if(furi_hal_speaker_is_mine() || furi_hal_speaker_acquire(30)) {
|
||||
furi_hal_speaker_start(NOTE_INPUT, volume);
|
||||
}
|
||||
}
|
||||
|
||||
void hex_viewer_stop_all_sound(void* context) {
|
||||
HexViewer* app = context;
|
||||
if(app->speaker != 1) {
|
||||
return;
|
||||
}
|
||||
if(furi_hal_speaker_is_mine()) {
|
||||
furi_hal_speaker_stop();
|
||||
furi_hal_speaker_release();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#define NOTE_INPUT 587.33f
|
||||
|
||||
void hex_viewer_play_input_sound(void* context);
|
||||
void hex_viewer_stop_all_sound(void* context);
|
||||
@@ -1,173 +0,0 @@
|
||||
#include "hex_viewer_storage.h"
|
||||
|
||||
static Storage* hex_viewer_open_storage() {
|
||||
return furi_record_open(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
static void hex_viewer_close_storage() {
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
static void hex_viewer_close_config_file(FlipperFormat* file) {
|
||||
if(file == NULL) return;
|
||||
flipper_format_file_close(file);
|
||||
flipper_format_free(file);
|
||||
}
|
||||
|
||||
void hex_viewer_save_settings(void* context) {
|
||||
HexViewer* app = context;
|
||||
if(app->save_settings == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
FURI_LOG_D(TAG, "Saving Settings");
|
||||
Storage* storage = hex_viewer_open_storage();
|
||||
FlipperFormat* fff_file = flipper_format_file_alloc(storage);
|
||||
|
||||
// Overwrite wont work, so delete first
|
||||
if(storage_file_exists(storage, HEX_VIEWER_SETTINGS_SAVE_PATH)) {
|
||||
storage_simply_remove(storage, HEX_VIEWER_SETTINGS_SAVE_PATH);
|
||||
}
|
||||
|
||||
// Open File, create if not exists
|
||||
if(!storage_common_stat(storage, HEX_VIEWER_SETTINGS_SAVE_PATH, NULL) == FSE_OK) {
|
||||
FURI_LOG_D(
|
||||
TAG, "Config file %s is not found. Will create new.", HEX_VIEWER_SETTINGS_SAVE_PATH);
|
||||
if(storage_common_stat(storage, CONFIG_FILE_DIRECTORY_PATH, NULL) == FSE_NOT_EXIST) {
|
||||
FURI_LOG_D(
|
||||
TAG, "Directory %s doesn't exist. Will create new.", CONFIG_FILE_DIRECTORY_PATH);
|
||||
if(!storage_simply_mkdir(storage, CONFIG_FILE_DIRECTORY_PATH)) {
|
||||
FURI_LOG_E(TAG, "Error creating directory %s", CONFIG_FILE_DIRECTORY_PATH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_file_open_new(fff_file, HEX_VIEWER_SETTINGS_SAVE_PATH)) {
|
||||
//totp_close_config_file(fff_file);
|
||||
FURI_LOG_E(TAG, "Error creating new file %s", HEX_VIEWER_SETTINGS_SAVE_PATH);
|
||||
hex_viewer_close_storage();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store Settings
|
||||
flipper_format_write_header_cstr(
|
||||
fff_file, HEX_VIEWER_SETTINGS_HEADER, HEX_VIEWER_SETTINGS_FILE_VERSION);
|
||||
flipper_format_write_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_HAPTIC, &app->haptic, 1);
|
||||
flipper_format_write_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_SPEAKER, &app->speaker, 1);
|
||||
flipper_format_write_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_LED, &app->led, 1);
|
||||
flipper_format_write_uint32(
|
||||
fff_file, HEX_VIEWER_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1);
|
||||
|
||||
if(!flipper_format_rewind(fff_file)) {
|
||||
hex_viewer_close_config_file(fff_file);
|
||||
FURI_LOG_E(TAG, "Rewind error");
|
||||
hex_viewer_close_storage();
|
||||
return;
|
||||
}
|
||||
|
||||
hex_viewer_close_config_file(fff_file);
|
||||
hex_viewer_close_storage();
|
||||
}
|
||||
|
||||
void hex_viewer_read_settings(void* context) {
|
||||
HexViewer* app = context;
|
||||
Storage* storage = hex_viewer_open_storage();
|
||||
FlipperFormat* fff_file = flipper_format_file_alloc(storage);
|
||||
|
||||
if(storage_common_stat(storage, HEX_VIEWER_SETTINGS_SAVE_PATH, NULL) != FSE_OK) {
|
||||
hex_viewer_close_config_file(fff_file);
|
||||
hex_viewer_close_storage();
|
||||
return;
|
||||
}
|
||||
uint32_t file_version;
|
||||
FuriString* temp_str = furi_string_alloc();
|
||||
|
||||
if(!flipper_format_file_open_existing(fff_file, HEX_VIEWER_SETTINGS_SAVE_PATH)) {
|
||||
FURI_LOG_E(TAG, "Cannot open file %s", HEX_VIEWER_SETTINGS_SAVE_PATH);
|
||||
hex_viewer_close_config_file(fff_file);
|
||||
hex_viewer_close_storage();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!flipper_format_read_header(fff_file, temp_str, &file_version)) {
|
||||
FURI_LOG_E(TAG, "Missing Header Data");
|
||||
hex_viewer_close_config_file(fff_file);
|
||||
hex_viewer_close_storage();
|
||||
furi_string_free(temp_str);
|
||||
return;
|
||||
}
|
||||
furi_string_free(temp_str);
|
||||
|
||||
if(file_version < HEX_VIEWER_SETTINGS_FILE_VERSION) {
|
||||
FURI_LOG_I(TAG, "old config version, will be removed.");
|
||||
hex_viewer_close_config_file(fff_file);
|
||||
hex_viewer_close_storage();
|
||||
return;
|
||||
}
|
||||
|
||||
flipper_format_read_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_HAPTIC, &app->haptic, 1);
|
||||
flipper_format_read_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_SPEAKER, &app->speaker, 1);
|
||||
flipper_format_read_uint32(fff_file, HEX_VIEWER_SETTINGS_KEY_LED, &app->led, 1);
|
||||
flipper_format_read_uint32(
|
||||
fff_file, HEX_VIEWER_SETTINGS_KEY_SAVE_SETTINGS, &app->save_settings, 1);
|
||||
|
||||
flipper_format_rewind(fff_file);
|
||||
|
||||
hex_viewer_close_config_file(fff_file);
|
||||
hex_viewer_close_storage();
|
||||
}
|
||||
|
||||
bool hex_viewer_open_file(void* context, const char* file_path) {
|
||||
HexViewer* hex_viewer = context;
|
||||
furi_assert(hex_viewer);
|
||||
furi_assert(file_path);
|
||||
|
||||
// TODO Separate function?
|
||||
if(hex_viewer->model->stream) {
|
||||
buffered_file_stream_close(hex_viewer->model->stream);
|
||||
stream_free(hex_viewer->model->stream);
|
||||
hex_viewer->model->file_offset = 0;
|
||||
}
|
||||
|
||||
hex_viewer->model->stream = buffered_file_stream_alloc(hex_viewer->storage);
|
||||
bool isOk = true;
|
||||
|
||||
do {
|
||||
if(!buffered_file_stream_open(
|
||||
hex_viewer->model->stream, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
FURI_LOG_E(TAG, "Unable to open stream: %s", file_path);
|
||||
isOk = false;
|
||||
break;
|
||||
};
|
||||
|
||||
hex_viewer->model->file_size = stream_size(hex_viewer->model->stream);
|
||||
} while(false);
|
||||
|
||||
return isOk;
|
||||
}
|
||||
|
||||
bool hex_viewer_read_file(void* context) {
|
||||
HexViewer* hex_viewer = context;
|
||||
furi_assert(hex_viewer);
|
||||
furi_assert(hex_viewer->model->stream);
|
||||
furi_assert(hex_viewer->model->file_offset % HEX_VIEWER_BYTES_PER_LINE == 0);
|
||||
|
||||
memset(hex_viewer->model->file_bytes, 0x0, HEX_VIEWER_BUF_SIZE);
|
||||
bool isOk = true;
|
||||
|
||||
do {
|
||||
uint32_t offset = hex_viewer->model->file_offset;
|
||||
if(!stream_seek(hex_viewer->model->stream, offset, true)) {
|
||||
FURI_LOG_E(TAG, "Unable to seek stream");
|
||||
isOk = false;
|
||||
break;
|
||||
}
|
||||
|
||||
hex_viewer->model->file_read_bytes = stream_read(
|
||||
hex_viewer->model->stream,
|
||||
(uint8_t*)hex_viewer->model->file_bytes,
|
||||
HEX_VIEWER_BUF_SIZE);
|
||||
} while(false);
|
||||
|
||||
return isOk;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <storage/storage.h>
|
||||
#include <flipper_format/flipper_format_i.h>
|
||||
#include "../hex_viewer.h"
|
||||
|
||||
#define HEX_VIEWER_SETTINGS_FILE_VERSION 1
|
||||
#define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("apps_data/hex_viewer")
|
||||
#define HEX_VIEWER_SETTINGS_SAVE_PATH CONFIG_FILE_DIRECTORY_PATH "/hex_viewer.conf"
|
||||
#define HEX_VIEWER_SETTINGS_SAVE_PATH_TMP HEX_VIEWER_SETTINGS_SAVE_PATH ".tmp"
|
||||
#define HEX_VIEWER_SETTINGS_HEADER "HexViewer Config File"
|
||||
#define HEX_VIEWER_SETTINGS_KEY_HAPTIC "Haptic"
|
||||
#define HEX_VIEWER_SETTINGS_KEY_LED "Led"
|
||||
#define HEX_VIEWER_SETTINGS_KEY_SPEAKER "Speaker"
|
||||
#define HEX_VIEWER_SETTINGS_KEY_SAVE_SETTINGS "SaveSettings"
|
||||
|
||||
void hex_viewer_save_settings(void* context);
|
||||
void hex_viewer_read_settings(void* context);
|
||||
|
||||
bool hex_viewer_open_file(void* context, const char* file_path);
|
||||
bool hex_viewer_read_file(void* context);
|
||||
@@ -1,149 +0,0 @@
|
||||
#include "hex_viewer.h"
|
||||
|
||||
bool hex_viewer_custom_event_callback(void* context, uint32_t event) {
|
||||
furi_assert(context);
|
||||
HexViewer* app = context;
|
||||
return scene_manager_handle_custom_event(app->scene_manager, event);
|
||||
}
|
||||
|
||||
void hex_viewer_tick_event_callback(void* context) {
|
||||
furi_assert(context);
|
||||
HexViewer* app = context;
|
||||
scene_manager_handle_tick_event(app->scene_manager);
|
||||
}
|
||||
|
||||
//leave app if back button pressed
|
||||
bool hex_viewer_navigation_event_callback(void* context) {
|
||||
furi_assert(context);
|
||||
HexViewer* app = context;
|
||||
return scene_manager_handle_back_event(app->scene_manager);
|
||||
}
|
||||
|
||||
HexViewer* hex_viewer_app_alloc() {
|
||||
HexViewer* app = malloc(sizeof(HexViewer));
|
||||
|
||||
app->model = malloc(sizeof(HexViewerModel));
|
||||
memset(app->model, 0, sizeof(HexViewerModel));
|
||||
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
app->storage = furi_record_open(RECORD_STORAGE);
|
||||
app->notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
|
||||
//Turn backlight on, believe me this makes testing your app easier
|
||||
notification_message(app->notification, &sequence_display_backlight_on);
|
||||
|
||||
//Scene additions
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
view_dispatcher_enable_queue(app->view_dispatcher);
|
||||
|
||||
app->scene_manager = scene_manager_alloc(&hex_viewer_scene_handlers, app);
|
||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||
view_dispatcher_set_navigation_event_callback(
|
||||
app->view_dispatcher, hex_viewer_navigation_event_callback);
|
||||
view_dispatcher_set_tick_event_callback(
|
||||
app->view_dispatcher, hex_viewer_tick_event_callback, 100);
|
||||
view_dispatcher_set_custom_event_callback(
|
||||
app->view_dispatcher, hex_viewer_custom_event_callback);
|
||||
|
||||
// Set defaults, in case no config loaded
|
||||
app->haptic = 1;
|
||||
app->speaker = 1;
|
||||
app->led = 1;
|
||||
app->save_settings = 1;
|
||||
|
||||
// Used for File Browser
|
||||
app->dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
app->file_path = furi_string_alloc();
|
||||
|
||||
// Load configs
|
||||
hex_viewer_read_settings(app);
|
||||
|
||||
app->submenu = submenu_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, HexViewerViewIdMenu, submenu_get_view(app->submenu));
|
||||
|
||||
app->hex_viewer_startscreen = hex_viewer_startscreen_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher,
|
||||
HexViewerViewIdStartscreen,
|
||||
hex_viewer_startscreen_get_view(app->hex_viewer_startscreen));
|
||||
|
||||
app->text_input = text_input_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, HexViewerViewIdScroll, text_input_get_view(app->text_input));
|
||||
|
||||
app->variable_item_list = variable_item_list_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher,
|
||||
HexViewerViewIdSettings,
|
||||
variable_item_list_get_view(app->variable_item_list));
|
||||
|
||||
//End Scene Additions
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
void hex_viewer_app_free(HexViewer* app) {
|
||||
furi_assert(app);
|
||||
|
||||
if(app->model->stream) {
|
||||
buffered_file_stream_close(app->model->stream);
|
||||
stream_free(app->model->stream);
|
||||
}
|
||||
|
||||
// Scene manager
|
||||
scene_manager_free(app->scene_manager);
|
||||
|
||||
// View Dispatcher
|
||||
view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdMenu);
|
||||
submenu_free(app->submenu);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdStartscreen);
|
||||
hex_viewer_startscreen_free(app->hex_viewer_startscreen);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdScroll);
|
||||
text_input_free(app->text_input);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, HexViewerViewIdSettings);
|
||||
variable_item_list_free(app->variable_item_list);
|
||||
|
||||
view_dispatcher_free(app->view_dispatcher);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
app->storage = NULL;
|
||||
app->gui = NULL;
|
||||
app->notification = NULL;
|
||||
|
||||
// Close File Browser
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
furi_string_free(app->file_path);
|
||||
|
||||
free(app->model);
|
||||
|
||||
//Remove whatever is left
|
||||
free(app);
|
||||
}
|
||||
|
||||
int32_t hex_viewer_app(void* p) {
|
||||
UNUSED(p);
|
||||
HexViewer* app = hex_viewer_app_alloc();
|
||||
|
||||
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||
|
||||
if(p && strlen(p) && hex_viewer_open_file(app, (const char*)p)) {
|
||||
hex_viewer_read_file(app);
|
||||
scene_manager_next_scene(app->scene_manager, HexViewerSceneStartscreen);
|
||||
} else {
|
||||
scene_manager_next_scene(app->scene_manager, HexViewerSceneStartscreen);
|
||||
scene_manager_next_scene(app->scene_manager, HexViewerSceneOpen);
|
||||
}
|
||||
|
||||
furi_hal_power_suppress_charge_enter();
|
||||
|
||||
view_dispatcher_run(app->view_dispatcher);
|
||||
|
||||
hex_viewer_save_settings(app);
|
||||
|
||||
furi_hal_power_suppress_charge_exit();
|
||||
hex_viewer_app_free(app);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <stdlib.h>
|
||||
#include <hex_viewer_icons.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/modules/text_input.h>
|
||||
#include <gui/scene_manager.h>
|
||||
#include <gui/modules/variable_item_list.h>
|
||||
#include <gui/modules/button_menu.h>
|
||||
#include <gui/modules/dialog_ex.h>
|
||||
#include "scenes/hex_viewer_scene.h"
|
||||
#include "views/hex_viewer_startscreen.h"
|
||||
#include "helpers/hex_viewer_storage.h"
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <stream/stream.h>
|
||||
#include <stream/buffered_file_stream.h>
|
||||
#include <toolbox/stream/file_stream.h>
|
||||
|
||||
#define TAG "HexViewer"
|
||||
|
||||
#define HEX_VIEWER_APP_PATH_FOLDER "/any" // TODO ANY_PATH
|
||||
#define HEX_VIEWER_APP_EXTENSION "*"
|
||||
#define HEX_VIEWER_PERCENT_INPUT 16
|
||||
|
||||
#define HEX_VIEWER_BYTES_PER_LINE 4u
|
||||
#define HEX_VIEWER_LINES_ON_SCREEN 4u
|
||||
#define HEX_VIEWER_BUF_SIZE (HEX_VIEWER_LINES_ON_SCREEN * HEX_VIEWER_BYTES_PER_LINE)
|
||||
|
||||
typedef struct {
|
||||
uint8_t file_bytes[HEX_VIEWER_LINES_ON_SCREEN][HEX_VIEWER_BYTES_PER_LINE];
|
||||
uint32_t file_offset;
|
||||
uint32_t file_read_bytes;
|
||||
uint32_t file_size;
|
||||
|
||||
Stream* stream;
|
||||
} HexViewerModel;
|
||||
|
||||
typedef struct {
|
||||
HexViewerModel* model;
|
||||
|
||||
Gui* gui;
|
||||
Storage* storage;
|
||||
NotificationApp* notification;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
Submenu* submenu;
|
||||
TextInput* text_input;
|
||||
SceneManager* scene_manager;
|
||||
VariableItemList* variable_item_list;
|
||||
HexViewerStartscreen* hex_viewer_startscreen;
|
||||
DialogsApp* dialogs; // File Browser
|
||||
FuriString* file_path; // File Browser
|
||||
uint32_t haptic;
|
||||
uint32_t speaker;
|
||||
uint32_t led;
|
||||
uint32_t save_settings;
|
||||
char percent_buf[HEX_VIEWER_PERCENT_INPUT];
|
||||
} HexViewer;
|
||||
|
||||
typedef enum {
|
||||
HexViewerViewIdStartscreen,
|
||||
HexViewerViewIdMenu,
|
||||
HexViewerViewIdScroll,
|
||||
HexViewerViewIdSettings,
|
||||
} HexViewerViewId;
|
||||
|
||||
typedef enum {
|
||||
HexViewerHapticOff,
|
||||
HexViewerHapticOn,
|
||||
} HexViewerHapticState;
|
||||
|
||||
typedef enum {
|
||||
HexViewerSpeakerOff,
|
||||
HexViewerSpeakerOn,
|
||||
} HexViewerSpeakerState;
|
||||
|
||||
typedef enum {
|
||||
HexViewerLedOff,
|
||||
HexViewerLedOn,
|
||||
} HexViewerLedState;
|
||||
|
||||
typedef enum {
|
||||
HexViewerSettingsOff,
|
||||
HexViewerSettingsOn,
|
||||
} HexViewerSettingsStoreState;
|
||||
|
Before Width: | Height: | Size: 102 B |
|
Before Width: | Height: | Size: 171 B |
@@ -1,30 +0,0 @@
|
||||
#include "hex_viewer_scene.h"
|
||||
|
||||
// Generate scene on_enter handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
|
||||
void (*const hex_viewer_on_enter_handlers[])(void*) = {
|
||||
#include "hex_viewer_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_event handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
|
||||
bool (*const hex_viewer_on_event_handlers[])(void* context, SceneManagerEvent event) = {
|
||||
#include "hex_viewer_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_exit handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
|
||||
void (*const hex_viewer_on_exit_handlers[])(void* context) = {
|
||||
#include "hex_viewer_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Initialize scene handlers configuration structure
|
||||
const SceneManagerHandlers hex_viewer_scene_handlers = {
|
||||
.on_enter_handlers = hex_viewer_on_enter_handlers,
|
||||
.on_event_handlers = hex_viewer_on_event_handlers,
|
||||
.on_exit_handlers = hex_viewer_on_exit_handlers,
|
||||
.scene_num = HexViewerSceneNum,
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/scene_manager.h>
|
||||
|
||||
// Generate scene id and total number
|
||||
#define ADD_SCENE(prefix, name, id) HexViewerScene##id,
|
||||
typedef enum {
|
||||
#include "hex_viewer_scene_config.h"
|
||||
HexViewerSceneNum,
|
||||
} HexViewerScene;
|
||||
#undef ADD_SCENE
|
||||
|
||||
extern const SceneManagerHandlers hex_viewer_scene_handlers;
|
||||
|
||||
// Generate scene on_enter handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
|
||||
#include "hex_viewer_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_event handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) \
|
||||
bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
|
||||
#include "hex_viewer_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_exit handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
|
||||
#include "hex_viewer_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
@@ -1,6 +0,0 @@
|
||||
ADD_SCENE(hex_viewer, startscreen, Startscreen)
|
||||
ADD_SCENE(hex_viewer, menu, Menu)
|
||||
ADD_SCENE(hex_viewer, scroll, Scroll)
|
||||
ADD_SCENE(hex_viewer, info, Info)
|
||||
ADD_SCENE(hex_viewer, open, Open)
|
||||
ADD_SCENE(hex_viewer, settings, Settings)
|
||||
@@ -1,42 +0,0 @@
|
||||
#include "../hex_viewer.h"
|
||||
|
||||
void hex_viewer_scene_info_on_enter(void* context) {
|
||||
furi_assert(context);
|
||||
HexViewer* app = context;
|
||||
|
||||
FuriString* buffer;
|
||||
buffer = furi_string_alloc();
|
||||
furi_string_printf(
|
||||
buffer,
|
||||
"File path: %s\nFile size: %lu (0x%lX)",
|
||||
furi_string_get_cstr(app->file_path),
|
||||
app->model->file_size,
|
||||
app->model->file_size);
|
||||
|
||||
DialogMessage* message = dialog_message_alloc();
|
||||
dialog_message_set_header(message, "Hex Viewer v2.0", 16, 2, AlignLeft, AlignTop);
|
||||
dialog_message_set_icon(message, &I_hex_10px, 3, 2);
|
||||
dialog_message_set_text(message, furi_string_get_cstr(buffer), 3, 16, AlignLeft, AlignTop);
|
||||
dialog_message_set_buttons(message, NULL, NULL, "Back");
|
||||
dialog_message_show(app->dialogs, message);
|
||||
|
||||
furi_string_free(buffer);
|
||||
dialog_message_free(message);
|
||||
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, HexViewerViewIdStartscreen);
|
||||
}
|
||||
|
||||
bool hex_viewer_scene_info_on_event(void* context, SceneManagerEvent event) {
|
||||
HexViewer* app = context;
|
||||
UNUSED(app);
|
||||
UNUSED(event);
|
||||
bool consumed = true;
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void hex_viewer_scene_info_on_exit(void* context) {
|
||||
HexViewer* app = context;
|
||||
UNUSED(app);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
#include "../hex_viewer.h"
|
||||
|
||||
enum SubmenuIndex {
|
||||
SubmenuIndexScroll = 10,
|
||||
SubmenuIndexInfo,
|
||||
SubmenuIndexOpen,
|
||||
// SubmenuIndexSettings,
|
||||
};
|
||||
|
||||
void hex_viewer_scene_menu_submenu_callback(void* context, uint32_t index) {
|
||||
HexViewer* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void hex_viewer_scene_menu_on_enter(void* context) {
|
||||
HexViewer* app = context;
|
||||
|
||||
submenu_set_header(app->submenu, "Select action");
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Open file ...",
|
||||
SubmenuIndexOpen,
|
||||
hex_viewer_scene_menu_submenu_callback,
|
||||
app);
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Scroll to ...",
|
||||
SubmenuIndexScroll,
|
||||
hex_viewer_scene_menu_submenu_callback,
|
||||
app);
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Show info ...",
|
||||
SubmenuIndexInfo,
|
||||
hex_viewer_scene_menu_submenu_callback,
|
||||
app);
|
||||
// submenu_add_item(app->submenu, "Settings", SubmenuIndexSettings, hex_viewer_scene_menu_submenu_callback, app);
|
||||
|
||||
submenu_set_selected_item(
|
||||
app->submenu, scene_manager_get_scene_state(app->scene_manager, HexViewerSceneMenu));
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerViewIdMenu);
|
||||
}
|
||||
|
||||
bool hex_viewer_scene_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
HexViewer* app = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeBack) {
|
||||
//exit app
|
||||
// scene_manager_stop(app->scene_manager);
|
||||
// view_dispatcher_stop(app->view_dispatcher);
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
return true;
|
||||
} else if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == SubmenuIndexScroll) {
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, HexViewerSceneMenu, SubmenuIndexScroll);
|
||||
scene_manager_next_scene(app->scene_manager, HexViewerSceneScroll);
|
||||
return true;
|
||||
} else if(event.event == SubmenuIndexInfo) {
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, HexViewerSceneMenu, SubmenuIndexInfo);
|
||||
scene_manager_next_scene(app->scene_manager, HexViewerSceneInfo);
|
||||
return true;
|
||||
} else if(event.event == SubmenuIndexOpen) {
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, HexViewerSceneMenu, SubmenuIndexOpen);
|
||||
scene_manager_next_scene(app->scene_manager, HexViewerSceneOpen);
|
||||
// } else if (event.event == SubmenuIndexSettings) {
|
||||
// scene_manager_set_scene_state(
|
||||
// app->scene_manager, HexViewerSceneMenu, SubmenuIndexSettings);
|
||||
// scene_manager_next_scene(app->scene_manager, HexViewerSceneSettings);
|
||||
// return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void hex_viewer_scene_menu_on_exit(void* context) {
|
||||
HexViewer* app = context;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#include "../hex_viewer.h"
|
||||
|
||||
void hex_viewer_scene_open_on_enter(void* context) {
|
||||
furi_assert(context);
|
||||
HexViewer* app = context;
|
||||
|
||||
FuriString* initial_path;
|
||||
initial_path = furi_string_alloc();
|
||||
furi_string_set(initial_path, HEX_VIEWER_APP_PATH_FOLDER);
|
||||
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
dialog_file_browser_set_basic_options(&browser_options, HEX_VIEWER_APP_EXTENSION, &I_hex_10px);
|
||||
browser_options.hide_ext = false;
|
||||
|
||||
bool success =
|
||||
dialog_file_browser_show(app->dialogs, app->file_path, initial_path, &browser_options);
|
||||
furi_string_free(initial_path);
|
||||
|
||||
if(success) {
|
||||
success = hex_viewer_open_file(app, furi_string_get_cstr(app->file_path));
|
||||
if(success) hex_viewer_read_file(app);
|
||||
}
|
||||
|
||||
if(success) {
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, HexViewerViewIdStartscreen);
|
||||
} else {
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
}
|
||||
}
|
||||
|
||||
bool hex_viewer_scene_open_on_event(void* context, SceneManagerEvent event) {
|
||||
UNUSED(context);
|
||||
UNUSED(event);
|
||||
bool consumed = true;
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void hex_viewer_scene_open_on_exit(void* context) {
|
||||
UNUSED(context);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#include "../hex_viewer.h"
|
||||
#include "../helpers/hex_viewer_custom_event.h"
|
||||
|
||||
void hex_viewer_scene_scroll_callback(void* context) {
|
||||
HexViewer* app = (HexViewer*)context;
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, HexViewerCustomEventMenuPercentEntered);
|
||||
}
|
||||
|
||||
void hex_viewer_scene_scroll_on_enter(void* context) {
|
||||
furi_assert(context);
|
||||
HexViewer* app = context;
|
||||
|
||||
TextInput* text_input = app->text_input;
|
||||
|
||||
text_input_set_header_text(text_input, "Scroll to percentage (0..100)");
|
||||
text_input_set_result_callback(
|
||||
text_input,
|
||||
hex_viewer_scene_scroll_callback,
|
||||
app,
|
||||
app->percent_buf,
|
||||
HEX_VIEWER_PERCENT_INPUT,
|
||||
false);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerSceneScroll);
|
||||
}
|
||||
|
||||
bool hex_viewer_scene_scroll_on_event(void* context, SceneManagerEvent event) {
|
||||
HexViewer* app = (HexViewer*)context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == HexViewerCustomEventMenuPercentEntered) {
|
||||
int ipercent = atoi(app->percent_buf);
|
||||
ipercent = MIN(ipercent, 100);
|
||||
ipercent = MAX(ipercent, 0);
|
||||
float percent = ipercent / 100.0;
|
||||
|
||||
uint32_t line_count = app->model->file_size / HEX_VIEWER_BYTES_PER_LINE;
|
||||
if(app->model->file_size % HEX_VIEWER_BYTES_PER_LINE != 0) line_count += 1;
|
||||
uint32_t scrollable_lines = line_count - HEX_VIEWER_LINES_ON_SCREEN;
|
||||
uint32_t target_line = (uint32_t)(percent * scrollable_lines);
|
||||
|
||||
uint32_t new_file_offset = target_line * HEX_VIEWER_BYTES_PER_LINE;
|
||||
if(app->model->file_size > new_file_offset) {
|
||||
app->model->file_offset = new_file_offset;
|
||||
if(!hex_viewer_read_file(app)) new_file_offset = new_file_offset; // TODO Do smth
|
||||
}
|
||||
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, HexViewerViewIdStartscreen);
|
||||
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void hex_viewer_scene_scroll_on_exit(void* context) {
|
||||
UNUSED(context);
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
#include "../hex_viewer.h"
|
||||
#include <lib/toolbox/value_index.h>
|
||||
|
||||
enum SettingsIndex {
|
||||
SettingsIndexHaptic = 10,
|
||||
SettingsIndexValue1,
|
||||
SettingsIndexValue2,
|
||||
};
|
||||
|
||||
const char* const haptic_text[2] = {
|
||||
"OFF",
|
||||
"ON",
|
||||
};
|
||||
const uint32_t haptic_value[2] = {
|
||||
HexViewerHapticOff,
|
||||
HexViewerHapticOn,
|
||||
};
|
||||
|
||||
const char* const speaker_text[2] = {
|
||||
"OFF",
|
||||
"ON",
|
||||
};
|
||||
const uint32_t speaker_value[2] = {
|
||||
HexViewerSpeakerOff,
|
||||
HexViewerSpeakerOn,
|
||||
};
|
||||
|
||||
const char* const led_text[2] = {
|
||||
"OFF",
|
||||
"ON",
|
||||
};
|
||||
const uint32_t led_value[2] = {
|
||||
HexViewerLedOff,
|
||||
HexViewerLedOn,
|
||||
};
|
||||
|
||||
const char* const settings_text[2] = {
|
||||
"OFF",
|
||||
"ON",
|
||||
};
|
||||
const uint32_t settings_value[2] = {
|
||||
HexViewerSettingsOff,
|
||||
HexViewerSettingsOn,
|
||||
};
|
||||
|
||||
static void hex_viewer_scene_settings_set_haptic(VariableItem* item) {
|
||||
HexViewer* app = variable_item_get_context(item);
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
|
||||
variable_item_set_current_value_text(item, haptic_text[index]);
|
||||
app->haptic = haptic_value[index];
|
||||
}
|
||||
|
||||
static void hex_viewer_scene_settings_set_speaker(VariableItem* item) {
|
||||
HexViewer* app = variable_item_get_context(item);
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, speaker_text[index]);
|
||||
app->speaker = speaker_value[index];
|
||||
}
|
||||
|
||||
static void hex_viewer_scene_settings_set_led(VariableItem* item) {
|
||||
HexViewer* app = variable_item_get_context(item);
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, led_text[index]);
|
||||
app->led = led_value[index];
|
||||
}
|
||||
|
||||
static void hex_viewer_scene_settings_set_save_settings(VariableItem* item) {
|
||||
HexViewer* app = variable_item_get_context(item);
|
||||
uint8_t index = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, settings_text[index]);
|
||||
app->save_settings = settings_value[index];
|
||||
}
|
||||
|
||||
void hex_viewer_scene_settings_submenu_callback(void* context, uint32_t index) {
|
||||
HexViewer* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void hex_viewer_scene_settings_on_enter(void* context) {
|
||||
HexViewer* app = context;
|
||||
VariableItem* item;
|
||||
uint8_t value_index;
|
||||
|
||||
// Vibro on/off
|
||||
item = variable_item_list_add(
|
||||
app->variable_item_list, "Vibro/Haptic:", 2, hex_viewer_scene_settings_set_haptic, app);
|
||||
value_index = value_index_uint32(app->haptic, haptic_value, 2);
|
||||
variable_item_set_current_value_index(item, value_index);
|
||||
variable_item_set_current_value_text(item, haptic_text[value_index]);
|
||||
|
||||
// Sound on/off
|
||||
item = variable_item_list_add(
|
||||
app->variable_item_list, "Sound:", 2, hex_viewer_scene_settings_set_speaker, app);
|
||||
value_index = value_index_uint32(app->speaker, speaker_value, 2);
|
||||
variable_item_set_current_value_index(item, value_index);
|
||||
variable_item_set_current_value_text(item, speaker_text[value_index]);
|
||||
|
||||
// LED Effects on/off
|
||||
item = variable_item_list_add(
|
||||
app->variable_item_list, "LED FX:", 2, hex_viewer_scene_settings_set_led, app);
|
||||
value_index = value_index_uint32(app->led, led_value, 2);
|
||||
variable_item_set_current_value_index(item, value_index);
|
||||
variable_item_set_current_value_text(item, led_text[value_index]);
|
||||
|
||||
// Save Settings to File
|
||||
item = variable_item_list_add(
|
||||
app->variable_item_list,
|
||||
"Save Settings",
|
||||
2,
|
||||
hex_viewer_scene_settings_set_save_settings,
|
||||
app);
|
||||
value_index = value_index_uint32(app->save_settings, settings_value, 2);
|
||||
variable_item_set_current_value_index(item, value_index);
|
||||
variable_item_set_current_value_text(item, settings_text[value_index]);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerViewIdSettings);
|
||||
}
|
||||
|
||||
bool hex_viewer_scene_settings_on_event(void* context, SceneManagerEvent event) {
|
||||
HexViewer* app = context;
|
||||
UNUSED(app);
|
||||
bool consumed = false;
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void hex_viewer_scene_settings_on_exit(void* context) {
|
||||
HexViewer* app = context;
|
||||
variable_item_list_set_selected_item(app->variable_item_list, 0);
|
||||
variable_item_list_reset(app->variable_item_list);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
#include "../hex_viewer.h"
|
||||
#include "../helpers/hex_viewer_custom_event.h"
|
||||
#include "../views/hex_viewer_startscreen.h"
|
||||
|
||||
void hex_viewer_scene_startscreen_callback(HexViewerCustomEvent event, void* context) {
|
||||
furi_assert(context);
|
||||
HexViewer* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, event);
|
||||
}
|
||||
|
||||
void hex_viewer_scene_startscreen_on_enter(void* context) {
|
||||
furi_assert(context);
|
||||
HexViewer* app = context;
|
||||
hex_viewer_startscreen_set_callback(
|
||||
app->hex_viewer_startscreen, hex_viewer_scene_startscreen_callback, app);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, HexViewerViewIdStartscreen);
|
||||
}
|
||||
|
||||
bool hex_viewer_scene_startscreen_on_event(void* context, SceneManagerEvent event) {
|
||||
HexViewer* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
switch(event.event) {
|
||||
case HexViewerCustomEventStartscreenLeft:
|
||||
//app->model->mode = !app->model->mode;
|
||||
consumed = true;
|
||||
break;
|
||||
case HexViewerCustomEventStartscreenRight:
|
||||
consumed = true;
|
||||
break;
|
||||
case HexViewerCustomEventStartscreenUp:
|
||||
consumed = true;
|
||||
break;
|
||||
case HexViewerCustomEventStartscreenDown:
|
||||
consumed = true;
|
||||
break;
|
||||
case HexViewerCustomEventStartscreenOk:
|
||||
if(!app->model->file_size)
|
||||
scene_manager_next_scene(app->scene_manager, HexViewerSceneOpen);
|
||||
else
|
||||
scene_manager_next_scene(app->scene_manager, HexViewerSceneMenu);
|
||||
consumed = true;
|
||||
break;
|
||||
case HexViewerCustomEventStartscreenBack: // TODO Delete
|
||||
notification_message(app->notification, &sequence_reset_red);
|
||||
notification_message(app->notification, &sequence_reset_green);
|
||||
notification_message(app->notification, &sequence_reset_blue);
|
||||
if(!scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, HexViewerSceneStartscreen)) {
|
||||
scene_manager_stop(app->scene_manager);
|
||||
view_dispatcher_stop(app->view_dispatcher);
|
||||
}
|
||||
consumed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void hex_viewer_scene_startscreen_on_exit(void* context) {
|
||||
HexViewer* app = context;
|
||||
UNUSED(app);
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
#include "../hex_viewer.h"
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <input/input.h>
|
||||
#include <gui/elements.h>
|
||||
|
||||
struct HexViewerStartscreen {
|
||||
View* view;
|
||||
HexViewerStartscreenCallback callback;
|
||||
void* context;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint8_t file_bytes[HEX_VIEWER_LINES_ON_SCREEN][HEX_VIEWER_BYTES_PER_LINE];
|
||||
uint32_t file_offset;
|
||||
uint32_t file_read_bytes;
|
||||
uint32_t file_size;
|
||||
bool mode;
|
||||
uint32_t dbg;
|
||||
} HexViewerStartscreenModel;
|
||||
|
||||
void hex_viewer_startscreen_set_callback(
|
||||
HexViewerStartscreen* instance,
|
||||
HexViewerStartscreenCallback callback,
|
||||
void* context) {
|
||||
furi_assert(instance);
|
||||
furi_assert(callback);
|
||||
instance->callback = callback;
|
||||
instance->context = context;
|
||||
}
|
||||
|
||||
void hex_viewer_startscreen_draw(Canvas* canvas, HexViewerStartscreenModel* model) {
|
||||
canvas_clear(canvas);
|
||||
|
||||
if(!model->file_size) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignTop, "HexViewer v2.0");
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(canvas, 64, 22, AlignCenter, AlignTop, "Basic hex viewer");
|
||||
canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignTop, "for your Flipper");
|
||||
elements_button_center(canvas, "Open");
|
||||
} else {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
elements_button_left(canvas, model->mode ? "Addr" : "Text");
|
||||
//elements_button_right(canvas, "Info");
|
||||
elements_button_center(canvas, "Menu");
|
||||
|
||||
int ROW_HEIGHT = 12;
|
||||
int TOP_OFFSET = 10;
|
||||
int LEFT_OFFSET = 3;
|
||||
|
||||
uint32_t line_count = model->file_size / HEX_VIEWER_BYTES_PER_LINE;
|
||||
if(model->file_size % HEX_VIEWER_BYTES_PER_LINE != 0) line_count += 1;
|
||||
uint32_t first_line_on_screen = model->file_offset / HEX_VIEWER_BYTES_PER_LINE;
|
||||
if(line_count > HEX_VIEWER_LINES_ON_SCREEN) {
|
||||
uint8_t width = canvas_width(canvas);
|
||||
elements_scrollbar_pos(
|
||||
canvas,
|
||||
width,
|
||||
0,
|
||||
ROW_HEIGHT * HEX_VIEWER_LINES_ON_SCREEN,
|
||||
first_line_on_screen,
|
||||
line_count - (HEX_VIEWER_LINES_ON_SCREEN - 1));
|
||||
}
|
||||
|
||||
char temp_buf[32];
|
||||
uint32_t row_iters = model->file_read_bytes / HEX_VIEWER_BYTES_PER_LINE;
|
||||
if(model->file_read_bytes % HEX_VIEWER_BYTES_PER_LINE != 0) row_iters += 1;
|
||||
|
||||
// For the rest of drawing.
|
||||
canvas_set_font(canvas, FontKeyboard);
|
||||
|
||||
for(uint32_t i = 0; i < row_iters; ++i) {
|
||||
uint32_t bytes_left_per_row = model->file_read_bytes - i * HEX_VIEWER_BYTES_PER_LINE;
|
||||
bytes_left_per_row = MIN(bytes_left_per_row, HEX_VIEWER_BYTES_PER_LINE);
|
||||
|
||||
if(model->mode) {
|
||||
memcpy(temp_buf, model->file_bytes[i], bytes_left_per_row);
|
||||
temp_buf[bytes_left_per_row] = '\0';
|
||||
for(uint32_t j = 0; j < bytes_left_per_row; ++j)
|
||||
if(!isprint((int)temp_buf[j])) temp_buf[j] = '.';
|
||||
|
||||
//canvas_set_font(canvas, FontKeyboard);
|
||||
canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf);
|
||||
} else {
|
||||
uint32_t addr = model->file_offset + i * HEX_VIEWER_BYTES_PER_LINE;
|
||||
snprintf(temp_buf, 32, "%04lX", addr);
|
||||
|
||||
//canvas_set_font(canvas, FontKeyboard);
|
||||
canvas_draw_str(canvas, LEFT_OFFSET, TOP_OFFSET + i * ROW_HEIGHT, temp_buf);
|
||||
}
|
||||
|
||||
char* p = temp_buf;
|
||||
for(uint32_t j = 0; j < bytes_left_per_row; ++j)
|
||||
p += snprintf(p, 32, "%02X ", model->file_bytes[i][j]);
|
||||
|
||||
//canvas_set_font(canvas, FontKeyboard);
|
||||
canvas_draw_str(canvas, LEFT_OFFSET + 41, TOP_OFFSET + i * ROW_HEIGHT, temp_buf);
|
||||
}
|
||||
|
||||
// Poor man's debug
|
||||
// snprintf(temp_buf, 32, "D %02lX", model->dbg);
|
||||
// elements_button_right(canvas, temp_buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void hex_viewer_startscreen_model_init(HexViewerStartscreenModel* const model) {
|
||||
memset(model->file_bytes, 0, sizeof(model->file_bytes));
|
||||
model->file_offset = 0;
|
||||
model->file_read_bytes = 0;
|
||||
model->file_size = 0;
|
||||
model->mode = false;
|
||||
model->dbg = 0;
|
||||
}
|
||||
|
||||
static void
|
||||
update_local_model_from_app(HexViewer* const app, HexViewerStartscreenModel* const model) {
|
||||
memcpy(model->file_bytes, app->model->file_bytes, sizeof(model->file_bytes));
|
||||
model->file_offset = app->model->file_offset;
|
||||
model->file_read_bytes = app->model->file_read_bytes;
|
||||
model->file_size = app->model->file_size;
|
||||
//model->mode = app->model->mode;
|
||||
}
|
||||
|
||||
bool hex_viewer_startscreen_input(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
HexViewerStartscreen* instance = context;
|
||||
HexViewer* app = instance->context; // TO so good, but works
|
||||
|
||||
if(event->type == InputTypeRelease || event->type == InputTypeRepeat) {
|
||||
switch(event->key) {
|
||||
case InputKeyBack:
|
||||
with_view_model(
|
||||
instance->view,
|
||||
HexViewerStartscreenModel * model,
|
||||
{
|
||||
instance->callback(HexViewerCustomEventStartscreenBack, instance->context);
|
||||
update_local_model_from_app(instance->context, model);
|
||||
},
|
||||
true);
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
with_view_model(
|
||||
instance->view,
|
||||
HexViewerStartscreenModel * model,
|
||||
{ model->mode = !model->mode; },
|
||||
true);
|
||||
break;
|
||||
case InputKeyRight:
|
||||
with_view_model(
|
||||
instance->view, HexViewerStartscreenModel * model, { model->dbg = 0; }, true);
|
||||
break;
|
||||
case InputKeyUp:
|
||||
with_view_model(
|
||||
instance->view,
|
||||
HexViewerStartscreenModel * model,
|
||||
{
|
||||
if(app->model->file_offset > 0) {
|
||||
app->model->file_offset -= HEX_VIEWER_BYTES_PER_LINE;
|
||||
if(!hex_viewer_read_file(app)) break; // TODO Do smth
|
||||
}
|
||||
|
||||
update_local_model_from_app(instance->context, model);
|
||||
},
|
||||
true);
|
||||
break;
|
||||
case InputKeyDown:
|
||||
with_view_model(
|
||||
instance->view,
|
||||
HexViewerStartscreenModel * model,
|
||||
{
|
||||
uint32_t last_byte_on_screen =
|
||||
app->model->file_offset + app->model->file_read_bytes;
|
||||
if(app->model->file_size > last_byte_on_screen) {
|
||||
app->model->file_offset += HEX_VIEWER_BYTES_PER_LINE;
|
||||
if(!hex_viewer_read_file(app)) break; // TODO Do smth
|
||||
}
|
||||
|
||||
update_local_model_from_app(instance->context, model);
|
||||
},
|
||||
true);
|
||||
break;
|
||||
case InputKeyOk:
|
||||
with_view_model(
|
||||
instance->view,
|
||||
HexViewerStartscreenModel * model,
|
||||
{
|
||||
instance->callback(HexViewerCustomEventStartscreenOk, instance->context);
|
||||
update_local_model_from_app(instance->context, model);
|
||||
},
|
||||
true);
|
||||
break;
|
||||
case InputKeyMAX:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void hex_viewer_startscreen_exit(void* context) {
|
||||
furi_assert(context);
|
||||
}
|
||||
|
||||
void hex_viewer_startscreen_enter(void* context) {
|
||||
furi_assert(context);
|
||||
HexViewerStartscreen* instance = (HexViewerStartscreen*)context;
|
||||
with_view_model(
|
||||
instance->view,
|
||||
HexViewerStartscreenModel * model,
|
||||
{ update_local_model_from_app(instance->context, model); },
|
||||
true);
|
||||
}
|
||||
|
||||
HexViewerStartscreen* hex_viewer_startscreen_alloc() {
|
||||
HexViewerStartscreen* instance = malloc(sizeof(HexViewerStartscreen));
|
||||
instance->view = view_alloc();
|
||||
view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(HexViewerStartscreenModel));
|
||||
view_set_context(instance->view, instance);
|
||||
view_set_draw_callback(instance->view, (ViewDrawCallback)hex_viewer_startscreen_draw);
|
||||
view_set_input_callback(instance->view, hex_viewer_startscreen_input);
|
||||
view_set_enter_callback(instance->view, hex_viewer_startscreen_enter);
|
||||
view_set_exit_callback(instance->view, hex_viewer_startscreen_exit);
|
||||
|
||||
with_view_model(
|
||||
instance->view,
|
||||
HexViewerStartscreenModel * model,
|
||||
{ hex_viewer_startscreen_model_init(model); },
|
||||
true);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
void hex_viewer_startscreen_free(HexViewerStartscreen* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
with_view_model(
|
||||
instance->view, HexViewerStartscreenModel * model, { UNUSED(model); }, true);
|
||||
view_free(instance->view);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
View* hex_viewer_startscreen_get_view(HexViewerStartscreen* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->view;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
#include "../helpers/hex_viewer_custom_event.h"
|
||||
|
||||
typedef struct HexViewerStartscreen HexViewerStartscreen;
|
||||
|
||||
typedef void (*HexViewerStartscreenCallback)(HexViewerCustomEvent event, void* context);
|
||||
|
||||
void hex_viewer_startscreen_set_callback(
|
||||
HexViewerStartscreen* hex_viewer_startscreen,
|
||||
HexViewerStartscreenCallback callback,
|
||||
void* context);
|
||||
|
||||
View* hex_viewer_startscreen_get_view(HexViewerStartscreen* hex_viewer_static);
|
||||
|
||||
HexViewerStartscreen* hex_viewer_startscreen_alloc();
|
||||
|
||||
void hex_viewer_startscreen_free(HexViewerStartscreen* hex_viewer_static);
|
||||
@@ -1,18 +0,0 @@
|
||||
App(
|
||||
appid="ir_remote",
|
||||
name="IR Remote",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="infrared_remote_app",
|
||||
stack_size=3 * 1024,
|
||||
requires=[
|
||||
"gui",
|
||||
"dialogs",
|
||||
],
|
||||
fap_category="Infrared",
|
||||
fap_icon="ir_10px.png",
|
||||
fap_icon_assets="images",
|
||||
fap_author="@Hong5489 & @friebel & @d4ve10",
|
||||
fap_weburl="https://github.com/Hong5489/ir_remote",
|
||||
fap_version="1.0",
|
||||
fap_description="Bind any IR remote button to each button on flipper d-pad, provides another way to use flipper as IR remote.",
|
||||
)
|
||||
@@ -1,188 +0,0 @@
|
||||
#include "infrared_remote.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <m-array.h>
|
||||
#include <toolbox/path.h>
|
||||
#include <storage/storage.h>
|
||||
#include <core/common_defines.h>
|
||||
|
||||
#define TAG "InfraredRemote"
|
||||
|
||||
ARRAY_DEF(InfraredButtonArray, InfraredRemoteButton*, M_PTR_OPLIST);
|
||||
|
||||
struct InfraredRemote {
|
||||
InfraredButtonArray_t buttons;
|
||||
FuriString* name;
|
||||
FuriString* path;
|
||||
};
|
||||
|
||||
static void infrared_remote_clear_buttons(InfraredRemote* remote) {
|
||||
InfraredButtonArray_it_t it;
|
||||
for(InfraredButtonArray_it(it, remote->buttons); !InfraredButtonArray_end_p(it);
|
||||
InfraredButtonArray_next(it)) {
|
||||
infrared_remote_button_free(*InfraredButtonArray_cref(it));
|
||||
}
|
||||
InfraredButtonArray_reset(remote->buttons);
|
||||
}
|
||||
|
||||
InfraredRemote* infrared_remote_alloc(void) {
|
||||
InfraredRemote* remote = malloc(sizeof(InfraredRemote));
|
||||
InfraredButtonArray_init(remote->buttons);
|
||||
remote->name = furi_string_alloc();
|
||||
remote->path = furi_string_alloc();
|
||||
return remote;
|
||||
}
|
||||
|
||||
void infrared_remote_free(InfraredRemote* remote) {
|
||||
infrared_remote_clear_buttons(remote);
|
||||
InfraredButtonArray_clear(remote->buttons);
|
||||
furi_string_free(remote->path);
|
||||
furi_string_free(remote->name);
|
||||
free(remote);
|
||||
}
|
||||
|
||||
void infrared_remote_reset(InfraredRemote* remote) {
|
||||
infrared_remote_clear_buttons(remote);
|
||||
furi_string_reset(remote->name);
|
||||
furi_string_reset(remote->path);
|
||||
}
|
||||
|
||||
void infrared_remote_set_name(InfraredRemote* remote, const char* name) {
|
||||
furi_string_set(remote->name, name);
|
||||
}
|
||||
|
||||
const char* infrared_remote_get_name(InfraredRemote* remote) {
|
||||
return furi_string_get_cstr(remote->name);
|
||||
}
|
||||
|
||||
void infrared_remote_set_path(InfraredRemote* remote, const char* path) {
|
||||
furi_string_set(remote->path, path);
|
||||
}
|
||||
|
||||
const char* infrared_remote_get_path(InfraredRemote* remote) {
|
||||
return furi_string_get_cstr(remote->path);
|
||||
}
|
||||
|
||||
size_t infrared_remote_get_button_count(InfraredRemote* remote) {
|
||||
return InfraredButtonArray_size(remote->buttons);
|
||||
}
|
||||
|
||||
InfraredRemoteButton* infrared_remote_get_button(InfraredRemote* remote, size_t index) {
|
||||
furi_assert(index < InfraredButtonArray_size(remote->buttons));
|
||||
return *InfraredButtonArray_get(remote->buttons, index);
|
||||
}
|
||||
|
||||
bool infrared_remote_find_button_by_name(InfraredRemote* remote, const char* name, size_t* index) {
|
||||
for(size_t i = 0; i < InfraredButtonArray_size(remote->buttons); i++) {
|
||||
InfraredRemoteButton* button = *InfraredButtonArray_get(remote->buttons, i);
|
||||
if(!strcmp(infrared_remote_button_get_name(button), name)) {
|
||||
*index = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool infrared_remote_add_button(InfraredRemote* remote, const char* name, InfraredSignal* signal) {
|
||||
InfraredRemoteButton* button = infrared_remote_button_alloc();
|
||||
infrared_remote_button_set_name(button, name);
|
||||
infrared_remote_button_set_signal(button, signal);
|
||||
InfraredButtonArray_push_back(remote->buttons, button);
|
||||
return infrared_remote_store(remote);
|
||||
}
|
||||
|
||||
bool infrared_remote_rename_button(InfraredRemote* remote, const char* new_name, size_t index) {
|
||||
furi_assert(index < InfraredButtonArray_size(remote->buttons));
|
||||
InfraredRemoteButton* button = *InfraredButtonArray_get(remote->buttons, index);
|
||||
infrared_remote_button_set_name(button, new_name);
|
||||
return infrared_remote_store(remote);
|
||||
}
|
||||
|
||||
bool infrared_remote_delete_button(InfraredRemote* remote, size_t index) {
|
||||
furi_assert(index < InfraredButtonArray_size(remote->buttons));
|
||||
InfraredRemoteButton* button;
|
||||
InfraredButtonArray_pop_at(&button, remote->buttons, index);
|
||||
infrared_remote_button_free(button);
|
||||
return infrared_remote_store(remote);
|
||||
}
|
||||
|
||||
bool infrared_remote_store(InfraredRemote* remote) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* ff = flipper_format_file_alloc(storage);
|
||||
const char* path = furi_string_get_cstr(remote->path);
|
||||
|
||||
FURI_LOG_I(TAG, "store file: \'%s\'", path);
|
||||
|
||||
bool success = flipper_format_file_open_always(ff, path) &&
|
||||
flipper_format_write_header_cstr(ff, "IR signals file", 1);
|
||||
if(success) {
|
||||
InfraredButtonArray_it_t it;
|
||||
for(InfraredButtonArray_it(it, remote->buttons); !InfraredButtonArray_end_p(it);
|
||||
InfraredButtonArray_next(it)) {
|
||||
InfraredRemoteButton* button = *InfraredButtonArray_cref(it);
|
||||
success = infrared_signal_save(
|
||||
infrared_remote_button_get_signal(button),
|
||||
ff,
|
||||
infrared_remote_button_get_name(button));
|
||||
if(!success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flipper_format_free(ff);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return success;
|
||||
}
|
||||
|
||||
bool infrared_remote_load(InfraredRemote* remote, FuriString* path) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* ff = flipper_format_buffered_file_alloc(storage);
|
||||
|
||||
FuriString* buf;
|
||||
buf = furi_string_alloc();
|
||||
|
||||
FURI_LOG_I(TAG, "load file: \'%s\'", furi_string_get_cstr(path));
|
||||
bool success = flipper_format_buffered_file_open_existing(ff, furi_string_get_cstr(path));
|
||||
|
||||
if(success) {
|
||||
uint32_t version;
|
||||
success = flipper_format_read_header(ff, buf, &version) &&
|
||||
!furi_string_cmp(buf, "IR signals file") && (version == 1);
|
||||
}
|
||||
|
||||
if(success) {
|
||||
path_extract_filename(path, buf, true);
|
||||
infrared_remote_clear_buttons(remote);
|
||||
infrared_remote_set_name(remote, furi_string_get_cstr(buf));
|
||||
infrared_remote_set_path(remote, furi_string_get_cstr(path));
|
||||
|
||||
for(bool can_read = true; can_read;) {
|
||||
InfraredRemoteButton* button = infrared_remote_button_alloc();
|
||||
can_read = infrared_signal_read(infrared_remote_button_get_signal(button), ff, buf);
|
||||
if(can_read) {
|
||||
infrared_remote_button_set_name(button, furi_string_get_cstr(buf));
|
||||
InfraredButtonArray_push_back(remote->buttons, button);
|
||||
} else {
|
||||
infrared_remote_button_free(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_free(buf);
|
||||
flipper_format_free(ff);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return success;
|
||||
}
|
||||
|
||||
bool infrared_remote_remove(InfraredRemote* remote) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
FS_Error status = storage_common_remove(storage, furi_string_get_cstr(remote->path));
|
||||
infrared_remote_reset(remote);
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return (status == FSE_OK || status == FSE_NOT_EXIST);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "infrared_remote_button.h"
|
||||
|
||||
#define IR_REMOTE_PATH EXT_PATH("infrared/remote")
|
||||
|
||||
typedef struct InfraredRemote InfraredRemote;
|
||||
|
||||
InfraredRemote* infrared_remote_alloc(void);
|
||||
void infrared_remote_free(InfraredRemote* remote);
|
||||
void infrared_remote_reset(InfraredRemote* remote);
|
||||
|
||||
void infrared_remote_set_name(InfraredRemote* remote, const char* name);
|
||||
const char* infrared_remote_get_name(InfraredRemote* remote);
|
||||
|
||||
void infrared_remote_set_path(InfraredRemote* remote, const char* path);
|
||||
const char* infrared_remote_get_path(InfraredRemote* remote);
|
||||
|
||||
size_t infrared_remote_get_button_count(InfraredRemote* remote);
|
||||
InfraredRemoteButton* infrared_remote_get_button(InfraredRemote* remote, size_t index);
|
||||
bool infrared_remote_find_button_by_name(InfraredRemote* remote, const char* name, size_t* index);
|
||||
|
||||
bool infrared_remote_add_button(InfraredRemote* remote, const char* name, InfraredSignal* signal);
|
||||
bool infrared_remote_rename_button(InfraredRemote* remote, const char* new_name, size_t index);
|
||||
bool infrared_remote_delete_button(InfraredRemote* remote, size_t index);
|
||||
|
||||
bool infrared_remote_store(InfraredRemote* remote);
|
||||
bool infrared_remote_load(InfraredRemote* remote, FuriString* path);
|
||||
bool infrared_remote_remove(InfraredRemote* remote);
|
||||
@@ -1,611 +0,0 @@
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
|
||||
#include <infrared_worker.h>
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <ir_remote_icons.h>
|
||||
#include <assets_icons.h>
|
||||
|
||||
#include <infrared/infrared_app.h>
|
||||
#include <toolbox/saved_struct.h>
|
||||
|
||||
#include <notification/notification.h>
|
||||
#include <notification/notification_messages.h>
|
||||
|
||||
#include "infrared_signal.h"
|
||||
#include "infrared_remote.h"
|
||||
#include "infrared_remote_button.h"
|
||||
#define TAG "ir_remote"
|
||||
|
||||
#include <flipper_format/flipper_format.h>
|
||||
|
||||
typedef struct {
|
||||
int status;
|
||||
ViewPort* view_port;
|
||||
FuriString* up_button;
|
||||
FuriString* down_button;
|
||||
FuriString* left_button;
|
||||
FuriString* right_button;
|
||||
FuriString* ok_button;
|
||||
FuriString* back_button;
|
||||
FuriString* up_hold_button;
|
||||
FuriString* down_hold_button;
|
||||
FuriString* left_hold_button;
|
||||
FuriString* right_hold_button;
|
||||
FuriString* ok_hold_button;
|
||||
InfraredWorker* infrared_worker;
|
||||
} IRApp;
|
||||
|
||||
// Screen is 128x64 px
|
||||
static void app_draw_callback(Canvas* canvas, void* ctx) {
|
||||
// Show config is incorrect when cannot read the remote file
|
||||
// Showing button string in the screen, upper part is short press, lower part is long press
|
||||
IRApp* app = ctx;
|
||||
if(app->status) {
|
||||
canvas_clear(canvas);
|
||||
view_port_set_orientation(app->view_port, ViewPortOrientationHorizontal);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(canvas, 62, 5, AlignCenter, AlignTop, "Config is incorrect.");
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(canvas, 62, 30, AlignCenter, AlignTop, "Please configure map.");
|
||||
canvas_draw_str_aligned(canvas, 62, 60, AlignCenter, AlignBottom, "Press Back to Exit.");
|
||||
} else {
|
||||
canvas_clear(canvas);
|
||||
view_port_set_orientation(app->view_port, ViewPortOrientationVertical);
|
||||
canvas_draw_icon(canvas, 1, 5, &I_ButtonUp_7x4);
|
||||
canvas_draw_icon(canvas, 1, 15, &I_ButtonDown_7x4);
|
||||
canvas_draw_icon(canvas, 2, 23, &I_ButtonLeft_4x7);
|
||||
canvas_draw_icon(canvas, 2, 33, &I_ButtonRight_4x7);
|
||||
canvas_draw_icon(canvas, 0, 42, &I_Ok_btn_9x9);
|
||||
canvas_draw_icon(canvas, 0, 53, &I_back_10px);
|
||||
|
||||
//Labels
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 8, AlignCenter, AlignCenter, furi_string_get_cstr(app->up_button));
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 18, AlignCenter, AlignCenter, furi_string_get_cstr(app->down_button));
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 28, AlignCenter, AlignCenter, furi_string_get_cstr(app->left_button));
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 38, AlignCenter, AlignCenter, furi_string_get_cstr(app->right_button));
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 48, AlignCenter, AlignCenter, furi_string_get_cstr(app->ok_button));
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 58, AlignCenter, AlignCenter, furi_string_get_cstr(app->back_button));
|
||||
|
||||
canvas_draw_line(canvas, 0, 65, 64, 65);
|
||||
|
||||
canvas_draw_icon(canvas, 1, 70, &I_ButtonUp_7x4);
|
||||
canvas_draw_icon(canvas, 1, 80, &I_ButtonDown_7x4);
|
||||
canvas_draw_icon(canvas, 2, 88, &I_ButtonLeft_4x7);
|
||||
canvas_draw_icon(canvas, 2, 98, &I_ButtonRight_4x7);
|
||||
canvas_draw_icon(canvas, 0, 107, &I_Ok_btn_9x9);
|
||||
canvas_draw_icon(canvas, 0, 118, &I_back_10px);
|
||||
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 73, AlignCenter, AlignCenter, furi_string_get_cstr(app->up_hold_button));
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 83, AlignCenter, AlignCenter, furi_string_get_cstr(app->down_hold_button));
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 93, AlignCenter, AlignCenter, furi_string_get_cstr(app->left_hold_button));
|
||||
canvas_draw_str_aligned(
|
||||
canvas,
|
||||
32,
|
||||
103,
|
||||
AlignCenter,
|
||||
AlignCenter,
|
||||
furi_string_get_cstr(app->right_hold_button));
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 32, 113, AlignCenter, AlignCenter, furi_string_get_cstr(app->ok_hold_button));
|
||||
canvas_draw_str_aligned(canvas, 32, 123, AlignCenter, AlignCenter, "Exit App");
|
||||
}
|
||||
}
|
||||
|
||||
static void app_input_callback(InputEvent* input_event, void* ctx) {
|
||||
furi_assert(ctx);
|
||||
|
||||
FuriMessageQueue* event_queue = ctx;
|
||||
furi_message_queue_put(event_queue, input_event, FuriWaitForever);
|
||||
}
|
||||
|
||||
int32_t infrared_remote_app(char* p) {
|
||||
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
|
||||
|
||||
// App button string
|
||||
IRApp* app = malloc(sizeof(IRApp));
|
||||
app->up_button = furi_string_alloc();
|
||||
app->down_button = furi_string_alloc();
|
||||
app->left_button = furi_string_alloc();
|
||||
app->right_button = furi_string_alloc();
|
||||
app->ok_button = furi_string_alloc();
|
||||
app->back_button = furi_string_alloc();
|
||||
app->up_hold_button = furi_string_alloc();
|
||||
app->down_hold_button = furi_string_alloc();
|
||||
app->left_hold_button = furi_string_alloc();
|
||||
app->right_hold_button = furi_string_alloc();
|
||||
app->ok_hold_button = furi_string_alloc();
|
||||
app->view_port = view_port_alloc();
|
||||
app->infrared_worker = infrared_worker_alloc();
|
||||
|
||||
// Configure view port
|
||||
view_port_draw_callback_set(app->view_port, app_draw_callback, app);
|
||||
view_port_input_callback_set(app->view_port, app_input_callback, event_queue);
|
||||
|
||||
// Register view port in GUI
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(gui, app->view_port, GuiLayerFullscreen);
|
||||
|
||||
InputEvent event;
|
||||
|
||||
FuriString* map_file = furi_string_alloc();
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* ff = flipper_format_file_alloc(storage);
|
||||
if(!storage_file_exists(storage, IR_REMOTE_PATH)) {
|
||||
storage_common_mkdir(storage, IR_REMOTE_PATH); //Make Folder If dir not exist
|
||||
}
|
||||
|
||||
bool res;
|
||||
if(p && strlen(p)) {
|
||||
furi_string_set(map_file, p);
|
||||
res = true;
|
||||
} else {
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
dialog_file_browser_set_basic_options(&browser_options, ".txt", &I_sub1_10px);
|
||||
browser_options.base_path = IR_REMOTE_PATH;
|
||||
furi_string_set(map_file, IR_REMOTE_PATH);
|
||||
res = dialog_file_browser_show(dialogs, map_file, map_file, &browser_options);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
}
|
||||
|
||||
// if user didn't choose anything, free everything and exit
|
||||
if(!res) {
|
||||
FURI_LOG_I(TAG, "exit");
|
||||
flipper_format_free(ff);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
furi_string_free(app->up_button);
|
||||
furi_string_free(app->down_button);
|
||||
furi_string_free(app->left_button);
|
||||
furi_string_free(app->right_button);
|
||||
furi_string_free(app->ok_button);
|
||||
furi_string_free(app->back_button);
|
||||
furi_string_free(app->up_hold_button);
|
||||
furi_string_free(app->down_hold_button);
|
||||
furi_string_free(app->left_hold_button);
|
||||
furi_string_free(app->right_hold_button);
|
||||
furi_string_free(app->ok_hold_button);
|
||||
|
||||
view_port_enabled_set(app->view_port, false);
|
||||
gui_remove_view_port(gui, app->view_port);
|
||||
view_port_free(app->view_port);
|
||||
free(app);
|
||||
furi_message_queue_free(event_queue);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
return 255;
|
||||
}
|
||||
|
||||
InfraredRemote* remote = infrared_remote_alloc();
|
||||
FuriString* remote_path = furi_string_alloc();
|
||||
|
||||
InfraredSignal* up_signal = infrared_signal_alloc();
|
||||
InfraredSignal* down_signal = infrared_signal_alloc();
|
||||
InfraredSignal* left_signal = infrared_signal_alloc();
|
||||
InfraredSignal* right_signal = infrared_signal_alloc();
|
||||
InfraredSignal* ok_signal = infrared_signal_alloc();
|
||||
InfraredSignal* back_signal = infrared_signal_alloc();
|
||||
InfraredSignal* up_hold_signal = infrared_signal_alloc();
|
||||
InfraredSignal* down_hold_signal = infrared_signal_alloc();
|
||||
InfraredSignal* left_hold_signal = infrared_signal_alloc();
|
||||
InfraredSignal* right_hold_signal = infrared_signal_alloc();
|
||||
InfraredSignal* ok_hold_signal = infrared_signal_alloc();
|
||||
|
||||
InfraredSignal* active_signal = NULL;
|
||||
bool is_transmitting = false;
|
||||
|
||||
bool up_enabled = false;
|
||||
bool down_enabled = false;
|
||||
bool left_enabled = false;
|
||||
bool right_enabled = false;
|
||||
bool ok_enabled = false;
|
||||
bool back_enabled = false;
|
||||
bool up_hold_enabled = false;
|
||||
bool down_hold_enabled = false;
|
||||
bool left_hold_enabled = false;
|
||||
bool right_hold_enabled = false;
|
||||
bool ok_hold_enabled = false;
|
||||
|
||||
if(!flipper_format_file_open_existing(ff, furi_string_get_cstr(map_file))) {
|
||||
FURI_LOG_E(TAG, "Could not open MAP file %s", furi_string_get_cstr(map_file));
|
||||
app->status = 1;
|
||||
} else {
|
||||
//Filename Assignment/Check Start
|
||||
|
||||
if(!flipper_format_read_string(ff, "REMOTE", remote_path)) {
|
||||
FURI_LOG_E(TAG, "Could not read REMOTE string");
|
||||
app->status = 1;
|
||||
} else {
|
||||
if(!infrared_remote_load(remote, remote_path)) {
|
||||
FURI_LOG_E(TAG, "Could not load ir file: %s", furi_string_get_cstr(remote_path));
|
||||
app->status = 1;
|
||||
} else {
|
||||
FURI_LOG_I(TAG, "Loaded REMOTE file: %s", furi_string_get_cstr(remote_path));
|
||||
}
|
||||
}
|
||||
|
||||
//assign variables to values within map file
|
||||
//set missing filenames to N/A
|
||||
//assign button signals
|
||||
size_t index = 0;
|
||||
if(!flipper_format_read_string(ff, "UP", app->up_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read UP string");
|
||||
furi_string_set(app->up_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->up_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
up_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
up_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "DOWN", app->down_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read DOWN string");
|
||||
furi_string_set(app->down_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->down_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
down_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
down_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "LEFT", app->left_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read LEFT string");
|
||||
furi_string_set(app->left_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->left_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
left_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
left_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "RIGHT", app->right_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read RIGHT string");
|
||||
furi_string_set(app->right_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->right_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
right_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
right_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "OK", app->ok_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read OK string");
|
||||
furi_string_set(app->ok_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->ok_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
ok_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
ok_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "BACK", app->back_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read BACK string");
|
||||
furi_string_set(app->back_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->back_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
back_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
back_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "UPHOLD", app->up_hold_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read UPHOLD string");
|
||||
furi_string_set(app->up_hold_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->up_hold_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
up_hold_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
up_hold_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "DOWNHOLD", app->down_hold_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read DOWNHOLD string");
|
||||
furi_string_set(app->down_hold_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->down_hold_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
down_hold_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
down_hold_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "LEFTHOLD", app->left_hold_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read LEFTHOLD string");
|
||||
furi_string_set(app->left_hold_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->left_hold_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
left_hold_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
left_hold_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "RIGHTHOLD", app->right_hold_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read RIGHTHOLD string");
|
||||
furi_string_set(app->right_hold_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->right_hold_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
right_hold_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
right_hold_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(ff, "OKHOLD", app->ok_hold_button)) {
|
||||
FURI_LOG_W(TAG, "Could not read OKHOLD string");
|
||||
furi_string_set(app->ok_hold_button, "N/A");
|
||||
} else {
|
||||
if(!infrared_remote_find_button_by_name(
|
||||
remote, furi_string_get_cstr(app->ok_hold_button), &index)) {
|
||||
FURI_LOG_W(TAG, "Error");
|
||||
} else {
|
||||
ok_hold_signal =
|
||||
infrared_remote_button_get_signal(infrared_remote_get_button(remote, index));
|
||||
ok_hold_enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_free(remote_path);
|
||||
|
||||
flipper_format_free(ff);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
bool otg_was_enabled = furi_hal_power_is_otg_enabled();
|
||||
InfraredSettings settings = {0};
|
||||
saved_struct_load(
|
||||
INFRARED_SETTINGS_PATH,
|
||||
&settings,
|
||||
sizeof(InfraredSettings),
|
||||
INFRARED_SETTINGS_MAGIC,
|
||||
INFRARED_SETTINGS_VERSION);
|
||||
if(settings.tx_pin < FuriHalInfraredTxPinMax) {
|
||||
furi_hal_infrared_set_tx_output(settings.tx_pin);
|
||||
if(settings.otg_enabled != otg_was_enabled) {
|
||||
if(settings.otg_enabled) {
|
||||
furi_hal_power_enable_otg();
|
||||
} else {
|
||||
furi_hal_power_disable_otg();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FuriHalInfraredTxPin tx_pin_detected = furi_hal_infrared_detect_tx_output();
|
||||
furi_hal_infrared_set_tx_output(tx_pin_detected);
|
||||
if(tx_pin_detected != FuriHalInfraredTxPinInternal) {
|
||||
furi_hal_power_enable_otg();
|
||||
}
|
||||
}
|
||||
|
||||
bool running = true;
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
|
||||
if(app->status) {
|
||||
view_port_update(app->view_port);
|
||||
while(running) {
|
||||
if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
|
||||
if(event.type == InputTypeShort) {
|
||||
switch(event.key) {
|
||||
case InputKeyBack:
|
||||
running = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view_port_update(app->view_port);
|
||||
while(running) {
|
||||
if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
|
||||
// short press signal
|
||||
if(event.type == InputTypeShort) {
|
||||
switch(event.key) {
|
||||
case InputKeyUp:
|
||||
if(up_enabled) {
|
||||
active_signal = up_signal;
|
||||
FURI_LOG_I(TAG, "up");
|
||||
}
|
||||
break;
|
||||
case InputKeyDown:
|
||||
if(down_enabled) {
|
||||
active_signal = down_signal;
|
||||
FURI_LOG_I(TAG, "down");
|
||||
}
|
||||
break;
|
||||
case InputKeyRight:
|
||||
if(right_enabled) {
|
||||
active_signal = right_signal;
|
||||
FURI_LOG_I(TAG, "right");
|
||||
}
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
if(left_enabled) {
|
||||
active_signal = left_signal;
|
||||
FURI_LOG_I(TAG, "left");
|
||||
}
|
||||
break;
|
||||
case InputKeyOk:
|
||||
if(ok_enabled) {
|
||||
active_signal = ok_signal;
|
||||
FURI_LOG_I(TAG, "ok");
|
||||
}
|
||||
break;
|
||||
case InputKeyBack:
|
||||
if(back_enabled) {
|
||||
active_signal = back_signal;
|
||||
FURI_LOG_I(TAG, "back");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
// long press signal
|
||||
} else if(event.type == InputTypeLong) {
|
||||
switch(event.key) {
|
||||
case InputKeyUp:
|
||||
if(up_hold_enabled) {
|
||||
active_signal = up_hold_signal;
|
||||
FURI_LOG_I(TAG, "up!");
|
||||
}
|
||||
break;
|
||||
case InputKeyDown:
|
||||
if(down_hold_enabled) {
|
||||
active_signal = down_hold_signal;
|
||||
FURI_LOG_I(TAG, "down!");
|
||||
}
|
||||
break;
|
||||
case InputKeyRight:
|
||||
if(right_hold_enabled) {
|
||||
active_signal = right_hold_signal;
|
||||
FURI_LOG_I(TAG, "right!");
|
||||
}
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
if(left_hold_enabled) {
|
||||
active_signal = left_hold_signal;
|
||||
FURI_LOG_I(TAG, "left!");
|
||||
}
|
||||
break;
|
||||
case InputKeyOk:
|
||||
if(ok_hold_enabled) {
|
||||
active_signal = ok_hold_signal;
|
||||
FURI_LOG_I(TAG, "ok!");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
} else if(event.type == InputTypeRelease && is_transmitting) {
|
||||
notification_message(notification, &sequence_blink_stop);
|
||||
infrared_worker_tx_stop(app->infrared_worker);
|
||||
is_transmitting = false;
|
||||
active_signal = NULL;
|
||||
}
|
||||
|
||||
if(active_signal != NULL &&
|
||||
(event.type == InputTypeShort || event.type == InputTypeLong)) {
|
||||
if(is_transmitting) {
|
||||
infrared_worker_tx_stop(app->infrared_worker);
|
||||
}
|
||||
|
||||
if(infrared_signal_is_raw(active_signal)) {
|
||||
InfraredRawSignal* raw_signal =
|
||||
infrared_signal_get_raw_signal(active_signal);
|
||||
infrared_worker_set_raw_signal(
|
||||
app->infrared_worker,
|
||||
raw_signal->timings,
|
||||
raw_signal->timings_size,
|
||||
raw_signal->frequency,
|
||||
raw_signal->duty_cycle);
|
||||
} else {
|
||||
InfraredMessage* message = infrared_signal_get_message(active_signal);
|
||||
infrared_worker_set_decoded_signal(app->infrared_worker, message);
|
||||
}
|
||||
|
||||
infrared_worker_tx_set_get_signal_callback(
|
||||
app->infrared_worker, infrared_worker_tx_get_signal_steady_callback, app);
|
||||
|
||||
infrared_worker_tx_start(app->infrared_worker);
|
||||
notification_message(notification, &sequence_blink_start_magenta);
|
||||
is_transmitting = true;
|
||||
}
|
||||
}
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
}
|
||||
|
||||
furi_hal_infrared_set_tx_output(FuriHalInfraredTxPinInternal);
|
||||
if(furi_hal_power_is_otg_enabled() != otg_was_enabled) {
|
||||
if(otg_was_enabled) {
|
||||
furi_hal_power_enable_otg();
|
||||
} else {
|
||||
furi_hal_power_disable_otg();
|
||||
}
|
||||
}
|
||||
|
||||
// Free all things
|
||||
furi_string_free(app->up_button);
|
||||
furi_string_free(app->down_button);
|
||||
furi_string_free(app->left_button);
|
||||
furi_string_free(app->right_button);
|
||||
furi_string_free(app->ok_button);
|
||||
furi_string_free(app->back_button);
|
||||
furi_string_free(app->up_hold_button);
|
||||
furi_string_free(app->down_hold_button);
|
||||
furi_string_free(app->left_hold_button);
|
||||
furi_string_free(app->right_hold_button);
|
||||
furi_string_free(app->ok_hold_button);
|
||||
|
||||
if(is_transmitting) {
|
||||
infrared_worker_tx_stop(app->infrared_worker);
|
||||
notification_message(notification, &sequence_blink_stop);
|
||||
}
|
||||
infrared_worker_free(app->infrared_worker);
|
||||
|
||||
infrared_remote_free(remote);
|
||||
view_port_enabled_set(app->view_port, false);
|
||||
gui_remove_view_port(gui, app->view_port);
|
||||
view_port_free(app->view_port);
|
||||
free(app);
|
||||
furi_message_queue_free(event_queue);
|
||||
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#include "infrared_remote_button.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
struct InfraredRemoteButton {
|
||||
FuriString* name;
|
||||
InfraredSignal* signal;
|
||||
};
|
||||
|
||||
InfraredRemoteButton* infrared_remote_button_alloc(void) {
|
||||
InfraredRemoteButton* button = malloc(sizeof(InfraredRemoteButton));
|
||||
button->name = furi_string_alloc();
|
||||
button->signal = infrared_signal_alloc();
|
||||
return button;
|
||||
}
|
||||
|
||||
void infrared_remote_button_free(InfraredRemoteButton* button) {
|
||||
furi_string_free(button->name);
|
||||
infrared_signal_free(button->signal);
|
||||
free(button);
|
||||
}
|
||||
|
||||
void infrared_remote_button_set_name(InfraredRemoteButton* button, const char* name) {
|
||||
furi_string_set(button->name, name);
|
||||
}
|
||||
|
||||
const char* infrared_remote_button_get_name(InfraredRemoteButton* button) {
|
||||
return furi_string_get_cstr(button->name);
|
||||
}
|
||||
|
||||
void infrared_remote_button_set_signal(InfraredRemoteButton* button, InfraredSignal* signal) {
|
||||
infrared_signal_set_signal(button->signal, signal);
|
||||
}
|
||||
|
||||
InfraredSignal* infrared_remote_button_get_signal(InfraredRemoteButton* button) {
|
||||
return button->signal;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "infrared_signal.h"
|
||||
|
||||
typedef struct InfraredRemoteButton InfraredRemoteButton;
|
||||
|
||||
InfraredRemoteButton* infrared_remote_button_alloc(void);
|
||||
void infrared_remote_button_free(InfraredRemoteButton* button);
|
||||
|
||||
void infrared_remote_button_set_name(InfraredRemoteButton* button, const char* name);
|
||||
const char* infrared_remote_button_get_name(InfraredRemoteButton* button);
|
||||
|
||||
void infrared_remote_button_set_signal(InfraredRemoteButton* button, InfraredSignal* signal);
|
||||
InfraredSignal* infrared_remote_button_get_signal(InfraredRemoteButton* button);
|
||||
@@ -1,300 +0,0 @@
|
||||
#include "infrared_signal.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <core/check.h>
|
||||
#include <lib/infrared/worker/infrared_transmit.h>
|
||||
#include <lib/infrared/worker/infrared_worker.h>
|
||||
|
||||
#define TAG "InfraredSignal"
|
||||
|
||||
struct InfraredSignal {
|
||||
bool is_raw;
|
||||
union {
|
||||
InfraredMessage message;
|
||||
InfraredRawSignal raw;
|
||||
} payload;
|
||||
};
|
||||
|
||||
static void infrared_signal_clear_timings(InfraredSignal* signal) {
|
||||
if(signal->is_raw) {
|
||||
free(signal->payload.raw.timings);
|
||||
signal->payload.raw.timings_size = 0;
|
||||
signal->payload.raw.timings = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static bool infrared_signal_is_message_valid(InfraredMessage* message) {
|
||||
if(!infrared_is_protocol_valid(message->protocol)) {
|
||||
FURI_LOG_E(TAG, "Unknown protocol");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t address_length = infrared_get_protocol_address_length(message->protocol);
|
||||
uint32_t address_mask = (1UL << address_length) - 1;
|
||||
|
||||
if(message->address != (message->address & address_mask)) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"Address is out of range (mask 0x%08lX): 0x%lX\r\n",
|
||||
address_mask,
|
||||
message->address);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t command_length = infrared_get_protocol_command_length(message->protocol);
|
||||
uint32_t command_mask = (1UL << command_length) - 1;
|
||||
|
||||
if(message->command != (message->command & command_mask)) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"Command is out of range (mask 0x%08lX): 0x%lX\r\n",
|
||||
command_mask,
|
||||
message->command);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool infrared_signal_is_raw_valid(InfraredRawSignal* raw) {
|
||||
if((raw->frequency > INFRARED_MAX_FREQUENCY) || (raw->frequency < INFRARED_MIN_FREQUENCY)) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"Frequency is out of range (%X - %X): %lX",
|
||||
INFRARED_MIN_FREQUENCY,
|
||||
INFRARED_MAX_FREQUENCY,
|
||||
raw->frequency);
|
||||
return false;
|
||||
|
||||
} else if((raw->duty_cycle <= 0) || (raw->duty_cycle > 1)) {
|
||||
FURI_LOG_E(TAG, "Duty cycle is out of range (0 - 1): %f", (double)raw->duty_cycle);
|
||||
return false;
|
||||
|
||||
} else if((raw->timings_size <= 0) || (raw->timings_size > MAX_TIMINGS_AMOUNT)) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"Timings amount is out of range (0 - %X): %X",
|
||||
MAX_TIMINGS_AMOUNT,
|
||||
raw->timings_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool infrared_signal_save_message(InfraredMessage* message, FlipperFormat* ff) {
|
||||
const char* protocol_name = infrared_get_protocol_name(message->protocol);
|
||||
return flipper_format_write_string_cstr(ff, "type", "parsed") &&
|
||||
flipper_format_write_string_cstr(ff, "protocol", protocol_name) &&
|
||||
flipper_format_write_hex(ff, "address", (uint8_t*)&message->address, 4) &&
|
||||
flipper_format_write_hex(ff, "command", (uint8_t*)&message->command, 4);
|
||||
}
|
||||
|
||||
static inline bool infrared_signal_save_raw(InfraredRawSignal* raw, FlipperFormat* ff) {
|
||||
furi_assert(raw->timings_size <= MAX_TIMINGS_AMOUNT);
|
||||
return flipper_format_write_string_cstr(ff, "type", "raw") &&
|
||||
flipper_format_write_uint32(ff, "frequency", &raw->frequency, 1) &&
|
||||
flipper_format_write_float(ff, "duty_cycle", &raw->duty_cycle, 1) &&
|
||||
flipper_format_write_uint32(ff, "data", raw->timings, raw->timings_size);
|
||||
}
|
||||
|
||||
static inline bool infrared_signal_read_message(InfraredSignal* signal, FlipperFormat* ff) {
|
||||
FuriString* buf;
|
||||
buf = furi_string_alloc();
|
||||
bool success = false;
|
||||
|
||||
do {
|
||||
if(!flipper_format_read_string(ff, "protocol", buf)) break;
|
||||
|
||||
InfraredMessage message;
|
||||
message.protocol = infrared_get_protocol_by_name(furi_string_get_cstr(buf));
|
||||
|
||||
success = flipper_format_read_hex(ff, "address", (uint8_t*)&message.address, 4) &&
|
||||
flipper_format_read_hex(ff, "command", (uint8_t*)&message.command, 4) &&
|
||||
infrared_signal_is_message_valid(&message);
|
||||
|
||||
if(!success) break;
|
||||
|
||||
infrared_signal_set_message(signal, &message);
|
||||
} while(0);
|
||||
|
||||
furi_string_free(buf);
|
||||
return success;
|
||||
}
|
||||
|
||||
static inline bool infrared_signal_read_raw(InfraredSignal* signal, FlipperFormat* ff) {
|
||||
uint32_t timings_size, frequency;
|
||||
float duty_cycle;
|
||||
|
||||
bool success = flipper_format_read_uint32(ff, "frequency", &frequency, 1) &&
|
||||
flipper_format_read_float(ff, "duty_cycle", &duty_cycle, 1) &&
|
||||
flipper_format_get_value_count(ff, "data", &timings_size);
|
||||
|
||||
if(!success || timings_size > MAX_TIMINGS_AMOUNT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t* timings = malloc(sizeof(uint32_t) * timings_size);
|
||||
success = flipper_format_read_uint32(ff, "data", timings, timings_size);
|
||||
|
||||
if(success) {
|
||||
infrared_signal_set_raw_signal(signal, timings, timings_size, frequency, duty_cycle);
|
||||
}
|
||||
|
||||
free(timings);
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool infrared_signal_read_body(InfraredSignal* signal, FlipperFormat* ff) {
|
||||
FuriString* tmp = furi_string_alloc();
|
||||
|
||||
bool success = false;
|
||||
|
||||
do {
|
||||
if(!flipper_format_read_string(ff, "type", tmp)) break;
|
||||
if(furi_string_equal(tmp, "raw")) {
|
||||
success = infrared_signal_read_raw(signal, ff);
|
||||
} else if(furi_string_equal(tmp, "parsed")) {
|
||||
success = infrared_signal_read_message(signal, ff);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Unknown signal type");
|
||||
}
|
||||
} while(false);
|
||||
|
||||
furi_string_free(tmp);
|
||||
return success;
|
||||
}
|
||||
|
||||
InfraredSignal* infrared_signal_alloc(void) {
|
||||
InfraredSignal* signal = malloc(sizeof(InfraredSignal));
|
||||
|
||||
signal->is_raw = false;
|
||||
signal->payload.message.protocol = InfraredProtocolUnknown;
|
||||
|
||||
return signal;
|
||||
}
|
||||
|
||||
void infrared_signal_free(InfraredSignal* signal) {
|
||||
infrared_signal_clear_timings(signal);
|
||||
free(signal);
|
||||
}
|
||||
|
||||
bool infrared_signal_is_raw(InfraredSignal* signal) {
|
||||
return signal->is_raw;
|
||||
}
|
||||
|
||||
bool infrared_signal_is_valid(InfraredSignal* signal) {
|
||||
return signal->is_raw ? infrared_signal_is_raw_valid(&signal->payload.raw) :
|
||||
infrared_signal_is_message_valid(&signal->payload.message);
|
||||
}
|
||||
|
||||
void infrared_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other) {
|
||||
if(other->is_raw) {
|
||||
const InfraredRawSignal* raw = &other->payload.raw;
|
||||
infrared_signal_set_raw_signal(
|
||||
signal, raw->timings, raw->timings_size, raw->frequency, raw->duty_cycle);
|
||||
} else {
|
||||
const InfraredMessage* message = &other->payload.message;
|
||||
infrared_signal_set_message(signal, message);
|
||||
}
|
||||
}
|
||||
|
||||
void infrared_signal_set_raw_signal(
|
||||
InfraredSignal* signal,
|
||||
const uint32_t* timings,
|
||||
size_t timings_size,
|
||||
uint32_t frequency,
|
||||
float duty_cycle) {
|
||||
infrared_signal_clear_timings(signal);
|
||||
|
||||
signal->is_raw = true;
|
||||
|
||||
signal->payload.raw.timings_size = timings_size;
|
||||
signal->payload.raw.frequency = frequency;
|
||||
signal->payload.raw.duty_cycle = duty_cycle;
|
||||
|
||||
signal->payload.raw.timings = malloc(timings_size * sizeof(uint32_t));
|
||||
memcpy(signal->payload.raw.timings, timings, timings_size * sizeof(uint32_t));
|
||||
}
|
||||
|
||||
InfraredRawSignal* infrared_signal_get_raw_signal(InfraredSignal* signal) {
|
||||
furi_assert(signal->is_raw);
|
||||
return &signal->payload.raw;
|
||||
}
|
||||
|
||||
void infrared_signal_set_message(InfraredSignal* signal, const InfraredMessage* message) {
|
||||
infrared_signal_clear_timings(signal);
|
||||
|
||||
signal->is_raw = false;
|
||||
signal->payload.message = *message;
|
||||
}
|
||||
|
||||
InfraredMessage* infrared_signal_get_message(InfraredSignal* signal) {
|
||||
furi_assert(!signal->is_raw);
|
||||
return &signal->payload.message;
|
||||
}
|
||||
|
||||
bool infrared_signal_save(InfraredSignal* signal, FlipperFormat* ff, const char* name) {
|
||||
if(!flipper_format_write_comment_cstr(ff, "") ||
|
||||
!flipper_format_write_string_cstr(ff, "name", name)) {
|
||||
return false;
|
||||
} else if(signal->is_raw) {
|
||||
return infrared_signal_save_raw(&signal->payload.raw, ff);
|
||||
} else {
|
||||
return infrared_signal_save_message(&signal->payload.message, ff);
|
||||
}
|
||||
}
|
||||
|
||||
bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name) {
|
||||
FuriString* tmp = furi_string_alloc();
|
||||
|
||||
bool success = false;
|
||||
|
||||
do {
|
||||
if(!flipper_format_read_string(ff, "name", tmp)) break;
|
||||
furi_string_set(name, tmp);
|
||||
if(!infrared_signal_read_body(signal, ff)) break;
|
||||
success = true;
|
||||
} while(0);
|
||||
|
||||
furi_string_free(tmp);
|
||||
return success;
|
||||
}
|
||||
|
||||
bool infrared_signal_search_and_read(
|
||||
InfraredSignal* signal,
|
||||
FlipperFormat* ff,
|
||||
const FuriString* name) {
|
||||
bool success = false;
|
||||
FuriString* tmp = furi_string_alloc();
|
||||
|
||||
do {
|
||||
bool is_name_found = false;
|
||||
while(flipper_format_read_string(ff, "name", tmp)) {
|
||||
is_name_found = furi_string_equal(name, tmp);
|
||||
if(is_name_found) break;
|
||||
}
|
||||
if(!is_name_found) break;
|
||||
if(!infrared_signal_read_body(signal, ff)) break;
|
||||
success = true;
|
||||
} while(false);
|
||||
|
||||
furi_string_free(tmp);
|
||||
return success;
|
||||
}
|
||||
|
||||
void infrared_signal_transmit(InfraredSignal* signal) {
|
||||
if(signal->is_raw) {
|
||||
InfraredRawSignal* raw_signal = &signal->payload.raw;
|
||||
infrared_send_raw_ext(
|
||||
raw_signal->timings,
|
||||
raw_signal->timings_size,
|
||||
true,
|
||||
raw_signal->frequency,
|
||||
raw_signal->duty_cycle);
|
||||
} else {
|
||||
InfraredMessage* message = &signal->payload.message;
|
||||
infrared_send(message, 2);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <lib/infrared/encoder_decoder/infrared.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
|
||||
typedef struct InfraredSignal InfraredSignal;
|
||||
|
||||
typedef struct {
|
||||
size_t timings_size;
|
||||
uint32_t* timings;
|
||||
uint32_t frequency;
|
||||
float duty_cycle;
|
||||
} InfraredRawSignal;
|
||||
|
||||
InfraredSignal* infrared_signal_alloc(void);
|
||||
void infrared_signal_free(InfraredSignal* signal);
|
||||
|
||||
bool infrared_signal_is_raw(InfraredSignal* signal);
|
||||
bool infrared_signal_is_valid(InfraredSignal* signal);
|
||||
|
||||
void infrared_signal_set_signal(InfraredSignal* signal, const InfraredSignal* other);
|
||||
|
||||
void infrared_signal_set_raw_signal(
|
||||
InfraredSignal* signal,
|
||||
const uint32_t* timings,
|
||||
size_t timings_size,
|
||||
uint32_t frequency,
|
||||
float duty_cycle);
|
||||
InfraredRawSignal* infrared_signal_get_raw_signal(InfraredSignal* signal);
|
||||
|
||||
void infrared_signal_set_message(InfraredSignal* signal, const InfraredMessage* message);
|
||||
InfraredMessage* infrared_signal_get_message(InfraredSignal* signal);
|
||||
|
||||
bool infrared_signal_save(InfraredSignal* signal, FlipperFormat* ff, const char* name);
|
||||
bool infrared_signal_read(InfraredSignal* signal, FlipperFormat* ff, FuriString* name);
|
||||
bool infrared_signal_search_and_read(
|
||||
InfraredSignal* signal,
|
||||
FlipperFormat* ff,
|
||||
const FuriString* name);
|
||||
|
||||
void infrared_signal_transmit(InfraredSignal* signal);
|
||||
|
Before Width: | Height: | Size: 305 B |
@@ -1,16 +0,0 @@
|
||||
App(
|
||||
appid="mass_storage",
|
||||
name="Mass Storage",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="mass_storage_app",
|
||||
requires=[
|
||||
"gui",
|
||||
"dialogs",
|
||||
],
|
||||
stack_size=2 * 1024,
|
||||
fap_description="Implements a mass storage device over USB for disk images",
|
||||
fap_version="1.3",
|
||||
fap_icon="assets/floppydisk_10px.png",
|
||||
fap_icon_assets="assets",
|
||||
fap_category="USB",
|
||||
)
|
||||
|
Before Width: | Height: | Size: 183 B |
@@ -1,266 +0,0 @@
|
||||
#include "mass_storage_scsi.h"
|
||||
|
||||
#include <core/log.h>
|
||||
|
||||
#define TAG "MassStorageSCSI"
|
||||
|
||||
#define SCSI_TEST_UNIT_READY (0x00)
|
||||
#define SCSI_REQUEST_SENSE (0x03)
|
||||
#define SCSI_INQUIRY (0x12)
|
||||
#define SCSI_READ_FORMAT_CAPACITIES (0x23)
|
||||
#define SCSI_READ_CAPACITY_10 (0x25)
|
||||
#define SCSI_MODE_SENSE_6 (0x1A)
|
||||
#define SCSI_READ_10 (0x28)
|
||||
#define SCSI_PREVENT_MEDIUM_REMOVAL (0x1E)
|
||||
#define SCSI_START_STOP_UNIT (0x1B)
|
||||
#define SCSI_WRITE_10 (0x2A)
|
||||
|
||||
bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len) {
|
||||
if(!len) {
|
||||
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
||||
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
||||
return false;
|
||||
}
|
||||
FURI_LOG_T(TAG, "START %02X", cmd[0]);
|
||||
scsi->cmd = cmd;
|
||||
scsi->cmd_len = len;
|
||||
scsi->rx_done = false;
|
||||
scsi->tx_done = false;
|
||||
switch(cmd[0]) {
|
||||
case SCSI_WRITE_10: {
|
||||
if(len < 10) return false;
|
||||
scsi->write_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
|
||||
scsi->write_10.count = cmd[7] << 8 | cmd[8];
|
||||
FURI_LOG_D(TAG, "SCSI_WRITE_10 %08lX %04X", scsi->write_10.lba, scsi->write_10.count);
|
||||
return true;
|
||||
}; break;
|
||||
case SCSI_READ_10: {
|
||||
if(len < 10) return false;
|
||||
scsi->read_10.lba = cmd[2] << 24 | cmd[3] << 16 | cmd[4] << 8 | cmd[5];
|
||||
scsi->read_10.count = cmd[7] << 8 | cmd[8];
|
||||
FURI_LOG_D(TAG, "SCSI_READ_10 %08lX %04X", scsi->read_10.lba, scsi->read_10.count);
|
||||
return true;
|
||||
}; break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool scsi_cmd_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len) {
|
||||
FURI_LOG_T(TAG, "RX %02X len %lu", scsi->cmd[0], len);
|
||||
if(scsi->rx_done) return false;
|
||||
switch(scsi->cmd[0]) {
|
||||
case SCSI_WRITE_10: {
|
||||
uint32_t block_size = SCSI_BLOCK_SIZE;
|
||||
uint16_t blocks = len / block_size;
|
||||
bool result =
|
||||
scsi->fn.write(scsi->fn.ctx, scsi->write_10.lba, blocks, data, blocks * block_size);
|
||||
scsi->write_10.lba += blocks;
|
||||
scsi->write_10.count -= blocks;
|
||||
if(!scsi->write_10.count) {
|
||||
scsi->rx_done = true;
|
||||
}
|
||||
return result;
|
||||
}; break;
|
||||
default: {
|
||||
FURI_LOG_W(TAG, "unexpected scsi rx data cmd=%02X", scsi->cmd[0]);
|
||||
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
||||
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
||||
return false;
|
||||
}; break;
|
||||
}
|
||||
}
|
||||
|
||||
bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t cap) {
|
||||
FURI_LOG_T(TAG, "TX %02X cap %lu", scsi->cmd[0], cap);
|
||||
if(scsi->tx_done) return false;
|
||||
switch(scsi->cmd[0]) {
|
||||
case SCSI_REQUEST_SENSE: {
|
||||
FURI_LOG_D(TAG, "SCSI_REQUEST_SENSE");
|
||||
if(cap < 18) return false;
|
||||
memset(data, 0, cap);
|
||||
data[0] = 0x70; // fixed format sense data
|
||||
data[1] = 0; // obsolete
|
||||
data[2] = scsi->sk; // sense key
|
||||
data[3] = 0; // information
|
||||
data[4] = 0; // information
|
||||
data[5] = 0; // information
|
||||
data[6] = 0; // information
|
||||
data[7] = 10; // additional sense length (len-8)
|
||||
data[8] = 0; // command specific information
|
||||
data[9] = 0; // command specific information
|
||||
data[10] = 0; // command specific information
|
||||
data[11] = 0; // command specific information
|
||||
data[12] = scsi->asc; // additional sense code
|
||||
data[13] = 0; // additional sense code qualifier
|
||||
data[14] = 0; // field replaceable unit code
|
||||
data[15] = 0; // sense key specific information
|
||||
data[16] = 0; // sense key specific information
|
||||
data[17] = 0; // sense key specific information
|
||||
*len = 18;
|
||||
scsi->sk = 0;
|
||||
scsi->asc = 0;
|
||||
scsi->tx_done = true;
|
||||
return true;
|
||||
}; break;
|
||||
case SCSI_INQUIRY: {
|
||||
FURI_LOG_D(TAG, "SCSI_INQUIRY");
|
||||
if(scsi->cmd_len < 5) return false;
|
||||
|
||||
if(cap < 36) return false;
|
||||
|
||||
bool evpd = scsi->cmd[1] & 1;
|
||||
uint8_t page_code = scsi->cmd[2];
|
||||
if(evpd == 0) {
|
||||
if(page_code != 0) return false;
|
||||
|
||||
data[0] = 0x00; // device type: direct access block device
|
||||
data[1] = 0x80; // removable: true
|
||||
data[2] = 0x04; // version
|
||||
data[3] = 0x02; // response data format
|
||||
data[4] = 31; // additional length (len - 5)
|
||||
data[5] = 0; // flags
|
||||
data[6] = 0; // flags
|
||||
data[7] = 0; // flags
|
||||
memcpy(data + 8, "Flipper ", 8); // vendor id
|
||||
memcpy(data + 16, "Mass Storage ", 16); // product id
|
||||
memcpy(data + 32, "0001", 4); // product revision level
|
||||
*len = 36;
|
||||
scsi->tx_done = true;
|
||||
return true;
|
||||
} else {
|
||||
if(page_code != 0x80) {
|
||||
FURI_LOG_W(TAG, "Unsupported VPD code %02X", page_code);
|
||||
return false;
|
||||
}
|
||||
data[0] = 0x00;
|
||||
data[1] = 0x80;
|
||||
data[2] = 0x00;
|
||||
data[3] = 0x01; // Serial len
|
||||
data[4] = '0';
|
||||
*len = 5;
|
||||
scsi->tx_done = true;
|
||||
return true;
|
||||
}
|
||||
}; break;
|
||||
case SCSI_READ_FORMAT_CAPACITIES: {
|
||||
FURI_LOG_D(TAG, "SCSI_READ_FORMAT_CAPACITIES");
|
||||
if(cap < 12) {
|
||||
return false;
|
||||
}
|
||||
uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx);
|
||||
uint32_t block_size = SCSI_BLOCK_SIZE;
|
||||
// Capacity List Header
|
||||
data[0] = 0;
|
||||
data[1] = 0;
|
||||
data[2] = 0;
|
||||
data[3] = 8;
|
||||
|
||||
// Capacity Descriptor
|
||||
data[4] = (n_blocks - 1) >> 24;
|
||||
data[5] = (n_blocks - 1) >> 16;
|
||||
data[6] = (n_blocks - 1) >> 8;
|
||||
data[7] = (n_blocks - 1) & 0xFF;
|
||||
data[8] = 0x02; // Formatted media
|
||||
data[9] = block_size >> 16;
|
||||
data[10] = block_size >> 8;
|
||||
data[11] = block_size & 0xFF;
|
||||
*len = 12;
|
||||
scsi->tx_done = true;
|
||||
return true;
|
||||
}; break;
|
||||
case SCSI_READ_CAPACITY_10: {
|
||||
FURI_LOG_D(TAG, "SCSI_READ_CAPACITY_10");
|
||||
if(cap < 8) return false;
|
||||
uint32_t n_blocks = scsi->fn.num_blocks(scsi->fn.ctx);
|
||||
uint32_t block_size = SCSI_BLOCK_SIZE;
|
||||
data[0] = (n_blocks - 1) >> 24;
|
||||
data[1] = (n_blocks - 1) >> 16;
|
||||
data[2] = (n_blocks - 1) >> 8;
|
||||
data[3] = (n_blocks - 1) & 0xFF;
|
||||
data[4] = block_size >> 24;
|
||||
data[5] = block_size >> 16;
|
||||
data[6] = block_size >> 8;
|
||||
data[7] = block_size & 0xFF;
|
||||
*len = 8;
|
||||
scsi->tx_done = true;
|
||||
return true;
|
||||
}; break;
|
||||
case SCSI_MODE_SENSE_6: {
|
||||
FURI_LOG_D(TAG, "SCSI_MODE_SENSE_6 %lu", cap);
|
||||
if(cap < 4) return false;
|
||||
data[0] = 3; // mode data length (len - 1)
|
||||
data[1] = 0; // medium type
|
||||
data[2] = 0; // device-specific parameter
|
||||
data[3] = 0; // block descriptor length
|
||||
*len = 4;
|
||||
scsi->tx_done = true;
|
||||
return true;
|
||||
}; break;
|
||||
case SCSI_READ_10: {
|
||||
uint32_t block_size = SCSI_BLOCK_SIZE;
|
||||
bool result =
|
||||
scsi->fn.read(scsi->fn.ctx, scsi->read_10.lba, scsi->read_10.count, data, len, cap);
|
||||
*len -= *len % block_size;
|
||||
uint16_t blocks = *len / block_size;
|
||||
scsi->read_10.lba += blocks;
|
||||
scsi->read_10.count -= blocks;
|
||||
if(!scsi->read_10.count) {
|
||||
scsi->tx_done = true;
|
||||
}
|
||||
return result;
|
||||
}; break;
|
||||
default: {
|
||||
FURI_LOG_W(TAG, "unexpected scsi tx data cmd=%02X", scsi->cmd[0]);
|
||||
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
||||
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
||||
return false;
|
||||
}; break;
|
||||
}
|
||||
}
|
||||
|
||||
bool scsi_cmd_end(SCSISession* scsi) {
|
||||
FURI_LOG_T(TAG, "END %02X", scsi->cmd[0]);
|
||||
uint8_t* cmd = scsi->cmd;
|
||||
uint8_t len = scsi->cmd_len;
|
||||
scsi->cmd = NULL;
|
||||
scsi->cmd_len = 0;
|
||||
switch(cmd[0]) {
|
||||
case SCSI_WRITE_10:
|
||||
return scsi->rx_done;
|
||||
|
||||
case SCSI_REQUEST_SENSE:
|
||||
case SCSI_INQUIRY:
|
||||
case SCSI_READ_FORMAT_CAPACITIES:
|
||||
case SCSI_READ_CAPACITY_10:
|
||||
case SCSI_MODE_SENSE_6:
|
||||
case SCSI_READ_10:
|
||||
return scsi->tx_done;
|
||||
|
||||
case SCSI_TEST_UNIT_READY: {
|
||||
FURI_LOG_D(TAG, "SCSI_TEST_UNIT_READY");
|
||||
return true;
|
||||
}; break;
|
||||
case SCSI_PREVENT_MEDIUM_REMOVAL: {
|
||||
if(len < 6) return false;
|
||||
bool prevent = cmd[5];
|
||||
FURI_LOG_D(TAG, "SCSI_PREVENT_MEDIUM_REMOVAL prevent=%d", prevent);
|
||||
return !prevent;
|
||||
}; break;
|
||||
case SCSI_START_STOP_UNIT: {
|
||||
if(len < 6) return false;
|
||||
bool eject = (cmd[4] & 2) != 0;
|
||||
bool start = (cmd[4] & 1) != 0;
|
||||
FURI_LOG_D(TAG, "SCSI_START_STOP_UNIT eject=%d start=%d", eject, start);
|
||||
if(eject) {
|
||||
scsi->fn.eject(scsi->fn.ctx);
|
||||
}
|
||||
return true;
|
||||
}; break;
|
||||
default: {
|
||||
FURI_LOG_W(TAG, "unexpected scsi cmd=%02X", cmd[0]);
|
||||
scsi->sk = SCSI_SK_ILLEGAL_REQUEST;
|
||||
scsi->asc = SCSI_ASC_INVALID_COMMAND_OPERATION_CODE;
|
||||
return false;
|
||||
}; break;
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
|
||||
#define SCSI_BLOCK_SIZE (0x200UL)
|
||||
|
||||
#define SCSI_SK_ILLEGAL_REQUEST (5)
|
||||
|
||||
#define SCSI_ASC_INVALID_COMMAND_OPERATION_CODE (0x20)
|
||||
#define SCSI_ASC_LBA_OOB (0x21)
|
||||
#define SCSI_ASC_INVALID_FIELD_IN_CDB (0x24)
|
||||
|
||||
typedef struct {
|
||||
void* ctx;
|
||||
bool (*read)(
|
||||
void* ctx,
|
||||
uint32_t lba,
|
||||
uint16_t count,
|
||||
uint8_t* out,
|
||||
uint32_t* out_len,
|
||||
uint32_t out_cap);
|
||||
bool (*write)(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len);
|
||||
uint32_t (*num_blocks)(void* ctx);
|
||||
void (*eject)(void* ctx);
|
||||
} SCSIDeviceFunc;
|
||||
|
||||
typedef struct {
|
||||
SCSIDeviceFunc fn;
|
||||
|
||||
uint8_t* cmd;
|
||||
uint8_t cmd_len;
|
||||
bool rx_done;
|
||||
bool tx_done;
|
||||
|
||||
uint8_t sk; // sense key
|
||||
uint8_t asc; // additional sense code
|
||||
|
||||
// command-specific data
|
||||
// valid from cmd_start to cmd_end
|
||||
union {
|
||||
struct {
|
||||
uint16_t count;
|
||||
uint32_t lba;
|
||||
} read_10; // SCSI_READ_10
|
||||
|
||||
struct {
|
||||
uint16_t count;
|
||||
uint32_t lba;
|
||||
} write_10; // SCSI_WRITE_10
|
||||
};
|
||||
} SCSISession;
|
||||
|
||||
bool scsi_cmd_start(SCSISession* scsi, uint8_t* cmd, uint8_t len);
|
||||
bool scsi_cmd_rx_data(SCSISession* scsi, uint8_t* data, uint32_t len);
|
||||
bool scsi_cmd_tx_data(SCSISession* scsi, uint8_t* data, uint32_t* len, uint32_t cap);
|
||||
bool scsi_cmd_end(SCSISession* scsi);
|
||||
@@ -1,481 +0,0 @@
|
||||
#include "mass_storage_usb.h"
|
||||
#include <furi_hal.h>
|
||||
|
||||
#define TAG "MassStorageUsb"
|
||||
|
||||
#define USB_MSC_RX_EP (0x01)
|
||||
#define USB_MSC_TX_EP (0x82)
|
||||
|
||||
#define USB_MSC_RX_EP_SIZE (64UL)
|
||||
#define USB_MSC_TX_EP_SIZE (64UL)
|
||||
|
||||
#define USB_MSC_BOT_GET_MAX_LUN (0xFE)
|
||||
#define USB_MSC_BOT_RESET (0xFF)
|
||||
|
||||
#define CBW_SIG (0x43425355)
|
||||
#define CBW_FLAGS_DEVICE_TO_HOST (0x80)
|
||||
|
||||
#define CSW_SIG (0x53425355)
|
||||
#define CSW_STATUS_OK (0)
|
||||
#define CSW_STATUS_NOK (1)
|
||||
#define CSW_STATUS_PHASE_ERROR (2)
|
||||
|
||||
// must be SCSI_BLOCK_SIZE aligned
|
||||
// larger than 0x10000 exceeds size_t, storage_file_* ops fail
|
||||
#define USB_MSC_BUF_MAX (0x10000UL - SCSI_BLOCK_SIZE)
|
||||
|
||||
static usbd_respond usb_ep_config(usbd_device* dev, uint8_t cfg);
|
||||
static usbd_respond usb_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback);
|
||||
|
||||
typedef enum {
|
||||
EventExit = 1 << 0,
|
||||
EventReset = 1 << 1,
|
||||
EventRxTx = 1 << 2,
|
||||
|
||||
EventAll = EventExit | EventReset | EventRxTx,
|
||||
} MassStorageEvent;
|
||||
|
||||
typedef struct {
|
||||
uint32_t sig;
|
||||
uint32_t tag;
|
||||
uint32_t len;
|
||||
uint8_t flags;
|
||||
uint8_t lun;
|
||||
uint8_t cmd_len;
|
||||
uint8_t cmd[16];
|
||||
} __attribute__((packed)) CBW;
|
||||
|
||||
typedef struct {
|
||||
uint32_t sig;
|
||||
uint32_t tag;
|
||||
uint32_t residue;
|
||||
uint8_t status;
|
||||
} __attribute__((packed)) CSW;
|
||||
|
||||
struct MassStorageUsb {
|
||||
FuriHalUsbInterface usb;
|
||||
FuriHalUsbInterface* usb_prev;
|
||||
|
||||
FuriThread* thread;
|
||||
usbd_device* dev;
|
||||
SCSIDeviceFunc fn;
|
||||
};
|
||||
|
||||
static int32_t mass_thread_worker(void* context) {
|
||||
MassStorageUsb* mass = context;
|
||||
usbd_device* dev = mass->dev;
|
||||
SCSISession scsi = {
|
||||
.fn = mass->fn,
|
||||
};
|
||||
CBW cbw = {0};
|
||||
CSW csw = {0};
|
||||
uint8_t* buf = NULL;
|
||||
uint32_t buf_len = 0, buf_cap = 0, buf_sent = 0;
|
||||
enum {
|
||||
StateReadCBW,
|
||||
StateReadData,
|
||||
StateWriteData,
|
||||
StateBuildCSW,
|
||||
StateWriteCSW,
|
||||
} state = StateReadCBW;
|
||||
while(true) {
|
||||
uint32_t flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, FuriWaitForever);
|
||||
if(flags & EventExit) {
|
||||
FURI_LOG_D(TAG, "exit");
|
||||
break;
|
||||
}
|
||||
if(flags & EventReset) {
|
||||
FURI_LOG_D(TAG, "reset");
|
||||
scsi.sk = 0;
|
||||
scsi.asc = 0;
|
||||
memset(&cbw, 0, sizeof(cbw));
|
||||
memset(&csw, 0, sizeof(csw));
|
||||
if(buf) {
|
||||
free(buf);
|
||||
buf = NULL;
|
||||
}
|
||||
buf_len = buf_cap = buf_sent = 0;
|
||||
state = StateReadCBW;
|
||||
}
|
||||
if(flags & EventRxTx) do {
|
||||
switch(state) {
|
||||
case StateReadCBW: {
|
||||
FURI_LOG_T(TAG, "StateReadCBW");
|
||||
int32_t len = usbd_ep_read(dev, USB_MSC_RX_EP, &cbw, sizeof(cbw));
|
||||
if(len <= 0) {
|
||||
FURI_LOG_T(TAG, "cbw not ready");
|
||||
break;
|
||||
}
|
||||
if(len != sizeof(cbw) || cbw.sig != CBW_SIG) {
|
||||
FURI_LOG_W(TAG, "bad cbw sig=%08lx", cbw.sig);
|
||||
usbd_ep_stall(dev, USB_MSC_TX_EP);
|
||||
usbd_ep_stall(dev, USB_MSC_RX_EP);
|
||||
continue;
|
||||
}
|
||||
if(!scsi_cmd_start(&scsi, cbw.cmd, cbw.cmd_len)) {
|
||||
FURI_LOG_W(TAG, "bad cmd");
|
||||
usbd_ep_stall(dev, USB_MSC_RX_EP);
|
||||
csw.sig = CSW_SIG;
|
||||
csw.tag = cbw.tag;
|
||||
csw.status = CSW_STATUS_NOK;
|
||||
state = StateWriteCSW;
|
||||
continue;
|
||||
}
|
||||
if(cbw.flags & CBW_FLAGS_DEVICE_TO_HOST) {
|
||||
buf_len = 0;
|
||||
buf_sent = 0;
|
||||
state = StateWriteData;
|
||||
} else {
|
||||
buf_len = 0;
|
||||
state = StateReadData;
|
||||
}
|
||||
continue;
|
||||
}; break;
|
||||
case StateReadData: {
|
||||
FURI_LOG_T(TAG, "StateReadData %lu/%lu", buf_len, cbw.len);
|
||||
if(!cbw.len) {
|
||||
state = StateBuildCSW;
|
||||
continue;
|
||||
}
|
||||
uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
|
||||
if(buf_clamp > buf_cap) {
|
||||
FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
|
||||
if(buf) {
|
||||
free(buf);
|
||||
}
|
||||
buf_cap = buf_clamp;
|
||||
buf = malloc(buf_cap);
|
||||
}
|
||||
if(buf_len < buf_clamp) {
|
||||
int32_t len =
|
||||
usbd_ep_read(dev, USB_MSC_RX_EP, buf + buf_len, buf_clamp - buf_len);
|
||||
if(len < 0) {
|
||||
FURI_LOG_T(TAG, "rx not ready %ld", len);
|
||||
break;
|
||||
}
|
||||
FURI_LOG_T(TAG, "clamp %lu len %ld", buf_clamp, len);
|
||||
buf_len += len;
|
||||
}
|
||||
if(buf_len == buf_clamp) {
|
||||
if(!scsi_cmd_rx_data(&scsi, buf, buf_len)) {
|
||||
FURI_LOG_W(TAG, "short rx");
|
||||
usbd_ep_stall(dev, USB_MSC_RX_EP);
|
||||
csw.sig = CSW_SIG;
|
||||
csw.tag = cbw.tag;
|
||||
csw.status = CSW_STATUS_NOK;
|
||||
csw.residue = cbw.len;
|
||||
state = StateWriteCSW;
|
||||
continue;
|
||||
}
|
||||
cbw.len -= buf_len;
|
||||
buf_len = 0;
|
||||
}
|
||||
continue;
|
||||
}; break;
|
||||
case StateWriteData: {
|
||||
FURI_LOG_T(TAG, "StateWriteData %lu", cbw.len);
|
||||
if(!cbw.len) {
|
||||
state = StateBuildCSW;
|
||||
continue;
|
||||
}
|
||||
uint32_t buf_clamp = MIN(cbw.len, USB_MSC_BUF_MAX);
|
||||
if(buf_clamp > buf_cap) {
|
||||
FURI_LOG_T(TAG, "growing buf %lu -> %lu", buf_cap, buf_clamp);
|
||||
if(buf) {
|
||||
free(buf);
|
||||
}
|
||||
buf_cap = buf_clamp;
|
||||
buf = malloc(buf_cap);
|
||||
}
|
||||
if(!buf_len && !scsi_cmd_tx_data(&scsi, buf, &buf_len, buf_clamp)) {
|
||||
FURI_LOG_W(TAG, "short tx");
|
||||
// usbd_ep_stall(dev, USB_MSC_TX_EP);
|
||||
state = StateBuildCSW;
|
||||
continue;
|
||||
}
|
||||
int32_t len = usbd_ep_write(
|
||||
dev,
|
||||
USB_MSC_TX_EP,
|
||||
buf + buf_sent,
|
||||
MIN(USB_MSC_TX_EP_SIZE, buf_len - buf_sent));
|
||||
if(len < 0) {
|
||||
FURI_LOG_T(TAG, "tx not ready %ld", len);
|
||||
break;
|
||||
}
|
||||
buf_sent += len;
|
||||
if(buf_sent == buf_len) {
|
||||
cbw.len -= buf_len;
|
||||
buf_len = 0;
|
||||
buf_sent = 0;
|
||||
}
|
||||
continue;
|
||||
}; break;
|
||||
case StateBuildCSW: {
|
||||
FURI_LOG_T(TAG, "StateBuildCSW");
|
||||
csw.sig = CSW_SIG;
|
||||
csw.tag = cbw.tag;
|
||||
if(scsi_cmd_end(&scsi)) {
|
||||
csw.status = CSW_STATUS_OK;
|
||||
} else {
|
||||
csw.status = CSW_STATUS_NOK;
|
||||
}
|
||||
csw.residue = cbw.len;
|
||||
state = StateWriteCSW;
|
||||
continue;
|
||||
}; break;
|
||||
case StateWriteCSW: {
|
||||
FURI_LOG_T(TAG, "StateWriteCSW");
|
||||
if(csw.status) {
|
||||
FURI_LOG_W(
|
||||
TAG,
|
||||
"csw sig=%08lx tag=%08lx residue=%08lx status=%02x",
|
||||
csw.sig,
|
||||
csw.tag,
|
||||
csw.residue,
|
||||
csw.status);
|
||||
}
|
||||
int32_t len = usbd_ep_write(dev, USB_MSC_TX_EP, &csw, sizeof(csw));
|
||||
if(len < 0) {
|
||||
FURI_LOG_T(TAG, "csw not ready");
|
||||
break;
|
||||
}
|
||||
if(len != sizeof(csw)) {
|
||||
FURI_LOG_W(TAG, "bad csw write %ld", len);
|
||||
usbd_ep_stall(dev, USB_MSC_TX_EP);
|
||||
break;
|
||||
}
|
||||
memset(&cbw, 0, sizeof(cbw));
|
||||
memset(&csw, 0, sizeof(csw));
|
||||
state = StateReadCBW;
|
||||
continue;
|
||||
}; break;
|
||||
}
|
||||
break;
|
||||
} while(true);
|
||||
}
|
||||
if(buf) {
|
||||
free(buf);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// needed in usb_deinit, usb_suspend, usb_rxtx_ep_callback, usb_control,
|
||||
// where if_ctx isn't passed
|
||||
static MassStorageUsb* mass_cur = NULL;
|
||||
|
||||
static void usb_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) {
|
||||
UNUSED(intf);
|
||||
MassStorageUsb* mass = ctx;
|
||||
mass_cur = mass;
|
||||
mass->dev = dev;
|
||||
|
||||
usbd_reg_config(dev, usb_ep_config);
|
||||
usbd_reg_control(dev, usb_control);
|
||||
usbd_connect(dev, true);
|
||||
|
||||
mass->thread = furi_thread_alloc();
|
||||
furi_thread_set_name(mass->thread, "MassStorageUsb");
|
||||
furi_thread_set_stack_size(mass->thread, 1024);
|
||||
furi_thread_set_context(mass->thread, ctx);
|
||||
furi_thread_set_callback(mass->thread, mass_thread_worker);
|
||||
furi_thread_start(mass->thread);
|
||||
}
|
||||
|
||||
static void usb_deinit(usbd_device* dev) {
|
||||
usbd_reg_config(dev, NULL);
|
||||
usbd_reg_control(dev, NULL);
|
||||
|
||||
MassStorageUsb* mass = mass_cur;
|
||||
if(!mass || mass->dev != dev) {
|
||||
FURI_LOG_E(TAG, "deinit mass_cur leak");
|
||||
return;
|
||||
}
|
||||
mass_cur = NULL;
|
||||
|
||||
furi_assert(mass->thread);
|
||||
furi_thread_flags_set(furi_thread_get_id(mass->thread), EventExit);
|
||||
furi_thread_join(mass->thread);
|
||||
furi_thread_free(mass->thread);
|
||||
mass->thread = NULL;
|
||||
|
||||
free(mass->usb.str_prod_descr);
|
||||
mass->usb.str_prod_descr = NULL;
|
||||
free(mass->usb.str_serial_descr);
|
||||
mass->usb.str_serial_descr = NULL;
|
||||
free(mass);
|
||||
}
|
||||
|
||||
static void usb_wakeup(usbd_device* dev) {
|
||||
UNUSED(dev);
|
||||
}
|
||||
|
||||
static void usb_suspend(usbd_device* dev) {
|
||||
MassStorageUsb* mass = mass_cur;
|
||||
if(!mass || mass->dev != dev) return;
|
||||
furi_thread_flags_set(furi_thread_get_id(mass->thread), EventReset);
|
||||
}
|
||||
|
||||
static void usb_rxtx_ep_callback(usbd_device* dev, uint8_t event, uint8_t ep) {
|
||||
UNUSED(ep);
|
||||
UNUSED(event);
|
||||
MassStorageUsb* mass = mass_cur;
|
||||
if(!mass || mass->dev != dev) return;
|
||||
furi_thread_flags_set(furi_thread_get_id(mass->thread), EventRxTx);
|
||||
}
|
||||
|
||||
static usbd_respond usb_ep_config(usbd_device* dev, uint8_t cfg) {
|
||||
switch(cfg) {
|
||||
case 0: // deconfig
|
||||
usbd_ep_deconfig(dev, USB_MSC_RX_EP);
|
||||
usbd_ep_deconfig(dev, USB_MSC_TX_EP);
|
||||
usbd_reg_endpoint(dev, USB_MSC_RX_EP, NULL);
|
||||
usbd_reg_endpoint(dev, USB_MSC_TX_EP, NULL);
|
||||
return usbd_ack;
|
||||
case 1: // config
|
||||
usbd_ep_config(
|
||||
dev, USB_MSC_RX_EP, USB_EPTYPE_BULK /* | USB_EPTYPE_DBLBUF*/, USB_MSC_RX_EP_SIZE);
|
||||
usbd_ep_config(
|
||||
dev, USB_MSC_TX_EP, USB_EPTYPE_BULK /* | USB_EPTYPE_DBLBUF*/, USB_MSC_TX_EP_SIZE);
|
||||
usbd_reg_endpoint(dev, USB_MSC_RX_EP, usb_rxtx_ep_callback);
|
||||
usbd_reg_endpoint(dev, USB_MSC_TX_EP, usb_rxtx_ep_callback);
|
||||
return usbd_ack;
|
||||
}
|
||||
return usbd_fail;
|
||||
}
|
||||
|
||||
static usbd_respond usb_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback) {
|
||||
UNUSED(callback);
|
||||
if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) !=
|
||||
(USB_REQ_INTERFACE | USB_REQ_CLASS)) {
|
||||
return usbd_fail;
|
||||
}
|
||||
switch(req->bRequest) {
|
||||
case USB_MSC_BOT_GET_MAX_LUN: {
|
||||
static uint8_t max_lun = 0;
|
||||
dev->status.data_ptr = &max_lun;
|
||||
dev->status.data_count = 1;
|
||||
return usbd_ack;
|
||||
}; break;
|
||||
case USB_MSC_BOT_RESET: {
|
||||
MassStorageUsb* mass = mass_cur;
|
||||
if(!mass || mass->dev != dev) return usbd_fail;
|
||||
furi_thread_flags_set(furi_thread_get_id(mass->thread), EventReset);
|
||||
return usbd_ack;
|
||||
}; break;
|
||||
}
|
||||
return usbd_fail;
|
||||
}
|
||||
|
||||
static const struct usb_string_descriptor dev_manuf_desc = USB_STRING_DESC("Flipper Devices Inc.");
|
||||
|
||||
struct MassStorageDescriptor {
|
||||
struct usb_config_descriptor config;
|
||||
struct usb_interface_descriptor intf;
|
||||
struct usb_endpoint_descriptor ep_rx;
|
||||
struct usb_endpoint_descriptor ep_tx;
|
||||
} __attribute__((packed));
|
||||
|
||||
static const struct usb_device_descriptor usb_mass_dev_descr = {
|
||||
.bLength = sizeof(struct usb_device_descriptor),
|
||||
.bDescriptorType = USB_DTYPE_DEVICE,
|
||||
.bcdUSB = VERSION_BCD(2, 0, 0),
|
||||
.bDeviceClass = USB_CLASS_PER_INTERFACE,
|
||||
.bDeviceSubClass = USB_SUBCLASS_NONE,
|
||||
.bDeviceProtocol = USB_PROTO_NONE,
|
||||
.bMaxPacketSize0 = 8, // USB_EP0_SIZE
|
||||
.idVendor = 0x0483,
|
||||
.idProduct = 0x5720,
|
||||
.bcdDevice = VERSION_BCD(1, 0, 0),
|
||||
.iManufacturer = 1, // UsbDevManuf
|
||||
.iProduct = 2, // UsbDevProduct
|
||||
.iSerialNumber = 3, // UsbDevSerial
|
||||
.bNumConfigurations = 1,
|
||||
};
|
||||
|
||||
static const struct MassStorageDescriptor usb_mass_cfg_descr = {
|
||||
.config =
|
||||
{
|
||||
.bLength = sizeof(struct usb_config_descriptor),
|
||||
.bDescriptorType = USB_DTYPE_CONFIGURATION,
|
||||
.wTotalLength = sizeof(struct MassStorageDescriptor),
|
||||
.bNumInterfaces = 1,
|
||||
.bConfigurationValue = 1,
|
||||
.iConfiguration = NO_DESCRIPTOR,
|
||||
.bmAttributes = USB_CFG_ATTR_RESERVED | USB_CFG_ATTR_SELFPOWERED,
|
||||
.bMaxPower = USB_CFG_POWER_MA(100),
|
||||
},
|
||||
.intf =
|
||||
{
|
||||
.bLength = sizeof(struct usb_interface_descriptor),
|
||||
.bDescriptorType = USB_DTYPE_INTERFACE,
|
||||
.bInterfaceNumber = 0,
|
||||
.bAlternateSetting = 0,
|
||||
.bNumEndpoints = 2,
|
||||
.bInterfaceClass = USB_CLASS_MASS_STORAGE,
|
||||
.bInterfaceSubClass = 0x06, // scsi transparent
|
||||
.bInterfaceProtocol = 0x50, // bulk only
|
||||
.iInterface = NO_DESCRIPTOR,
|
||||
},
|
||||
.ep_rx =
|
||||
{
|
||||
.bLength = sizeof(struct usb_endpoint_descriptor),
|
||||
.bDescriptorType = USB_DTYPE_ENDPOINT,
|
||||
.bEndpointAddress = USB_MSC_RX_EP,
|
||||
.bmAttributes = USB_EPTYPE_BULK,
|
||||
.wMaxPacketSize = USB_MSC_RX_EP_SIZE,
|
||||
.bInterval = 0,
|
||||
},
|
||||
.ep_tx =
|
||||
{
|
||||
.bLength = sizeof(struct usb_endpoint_descriptor),
|
||||
.bDescriptorType = USB_DTYPE_ENDPOINT,
|
||||
.bEndpointAddress = USB_MSC_TX_EP,
|
||||
.bmAttributes = USB_EPTYPE_BULK,
|
||||
.wMaxPacketSize = USB_MSC_TX_EP_SIZE,
|
||||
.bInterval = 0,
|
||||
},
|
||||
};
|
||||
|
||||
MassStorageUsb* mass_storage_usb_start(const char* filename, SCSIDeviceFunc fn) {
|
||||
MassStorageUsb* mass = malloc(sizeof(MassStorageUsb));
|
||||
mass->usb_prev = furi_hal_usb_get_config();
|
||||
mass->usb.init = usb_init;
|
||||
mass->usb.deinit = usb_deinit;
|
||||
mass->usb.wakeup = usb_wakeup;
|
||||
mass->usb.suspend = usb_suspend;
|
||||
mass->usb.dev_descr = (struct usb_device_descriptor*)&usb_mass_dev_descr;
|
||||
mass->usb.str_manuf_descr = (void*)&dev_manuf_desc;
|
||||
mass->usb.str_prod_descr = NULL;
|
||||
mass->usb.str_serial_descr = NULL;
|
||||
mass->usb.cfg_descr = (void*)&usb_mass_cfg_descr;
|
||||
|
||||
const char* name = furi_hal_version_get_device_name_ptr();
|
||||
if(!name) name = "Flipper Zero";
|
||||
size_t len = strlen(name);
|
||||
struct usb_string_descriptor* str_prod_descr = malloc(len * 2 + 2);
|
||||
str_prod_descr->bLength = len * 2 + 2;
|
||||
str_prod_descr->bDescriptorType = USB_DTYPE_STRING;
|
||||
for(uint8_t i = 0; i < len; i++) str_prod_descr->wString[i] = name[i];
|
||||
mass->usb.str_prod_descr = str_prod_descr;
|
||||
|
||||
len = strlen(filename);
|
||||
struct usb_string_descriptor* str_serial_descr = malloc(len * 2 + 2);
|
||||
str_serial_descr->bLength = len * 2 + 2;
|
||||
str_serial_descr->bDescriptorType = USB_DTYPE_STRING;
|
||||
for(uint8_t i = 0; i < len; i++) str_serial_descr->wString[i] = filename[i];
|
||||
mass->usb.str_serial_descr = str_serial_descr;
|
||||
|
||||
mass->fn = fn;
|
||||
if(!furi_hal_usb_set_config(&mass->usb, mass)) {
|
||||
FURI_LOG_E(TAG, "USB locked, cannot start Mass Storage");
|
||||
free(mass->usb.str_prod_descr);
|
||||
free(mass->usb.str_serial_descr);
|
||||
free(mass);
|
||||
return NULL;
|
||||
}
|
||||
return mass;
|
||||
}
|
||||
|
||||
void mass_storage_usb_stop(MassStorageUsb* mass) {
|
||||
furi_hal_usb_set_config(mass->usb_prev, NULL);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include "mass_storage_scsi.h"
|
||||
|
||||
typedef struct MassStorageUsb MassStorageUsb;
|
||||
|
||||
MassStorageUsb* mass_storage_usb_start(const char* filename, SCSIDeviceFunc fn);
|
||||
void mass_storage_usb_stop(MassStorageUsb* mass);
|
||||
@@ -1,151 +0,0 @@
|
||||
#include "mass_storage_app_i.h"
|
||||
#include <furi.h>
|
||||
#include <storage/storage.h>
|
||||
#include <lib/toolbox/path.h>
|
||||
|
||||
static bool mass_storage_app_custom_event_callback(void* context, uint32_t event) {
|
||||
furi_assert(context);
|
||||
MassStorageApp* app = context;
|
||||
return scene_manager_handle_custom_event(app->scene_manager, event);
|
||||
}
|
||||
|
||||
static bool mass_storage_app_back_event_callback(void* context) {
|
||||
furi_assert(context);
|
||||
MassStorageApp* app = context;
|
||||
return scene_manager_handle_back_event(app->scene_manager);
|
||||
}
|
||||
|
||||
static void mass_storage_app_tick_event_callback(void* context) {
|
||||
furi_assert(context);
|
||||
MassStorageApp* app = context;
|
||||
scene_manager_handle_tick_event(app->scene_manager);
|
||||
}
|
||||
|
||||
void mass_storage_app_show_loading_popup(MassStorageApp* app, bool show) {
|
||||
if(show) {
|
||||
// Raise timer priority so that animations can play
|
||||
furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewLoading);
|
||||
} else {
|
||||
// Restore default timer priority
|
||||
furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
|
||||
}
|
||||
}
|
||||
|
||||
MassStorageApp* mass_storage_app_alloc(char* arg) {
|
||||
MassStorageApp* app = malloc(sizeof(MassStorageApp));
|
||||
app->file_path = furi_string_alloc();
|
||||
|
||||
if(arg != NULL) {
|
||||
furi_string_set_str(app->file_path, arg);
|
||||
} else {
|
||||
furi_string_set_str(app->file_path, MASS_STORAGE_APP_PATH_FOLDER);
|
||||
}
|
||||
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
app->fs_api = furi_record_open(RECORD_STORAGE);
|
||||
app->dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
|
||||
app->create_image_size = (uint8_t)-1;
|
||||
SDInfo sd_info;
|
||||
if(storage_sd_info(app->fs_api, &sd_info) == FSE_OK) {
|
||||
switch(sd_info.fs_type) {
|
||||
case FST_FAT12:
|
||||
app->create_image_max = 16LL * 1024 * 1024;
|
||||
break;
|
||||
case FST_FAT16:
|
||||
app->create_image_max = 2LL * 1024 * 1024 * 1024;
|
||||
break;
|
||||
case FST_FAT32:
|
||||
app->create_image_max = 4LL * 1024 * 1024 * 1024;
|
||||
break;
|
||||
default:
|
||||
app->create_image_max = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
view_dispatcher_enable_queue(app->view_dispatcher);
|
||||
|
||||
app->scene_manager = scene_manager_alloc(&mass_storage_scene_handlers, app);
|
||||
|
||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||
view_dispatcher_set_tick_event_callback(
|
||||
app->view_dispatcher, mass_storage_app_tick_event_callback, 500);
|
||||
view_dispatcher_set_custom_event_callback(
|
||||
app->view_dispatcher, mass_storage_app_custom_event_callback);
|
||||
view_dispatcher_set_navigation_event_callback(
|
||||
app->view_dispatcher, mass_storage_app_back_event_callback);
|
||||
|
||||
app->mass_storage_view = mass_storage_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher,
|
||||
MassStorageAppViewWork,
|
||||
mass_storage_get_view(app->mass_storage_view));
|
||||
|
||||
app->text_input = text_input_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, MassStorageAppViewTextInput, text_input_get_view(app->text_input));
|
||||
|
||||
app->loading = loading_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, MassStorageAppViewLoading, loading_get_view(app->loading));
|
||||
|
||||
app->variable_item_list = variable_item_list_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher,
|
||||
MassStorageAppViewStart,
|
||||
variable_item_list_get_view(app->variable_item_list));
|
||||
|
||||
app->popup = popup_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, MassStorageAppViewPopup, popup_get_view(app->popup));
|
||||
|
||||
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||
|
||||
if(storage_file_exists(app->fs_api, furi_string_get_cstr(app->file_path))) {
|
||||
scene_manager_next_scene(app->scene_manager, MassStorageSceneWork);
|
||||
} else {
|
||||
scene_manager_next_scene(app->scene_manager, MassStorageSceneStart);
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
void mass_storage_app_free(MassStorageApp* app) {
|
||||
furi_assert(app);
|
||||
|
||||
// Views
|
||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewWork);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewTextInput);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewStart);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewLoading);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, MassStorageAppViewPopup);
|
||||
|
||||
mass_storage_free(app->mass_storage_view);
|
||||
text_input_free(app->text_input);
|
||||
variable_item_list_free(app->variable_item_list);
|
||||
loading_free(app->loading);
|
||||
popup_free(app->popup);
|
||||
|
||||
// View dispatcher
|
||||
view_dispatcher_free(app->view_dispatcher);
|
||||
scene_manager_free(app->scene_manager);
|
||||
|
||||
furi_string_free(app->file_path);
|
||||
|
||||
// Close records
|
||||
furi_record_close(RECORD_GUI);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
|
||||
free(app);
|
||||
}
|
||||
|
||||
int32_t mass_storage_app(void* p) {
|
||||
MassStorageApp* mass_storage_app = mass_storage_app_alloc((char*)p);
|
||||
view_dispatcher_run(mass_storage_app->view_dispatcher);
|
||||
mass_storage_app_free(mass_storage_app);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct MassStorageApp MassStorageApp;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "mass_storage_app.h"
|
||||
#include "scenes/mass_storage_scene.h"
|
||||
#include "helpers/mass_storage_usb.h"
|
||||
|
||||
#include <furi_hal.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/scene_manager.h>
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <gui/modules/variable_item_list.h>
|
||||
#include <gui/modules/text_input.h>
|
||||
#include <gui/modules/loading.h>
|
||||
#include <gui/modules/popup.h>
|
||||
#include <storage/storage.h>
|
||||
#include "views/mass_storage_view.h"
|
||||
#include <mass_storage_icons.h>
|
||||
|
||||
#include <assets_icons.h>
|
||||
|
||||
#define MASS_STORAGE_APP_PATH_FOLDER STORAGE_APP_DATA_PATH_PREFIX
|
||||
#define MASS_STORAGE_APP_EXTENSION ".img"
|
||||
#define MASS_STORAGE_FILE_NAME_LEN 40
|
||||
|
||||
struct MassStorageApp {
|
||||
Gui* gui;
|
||||
Storage* fs_api;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
SceneManager* scene_manager;
|
||||
Popup* popup;
|
||||
DialogsApp* dialogs;
|
||||
TextInput* text_input;
|
||||
VariableItemList* variable_item_list;
|
||||
Loading* loading;
|
||||
|
||||
FuriString* file_path;
|
||||
File* file;
|
||||
MassStorage* mass_storage_view;
|
||||
|
||||
FuriMutex* usb_mutex;
|
||||
MassStorageUsb* usb;
|
||||
|
||||
uint64_t create_image_max;
|
||||
uint8_t create_image_size;
|
||||
char create_image_name[MASS_STORAGE_FILE_NAME_LEN];
|
||||
|
||||
uint32_t bytes_read, bytes_written;
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
MassStorageAppViewStart,
|
||||
MassStorageAppViewTextInput,
|
||||
MassStorageAppViewWork,
|
||||
MassStorageAppViewLoading,
|
||||
MassStorageAppViewPopup,
|
||||
} MassStorageAppView;
|
||||
|
||||
enum MassStorageCustomEvent {
|
||||
// Reserve first 100 events for button types and indexes, starting from 0
|
||||
MassStorageCustomEventReserved = 100,
|
||||
|
||||
MassStorageCustomEventEject,
|
||||
};
|
||||
|
||||
void mass_storage_app_show_loading_popup(MassStorageApp* app, bool show);
|
||||
@@ -1,30 +0,0 @@
|
||||
#include "mass_storage_scene.h"
|
||||
|
||||
// Generate scene on_enter handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
|
||||
void (*const mass_storage_scene_on_enter_handlers[])(void*) = {
|
||||
#include "mass_storage_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_event handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
|
||||
bool (*const mass_storage_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
|
||||
#include "mass_storage_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_exit handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
|
||||
void (*const mass_storage_scene_on_exit_handlers[])(void* context) = {
|
||||
#include "mass_storage_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Initialize scene handlers configuration structure
|
||||
const SceneManagerHandlers mass_storage_scene_handlers = {
|
||||
.on_enter_handlers = mass_storage_scene_on_enter_handlers,
|
||||
.on_event_handlers = mass_storage_scene_on_event_handlers,
|
||||
.on_exit_handlers = mass_storage_scene_on_exit_handlers,
|
||||
.scene_num = MassStorageSceneNum,
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/scene_manager.h>
|
||||
|
||||
// Generate scene id and total number
|
||||
#define ADD_SCENE(prefix, name, id) MassStorageScene##id,
|
||||
typedef enum {
|
||||
#include "mass_storage_scene_config.h"
|
||||
MassStorageSceneNum,
|
||||
} MassStorageScene;
|
||||
#undef ADD_SCENE
|
||||
|
||||
extern const SceneManagerHandlers mass_storage_scene_handlers;
|
||||
|
||||
// Generate scene on_enter handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
|
||||
#include "mass_storage_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_event handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) \
|
||||
bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
|
||||
#include "mass_storage_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_exit handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
|
||||
#include "mass_storage_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
@@ -1,5 +0,0 @@
|
||||
ADD_SCENE(mass_storage, start, Start)
|
||||
ADD_SCENE(mass_storage, file_select, FileSelect)
|
||||
ADD_SCENE(mass_storage, work, Work)
|
||||
ADD_SCENE(mass_storage, create_image, CreateImage)
|
||||
ADD_SCENE(mass_storage, create_image_name, CreateImageName)
|
||||
@@ -1,193 +0,0 @@
|
||||
#include "../mass_storage_app_i.h"
|
||||
#include <lib/toolbox/value_index.h>
|
||||
|
||||
enum VarItemListIndex {
|
||||
VarItemListIndexImageSize,
|
||||
VarItemListIndexImageName,
|
||||
VarItemListIndexCreateImage,
|
||||
};
|
||||
|
||||
void mass_storage_scene_create_image_variable_item_list_callback(void* context, uint32_t index) {
|
||||
MassStorageApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
static const struct {
|
||||
char* name;
|
||||
uint64_t value;
|
||||
} image_sizes[] = {
|
||||
{"1MB", 1LL * 1024 * 1024},
|
||||
{"2MB", 2LL * 1024 * 1024},
|
||||
{"4MB", 4LL * 1024 * 1024},
|
||||
{"8MB", 8LL * 1024 * 1024},
|
||||
{"16MB", 16LL * 1024 * 1024},
|
||||
{"32MB", 32LL * 1024 * 1024},
|
||||
{"64MB", 64LL * 1024 * 1024},
|
||||
{"128MB", 128LL * 1024 * 1024},
|
||||
{"256MB", 256LL * 1024 * 1024},
|
||||
{"512MB", 512LL * 1024 * 1024},
|
||||
{"1GB", 1LL * 1024 * 1024 * 1024},
|
||||
{"2GB", 2LL * 1024 * 1024 * 1024},
|
||||
{"4GB", 4LL * 1024 * 1024 * 1024},
|
||||
{"8GB", 8LL * 1024 * 1024 * 1024},
|
||||
{"16GB", 16LL * 1024 * 1024 * 1024},
|
||||
{"32GB", 32LL * 1024 * 1024 * 1024},
|
||||
{"64GB", 64LL * 1024 * 1024 * 1024},
|
||||
{"128GB", 128LL * 1024 * 1024 * 1024},
|
||||
{"256GB", 256LL * 1024 * 1024 * 1024},
|
||||
{"512GB", 512LL * 1024 * 1024 * 1024},
|
||||
};
|
||||
static void mass_storage_scene_create_image_image_size_changed(VariableItem* item) {
|
||||
MassStorageApp* app = variable_item_get_context(item);
|
||||
app->create_image_size = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, image_sizes[app->create_image_size].name);
|
||||
}
|
||||
|
||||
void mass_storage_scene_create_image_on_enter(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
VariableItemList* variable_item_list = app->variable_item_list;
|
||||
VariableItem* item;
|
||||
|
||||
uint8_t size_count = COUNT_OF(image_sizes);
|
||||
if(app->create_image_max) {
|
||||
for(size_t i = 1; i < size_count; i++) {
|
||||
if(image_sizes[i].value > app->create_image_max) {
|
||||
size_count = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(app->create_image_size == (uint8_t)-1) {
|
||||
app->create_image_size = CLAMP(7, size_count - 2, 0); // 7 = 128MB
|
||||
}
|
||||
item = variable_item_list_add(
|
||||
variable_item_list,
|
||||
"Image Size",
|
||||
size_count,
|
||||
mass_storage_scene_create_image_image_size_changed,
|
||||
app);
|
||||
variable_item_set_current_value_index(item, app->create_image_size);
|
||||
variable_item_set_current_value_text(item, image_sizes[app->create_image_size].name);
|
||||
|
||||
item = variable_item_list_add(variable_item_list, "Image Name", 0, NULL, app);
|
||||
variable_item_set_current_value_text(item, app->create_image_name);
|
||||
|
||||
variable_item_list_add(variable_item_list, "Create Image", 0, NULL, app);
|
||||
|
||||
variable_item_list_set_enter_callback(
|
||||
variable_item_list, mass_storage_scene_create_image_variable_item_list_callback, app);
|
||||
|
||||
variable_item_list_set_header(variable_item_list, "Create Disk Image");
|
||||
|
||||
variable_item_list_set_selected_item(
|
||||
variable_item_list,
|
||||
scene_manager_get_scene_state(app->scene_manager, MassStorageSceneCreateImage));
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewStart);
|
||||
}
|
||||
|
||||
static void popup_callback_ok(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, MassStorageSceneStart, MassStorageSceneFileSelect);
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
scene_manager_next_scene(app->scene_manager, MassStorageSceneFileSelect);
|
||||
}
|
||||
|
||||
static void popup_callback_error(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewStart);
|
||||
}
|
||||
|
||||
bool mass_storage_scene_create_image_on_event(void* context, SceneManagerEvent event) {
|
||||
MassStorageApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, MassStorageSceneCreateImage, event.event);
|
||||
consumed = true;
|
||||
switch(event.event) {
|
||||
case VarItemListIndexImageName:
|
||||
scene_manager_next_scene(app->scene_manager, MassStorageSceneCreateImageName);
|
||||
break;
|
||||
case VarItemListIndexCreateImage: {
|
||||
mass_storage_app_show_loading_popup(app, true);
|
||||
const char* name = strnlen(app->create_image_name, sizeof(app->create_image_name)) ?
|
||||
app->create_image_name :
|
||||
image_sizes[app->create_image_size].name;
|
||||
furi_string_printf(
|
||||
app->file_path,
|
||||
"%s/%s%s",
|
||||
MASS_STORAGE_APP_PATH_FOLDER,
|
||||
name,
|
||||
MASS_STORAGE_APP_EXTENSION);
|
||||
|
||||
app->file = storage_file_alloc(app->fs_api);
|
||||
const char* error = NULL;
|
||||
bool success = false;
|
||||
|
||||
do {
|
||||
if(!storage_file_open(
|
||||
app->file,
|
||||
furi_string_get_cstr(app->file_path),
|
||||
FSAM_READ | FSAM_WRITE,
|
||||
FSOM_CREATE_NEW))
|
||||
break;
|
||||
|
||||
uint64_t size = image_sizes[app->create_image_size].value;
|
||||
if(size == app->create_image_max) size--;
|
||||
if(!storage_file_expand(app->file, size)) break;
|
||||
|
||||
// Format as exFAT
|
||||
error = "Image formatting failed";
|
||||
if(storage_virtual_init(app->fs_api, app->file) != FSE_OK) {
|
||||
if(storage_virtual_quit(app->fs_api) != FSE_OK) break;
|
||||
if(storage_virtual_init(app->fs_api, app->file) != FSE_OK) break;
|
||||
}
|
||||
if(storage_virtual_format(app->fs_api) == FSE_OK) {
|
||||
success = true;
|
||||
error = NULL;
|
||||
}
|
||||
storage_virtual_quit(app->fs_api);
|
||||
} while(false);
|
||||
|
||||
if(!success) {
|
||||
error = storage_file_get_error_desc(app->file);
|
||||
FS_Error error = storage_file_get_error(app->file);
|
||||
storage_file_close(app->file);
|
||||
if(error != FSE_EXIST) {
|
||||
storage_common_remove(app->fs_api, furi_string_get_cstr(app->file_path));
|
||||
}
|
||||
}
|
||||
storage_file_free(app->file);
|
||||
mass_storage_app_show_loading_popup(app, false);
|
||||
|
||||
if(error) {
|
||||
popup_set_header(
|
||||
app->popup, "Error Creating Image!", 64, 26, AlignCenter, AlignCenter);
|
||||
popup_set_text(app->popup, error, 64, 40, AlignCenter, AlignCenter);
|
||||
popup_set_callback(app->popup, popup_callback_error);
|
||||
} else {
|
||||
popup_set_header(app->popup, "Image Created!", 64, 32, AlignCenter, AlignCenter);
|
||||
popup_set_text(app->popup, "", 0, 0, AlignLeft, AlignBottom);
|
||||
popup_set_callback(app->popup, popup_callback_ok);
|
||||
}
|
||||
popup_set_context(app->popup, app);
|
||||
popup_set_timeout(app->popup, 0);
|
||||
popup_disable_timeout(app->popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewPopup);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void mass_storage_scene_create_image_on_exit(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
variable_item_list_reset(app->variable_item_list);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#include "../mass_storage_app_i.h"
|
||||
|
||||
enum TextInputIndex {
|
||||
TextInputResultOk,
|
||||
};
|
||||
|
||||
static void mass_storage_scene_create_image_name_text_input_callback(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, TextInputResultOk);
|
||||
}
|
||||
|
||||
void mass_storage_scene_create_image_name_on_enter(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
TextInput* text_input = app->text_input;
|
||||
|
||||
text_input_set_header_text(text_input, "Image name, empty = default");
|
||||
|
||||
text_input_set_minimum_length(text_input, 0);
|
||||
|
||||
text_input_set_result_callback(
|
||||
text_input,
|
||||
mass_storage_scene_create_image_name_text_input_callback,
|
||||
app,
|
||||
app->create_image_name,
|
||||
sizeof(app->create_image_name),
|
||||
false);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewTextInput);
|
||||
}
|
||||
|
||||
bool mass_storage_scene_create_image_name_on_event(void* context, SceneManagerEvent event) {
|
||||
MassStorageApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
consumed = true;
|
||||
switch(event.event) {
|
||||
case TextInputResultOk:
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void mass_storage_scene_create_image_name_on_exit(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
text_input_reset(app->text_input);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#include "../mass_storage_app_i.h"
|
||||
#include "furi_hal_power.h"
|
||||
|
||||
static bool mass_storage_file_select(MassStorageApp* mass_storage) {
|
||||
furi_assert(mass_storage);
|
||||
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
dialog_file_browser_set_basic_options(&browser_options, ".img|.iso", &I_floppydisk_10px);
|
||||
browser_options.base_path = MASS_STORAGE_APP_PATH_FOLDER;
|
||||
browser_options.hide_ext = false;
|
||||
|
||||
// Input events and views are managed by file_select
|
||||
bool res = dialog_file_browser_show(
|
||||
mass_storage->dialogs, mass_storage->file_path, mass_storage->file_path, &browser_options);
|
||||
return res;
|
||||
}
|
||||
|
||||
void mass_storage_scene_file_select_on_enter(void* context) {
|
||||
MassStorageApp* mass_storage = context;
|
||||
|
||||
if(mass_storage_file_select(mass_storage)) {
|
||||
scene_manager_next_scene(mass_storage->scene_manager, MassStorageSceneWork);
|
||||
} else {
|
||||
scene_manager_previous_scene(mass_storage->scene_manager);
|
||||
}
|
||||
}
|
||||
|
||||
bool mass_storage_scene_file_select_on_event(void* context, SceneManagerEvent event) {
|
||||
UNUSED(context);
|
||||
UNUSED(event);
|
||||
// MassStorageApp* mass_storage = context;
|
||||
return false;
|
||||
}
|
||||
|
||||
void mass_storage_scene_file_select_on_exit(void* context) {
|
||||
UNUSED(context);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#include "../mass_storage_app_i.h"
|
||||
|
||||
enum VarItemListIndex {
|
||||
VarItemListIndexSelectDiskImage,
|
||||
VarItemListIndexCreateDiskImage,
|
||||
};
|
||||
|
||||
static void mass_storage_scene_start_variable_item_list_callback(void* context, uint32_t index) {
|
||||
MassStorageApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void mass_storage_scene_start_on_enter(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
VariableItemList* variable_item_list = app->variable_item_list;
|
||||
|
||||
variable_item_list_add(variable_item_list, "Select Disk Image", 0, NULL, app);
|
||||
|
||||
variable_item_list_add(variable_item_list, "Create Disk Image", 0, NULL, app);
|
||||
|
||||
variable_item_list_set_enter_callback(
|
||||
variable_item_list, mass_storage_scene_start_variable_item_list_callback, app);
|
||||
|
||||
variable_item_list_set_header(variable_item_list, "USB Mass Storage");
|
||||
|
||||
variable_item_list_set_selected_item(
|
||||
variable_item_list,
|
||||
scene_manager_get_scene_state(app->scene_manager, MassStorageSceneStart));
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewStart);
|
||||
}
|
||||
|
||||
bool mass_storage_scene_start_on_event(void* context, SceneManagerEvent event) {
|
||||
UNUSED(context);
|
||||
UNUSED(event);
|
||||
MassStorageApp* app = context;
|
||||
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
scene_manager_set_scene_state(app->scene_manager, MassStorageSceneStart, event.event);
|
||||
consumed = true;
|
||||
switch(event.event) {
|
||||
case VarItemListIndexSelectDiskImage:
|
||||
scene_manager_next_scene(app->scene_manager, MassStorageSceneFileSelect);
|
||||
break;
|
||||
case VarItemListIndexCreateDiskImage:
|
||||
scene_manager_set_scene_state(app->scene_manager, MassStorageSceneCreateImage, 0);
|
||||
scene_manager_next_scene(app->scene_manager, MassStorageSceneCreateImage);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void mass_storage_scene_start_on_exit(void* context) {
|
||||
UNUSED(context);
|
||||
MassStorageApp* app = context;
|
||||
variable_item_list_reset(app->variable_item_list);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
#include "../mass_storage_app_i.h"
|
||||
#include "../views/mass_storage_view.h"
|
||||
#include "../helpers/mass_storage_usb.h"
|
||||
#include <lib/toolbox/path.h>
|
||||
|
||||
#define TAG "MassStorageSceneWork"
|
||||
|
||||
static bool file_read(
|
||||
void* ctx,
|
||||
uint32_t lba,
|
||||
uint16_t count,
|
||||
uint8_t* out,
|
||||
uint32_t* out_len,
|
||||
uint32_t out_cap) {
|
||||
MassStorageApp* app = ctx;
|
||||
FURI_LOG_T(TAG, "file_read lba=%08lX count=%04X out_cap=%08lX", lba, count, out_cap);
|
||||
if(!storage_file_seek(app->file, lba * SCSI_BLOCK_SIZE, true)) {
|
||||
FURI_LOG_W(TAG, "seek failed");
|
||||
return false;
|
||||
}
|
||||
uint16_t clamp = MIN(out_cap, count * SCSI_BLOCK_SIZE);
|
||||
*out_len = storage_file_read(app->file, out, clamp);
|
||||
FURI_LOG_T(TAG, "%lu/%lu", *out_len, count * SCSI_BLOCK_SIZE);
|
||||
app->bytes_read += *out_len;
|
||||
return *out_len == clamp;
|
||||
}
|
||||
|
||||
static bool file_write(void* ctx, uint32_t lba, uint16_t count, uint8_t* buf, uint32_t len) {
|
||||
MassStorageApp* app = ctx;
|
||||
FURI_LOG_T(TAG, "file_write lba=%08lX count=%04X len=%08lX", lba, count, len);
|
||||
if(len != count * SCSI_BLOCK_SIZE) {
|
||||
FURI_LOG_W(TAG, "bad write params count=%u len=%lu", count, len);
|
||||
return false;
|
||||
}
|
||||
if(!storage_file_seek(app->file, lba * SCSI_BLOCK_SIZE, true)) {
|
||||
FURI_LOG_W(TAG, "seek failed");
|
||||
return false;
|
||||
}
|
||||
app->bytes_written += len;
|
||||
return storage_file_write(app->file, buf, len) == len;
|
||||
}
|
||||
|
||||
static uint32_t file_num_blocks(void* ctx) {
|
||||
MassStorageApp* app = ctx;
|
||||
return storage_file_size(app->file) / SCSI_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
static void file_eject(void* ctx) {
|
||||
MassStorageApp* app = ctx;
|
||||
FURI_LOG_D(TAG, "EJECT");
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, MassStorageCustomEventEject);
|
||||
}
|
||||
|
||||
bool mass_storage_scene_work_on_event(void* context, SceneManagerEvent event) {
|
||||
MassStorageApp* app = context;
|
||||
bool consumed = false;
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == MassStorageCustomEventEject) {
|
||||
consumed = scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, MassStorageSceneFileSelect);
|
||||
if(!consumed) {
|
||||
consumed = scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, MassStorageSceneStart);
|
||||
}
|
||||
}
|
||||
} else if(event.type == SceneManagerEventTypeTick) {
|
||||
mass_storage_set_stats(app->mass_storage_view, app->bytes_read, app->bytes_written);
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
consumed = scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, MassStorageSceneFileSelect);
|
||||
if(!consumed) {
|
||||
consumed = scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, MassStorageSceneStart);
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void mass_storage_scene_work_on_enter(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
app->bytes_read = app->bytes_written = 0;
|
||||
|
||||
if(!storage_file_exists(app->fs_api, furi_string_get_cstr(app->file_path))) {
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, MassStorageSceneStart);
|
||||
return;
|
||||
}
|
||||
|
||||
mass_storage_app_show_loading_popup(app, true);
|
||||
|
||||
app->usb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
|
||||
FuriString* file_name = furi_string_alloc();
|
||||
path_extract_filename(app->file_path, file_name, true);
|
||||
|
||||
mass_storage_set_file_name(app->mass_storage_view, file_name);
|
||||
app->file = storage_file_alloc(app->fs_api);
|
||||
furi_assert(storage_file_open(
|
||||
app->file,
|
||||
furi_string_get_cstr(app->file_path),
|
||||
FSAM_READ | FSAM_WRITE,
|
||||
FSOM_OPEN_EXISTING));
|
||||
|
||||
SCSIDeviceFunc fn = {
|
||||
.ctx = app,
|
||||
.read = file_read,
|
||||
.write = file_write,
|
||||
.num_blocks = file_num_blocks,
|
||||
.eject = file_eject,
|
||||
};
|
||||
|
||||
furi_hal_usb_unlock();
|
||||
app->usb = mass_storage_usb_start(furi_string_get_cstr(file_name), fn);
|
||||
|
||||
furi_string_free(file_name);
|
||||
|
||||
mass_storage_app_show_loading_popup(app, false);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, MassStorageAppViewWork);
|
||||
}
|
||||
|
||||
void mass_storage_scene_work_on_exit(void* context) {
|
||||
MassStorageApp* app = context;
|
||||
mass_storage_app_show_loading_popup(app, true);
|
||||
|
||||
if(app->usb_mutex) {
|
||||
furi_mutex_free(app->usb_mutex);
|
||||
app->usb_mutex = NULL;
|
||||
}
|
||||
if(app->usb) {
|
||||
mass_storage_usb_stop(app->usb);
|
||||
app->usb = NULL;
|
||||
}
|
||||
if(app->file) {
|
||||
storage_file_free(app->file);
|
||||
app->file = NULL;
|
||||
}
|
||||
mass_storage_app_show_loading_popup(app, false);
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
#include "mass_storage_view.h"
|
||||
#include "../mass_storage_app_i.h"
|
||||
#include <gui/elements.h>
|
||||
|
||||
struct MassStorage {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
FuriString *file_name, *status_string;
|
||||
uint32_t read_speed, write_speed;
|
||||
uint32_t bytes_read, bytes_written;
|
||||
uint32_t update_time;
|
||||
} MassStorageModel;
|
||||
|
||||
static void append_suffixed_byte_count(FuriString* string, uint32_t count) {
|
||||
if(count < 1024) {
|
||||
furi_string_cat_printf(string, "%luB", count);
|
||||
} else if(count < 1024 * 1024) {
|
||||
furi_string_cat_printf(string, "%luK", count / 1024);
|
||||
} else if(count < 1024 * 1024 * 1024) {
|
||||
furi_string_cat_printf(string, "%.1fM", (double)count / (1024 * 1024));
|
||||
} else {
|
||||
furi_string_cat_printf(string, "%.1fG", (double)count / (1024 * 1024 * 1024));
|
||||
}
|
||||
}
|
||||
|
||||
static void mass_storage_draw_callback(Canvas* canvas, void* _model) {
|
||||
MassStorageModel* model = _model;
|
||||
|
||||
canvas_draw_icon(canvas, 8, 14, &I_Drive_112x35);
|
||||
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, canvas_width(canvas) / 2, 0, AlignCenter, AlignTop, "USB Mass Storage");
|
||||
|
||||
canvas_set_font(canvas, FontBatteryPercent);
|
||||
elements_string_fit_width(canvas, model->file_name, 89 - 2);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 92, 24, AlignRight, AlignBottom, furi_string_get_cstr(model->file_name));
|
||||
|
||||
furi_string_set_str(model->status_string, "R:");
|
||||
append_suffixed_byte_count(model->status_string, model->bytes_read);
|
||||
if(model->read_speed) {
|
||||
furi_string_cat_str(model->status_string, "/");
|
||||
append_suffixed_byte_count(model->status_string, model->read_speed);
|
||||
furi_string_cat_str(model->status_string, "s");
|
||||
}
|
||||
canvas_draw_str(canvas, 14, 34, furi_string_get_cstr(model->status_string));
|
||||
|
||||
furi_string_set_str(model->status_string, "W:");
|
||||
append_suffixed_byte_count(model->status_string, model->bytes_written);
|
||||
if(model->write_speed) {
|
||||
furi_string_cat_str(model->status_string, "/");
|
||||
append_suffixed_byte_count(model->status_string, model->write_speed);
|
||||
furi_string_cat_str(model->status_string, "s");
|
||||
}
|
||||
canvas_draw_str(canvas, 14, 43, furi_string_get_cstr(model->status_string));
|
||||
}
|
||||
|
||||
MassStorage* mass_storage_alloc() {
|
||||
MassStorage* mass_storage = malloc(sizeof(MassStorage));
|
||||
|
||||
mass_storage->view = view_alloc();
|
||||
view_allocate_model(mass_storage->view, ViewModelTypeLocking, sizeof(MassStorageModel));
|
||||
with_view_model(
|
||||
mass_storage->view,
|
||||
MassStorageModel * model,
|
||||
{
|
||||
model->file_name = furi_string_alloc();
|
||||
model->status_string = furi_string_alloc();
|
||||
},
|
||||
false);
|
||||
view_set_context(mass_storage->view, mass_storage);
|
||||
view_set_draw_callback(mass_storage->view, mass_storage_draw_callback);
|
||||
|
||||
return mass_storage;
|
||||
}
|
||||
|
||||
void mass_storage_free(MassStorage* mass_storage) {
|
||||
furi_assert(mass_storage);
|
||||
with_view_model(
|
||||
mass_storage->view,
|
||||
MassStorageModel * model,
|
||||
{
|
||||
furi_string_free(model->file_name);
|
||||
furi_string_free(model->status_string);
|
||||
},
|
||||
false);
|
||||
view_free(mass_storage->view);
|
||||
free(mass_storage);
|
||||
}
|
||||
|
||||
View* mass_storage_get_view(MassStorage* mass_storage) {
|
||||
furi_assert(mass_storage);
|
||||
return mass_storage->view;
|
||||
}
|
||||
|
||||
void mass_storage_set_file_name(MassStorage* mass_storage, FuriString* name) {
|
||||
furi_assert(name);
|
||||
with_view_model(
|
||||
mass_storage->view,
|
||||
MassStorageModel * model,
|
||||
{ furi_string_set(model->file_name, name); },
|
||||
true);
|
||||
}
|
||||
|
||||
void mass_storage_set_stats(MassStorage* mass_storage, uint32_t read, uint32_t written) {
|
||||
with_view_model(
|
||||
mass_storage->view,
|
||||
MassStorageModel * model,
|
||||
{
|
||||
uint32_t now = furi_get_tick();
|
||||
model->read_speed = (read - model->bytes_read) * 1000 / (now - model->update_time);
|
||||
model->write_speed =
|
||||
(written - model->bytes_written) * 1000 / (now - model->update_time);
|
||||
model->bytes_read = read;
|
||||
model->bytes_written = written;
|
||||
model->update_time = now;
|
||||
},
|
||||
true);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
typedef struct MassStorage MassStorage;
|
||||
|
||||
MassStorage* mass_storage_alloc();
|
||||
|
||||
void mass_storage_free(MassStorage* mass_storage);
|
||||
|
||||
View* mass_storage_get_view(MassStorage* mass_storage);
|
||||
|
||||
void mass_storage_set_file_name(MassStorage* mass_storage, FuriString* name);
|
||||
|
||||
void mass_storage_set_stats(MassStorage* mass_storage, uint32_t read, uint32_t written);
|
||||
@@ -1,14 +0,0 @@
|
||||
App(
|
||||
appid="nightstand",
|
||||
name="Nightstand Clock",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="clock_app",
|
||||
requires=["gui"],
|
||||
stack_size=2 * 1024,
|
||||
fap_icon="clock.png",
|
||||
fap_category="Tools",
|
||||
fap_author="@nymda & @Willy-JL",
|
||||
fap_weburl="https://github.com/nymda/FlipperNightStand",
|
||||
fap_version="1.0",
|
||||
fap_description="Clock with screen brightness controls",
|
||||
)
|
||||
|
Before Width: | Height: | Size: 7.7 KiB |
@@ -1,366 +0,0 @@
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <gui/elements.h>
|
||||
|
||||
#include <notification/notification_messages.h>
|
||||
#include <notification/notification_app.h>
|
||||
|
||||
#include "clock_app.h"
|
||||
|
||||
/*
|
||||
This is a modified version of the default clock app intended for use overnight
|
||||
Up / Down controls the displays brightness. Down at brightness 0 turns the notification LED on and off.
|
||||
*/
|
||||
|
||||
int brightness = 5;
|
||||
bool led = false;
|
||||
NotificationApp* notif = 0;
|
||||
|
||||
int dspBrightnessBarFrames = 0;
|
||||
const int dspBrightnessBarDisplayFrames = 8;
|
||||
|
||||
const NotificationMessage message_red_dim = {
|
||||
.type = NotificationMessageTypeLedRed,
|
||||
.data.led.value = 0xFF / 16,
|
||||
};
|
||||
|
||||
const NotificationMessage message_red_off = {
|
||||
.type = NotificationMessageTypeLedRed,
|
||||
.data.led.value = 0x00,
|
||||
};
|
||||
|
||||
static const NotificationSequence led_on = {
|
||||
&message_red_dim,
|
||||
&message_do_not_reset,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const NotificationSequence led_off = {
|
||||
&message_red_off,
|
||||
&message_do_not_reset,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const NotificationSequence led_reset = {
|
||||
&message_red_0,
|
||||
NULL,
|
||||
};
|
||||
|
||||
void set_backlight_brightness(float brightness) {
|
||||
notif->settings.display_brightness = brightness;
|
||||
notification_message(notif, &sequence_display_backlight_on);
|
||||
}
|
||||
|
||||
void handle_up() {
|
||||
dspBrightnessBarFrames = dspBrightnessBarDisplayFrames;
|
||||
if(brightness < 100) {
|
||||
led = false;
|
||||
notification_message(notif, &led_off);
|
||||
brightness += 5;
|
||||
}
|
||||
set_backlight_brightness((float)(brightness / 100.f));
|
||||
}
|
||||
|
||||
void handle_down() {
|
||||
dspBrightnessBarFrames = dspBrightnessBarDisplayFrames;
|
||||
if(brightness > 0) {
|
||||
brightness -= 5;
|
||||
if(brightness == 0) { //trigger only on the first brightness 5 -> 0 transition
|
||||
led = true;
|
||||
notification_message(notif, &led_on);
|
||||
}
|
||||
} else if(brightness == 0) { //trigger on every down press afterwards
|
||||
led = !led;
|
||||
if(led) {
|
||||
notification_message(notif, &led_on);
|
||||
} else {
|
||||
notification_message(notif, &led_off);
|
||||
}
|
||||
}
|
||||
set_backlight_brightness((float)(brightness / 100.f));
|
||||
}
|
||||
|
||||
static void clock_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
|
||||
furi_assert(event_queue);
|
||||
PluginEvent event = {.type = EventTypeKey, .input = *input_event};
|
||||
furi_message_queue_put(event_queue, &event, FuriWaitForever);
|
||||
}
|
||||
|
||||
//do you are have stupid?
|
||||
void elements_progress_bar_vertical(
|
||||
Canvas* canvas,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t height,
|
||||
float progress) {
|
||||
furi_assert(canvas);
|
||||
furi_assert((progress >= 0) && (progress <= 1.0));
|
||||
uint8_t width = 9;
|
||||
|
||||
uint8_t progress_length = roundf((1.f - progress) * (height - 2));
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(canvas, x + 1, y + 1, width - 2, height - 2);
|
||||
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_box(canvas, x + 1, y + 1, width - 2, progress_length);
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_rframe(canvas, x, y, width, height, 3);
|
||||
}
|
||||
|
||||
static void clock_render_callback(Canvas* const canvas, void* ctx) {
|
||||
//canvas_clear(canvas);
|
||||
//canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
//avoids a bug with the brightness being reverted after the backlight-off period
|
||||
set_backlight_brightness((float)(brightness / 100.f));
|
||||
|
||||
if(dspBrightnessBarFrames > 0) {
|
||||
elements_progress_bar_vertical(canvas, 119, 1, 62, (float)(brightness / 100.f));
|
||||
dspBrightnessBarFrames--;
|
||||
}
|
||||
|
||||
ClockState* state = ctx;
|
||||
if(furi_mutex_acquire(state->mutex, 200) != FuriStatusOk) {
|
||||
//FURI_LOG_D(TAG, "Can't obtain mutex, requeue render");
|
||||
PluginEvent event = {.type = EventTypeTick};
|
||||
furi_message_queue_put(state->event_queue, &event, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime curr_dt;
|
||||
furi_hal_rtc_get_datetime(&curr_dt);
|
||||
uint32_t curr_ts = datetime_datetime_to_timestamp(&curr_dt);
|
||||
|
||||
char time_string[TIME_LEN];
|
||||
char date_string[DATE_LEN];
|
||||
char meridian_string[MERIDIAN_LEN];
|
||||
char timer_string[20];
|
||||
|
||||
if(state->time_format == LocaleTimeFormat24h) {
|
||||
snprintf(
|
||||
time_string, TIME_LEN, CLOCK_TIME_FORMAT, curr_dt.hour, curr_dt.minute, curr_dt.second);
|
||||
} else {
|
||||
bool pm = curr_dt.hour > 12;
|
||||
bool pm12 = curr_dt.hour >= 12;
|
||||
snprintf(
|
||||
time_string,
|
||||
TIME_LEN,
|
||||
CLOCK_TIME_FORMAT,
|
||||
pm ? curr_dt.hour - 12 : curr_dt.hour,
|
||||
curr_dt.minute,
|
||||
curr_dt.second);
|
||||
|
||||
snprintf(
|
||||
meridian_string,
|
||||
MERIDIAN_LEN,
|
||||
MERIDIAN_FORMAT,
|
||||
pm12 ? MERIDIAN_STRING_PM : MERIDIAN_STRING_AM);
|
||||
}
|
||||
|
||||
if(state->date_format == LocaleDateFormatYMD) {
|
||||
snprintf(
|
||||
date_string, DATE_LEN, CLOCK_ISO_DATE_FORMAT, curr_dt.year, curr_dt.month, curr_dt.day);
|
||||
} else if(state->date_format == LocaleDateFormatMDY) {
|
||||
snprintf(
|
||||
date_string, DATE_LEN, CLOCK_RFC_DATE_FORMAT, curr_dt.month, curr_dt.day, curr_dt.year);
|
||||
} else {
|
||||
snprintf(
|
||||
date_string, DATE_LEN, CLOCK_RFC_DATE_FORMAT, curr_dt.day, curr_dt.month, curr_dt.year);
|
||||
}
|
||||
|
||||
bool timer_running = state->timer_running;
|
||||
uint32_t timer_start_timestamp = state->timer_start_timestamp;
|
||||
uint32_t timer_stopped_seconds = state->timer_stopped_seconds;
|
||||
|
||||
furi_mutex_release(state->mutex);
|
||||
|
||||
canvas_set_font(canvas, FontBigNumbers);
|
||||
|
||||
if(timer_start_timestamp != 0) {
|
||||
int32_t elapsed_secs = timer_running ? (curr_ts - timer_start_timestamp) :
|
||||
timer_stopped_seconds;
|
||||
snprintf(timer_string, 20, "%.2ld:%.2ld", elapsed_secs / 60, elapsed_secs % 60);
|
||||
canvas_draw_str_aligned(canvas, 64, 8, AlignCenter, AlignCenter, time_string); // DRAW TIME
|
||||
canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignTop, timer_string); // DRAW TIMER
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(canvas, 64, 20, AlignCenter, AlignTop, date_string); // DRAW DATE
|
||||
elements_button_left(canvas, "Reset");
|
||||
} else {
|
||||
canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignCenter, time_string);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(canvas, 65, 17, AlignCenter, AlignCenter, date_string);
|
||||
|
||||
if(state->time_format == LocaleTimeFormat12h)
|
||||
canvas_draw_str_aligned(canvas, 64, 47, AlignCenter, AlignCenter, meridian_string);
|
||||
}
|
||||
if(timer_running) {
|
||||
elements_button_center(canvas, "Stop");
|
||||
} else if(timer_start_timestamp != 0 && !timer_running) {
|
||||
elements_button_center(canvas, "Start");
|
||||
}
|
||||
}
|
||||
|
||||
static void clock_state_init(ClockState* const state) {
|
||||
state->time_format = locale_get_time_format();
|
||||
|
||||
state->date_format = locale_get_date_format();
|
||||
|
||||
//FURI_LOG_D(TAG, "Time format: %s", state->settings.time_format == H12 ? "12h" : "24h");
|
||||
//FURI_LOG_D(TAG, "Date format: %s", state->settings.date_format == Iso ? "ISO 8601" : "RFC 5322");
|
||||
//furi_hal_rtc_get_datetime(&state->datetime);
|
||||
}
|
||||
|
||||
// Runs every 1000ms by default
|
||||
static void clock_tick(void* ctx) {
|
||||
furi_assert(ctx);
|
||||
FuriMessageQueue* event_queue = ctx;
|
||||
PluginEvent event = {.type = EventTypeTick};
|
||||
// It's OK to loose this event if system overloaded
|
||||
furi_message_queue_put(event_queue, &event, 0);
|
||||
}
|
||||
|
||||
void timer_start_stop(ClockState* plugin_state) {
|
||||
// START/STOP TIMER
|
||||
uint32_t curr_ts = furi_hal_rtc_get_timestamp();
|
||||
|
||||
if(plugin_state->timer_running) {
|
||||
// Update stopped seconds
|
||||
plugin_state->timer_stopped_seconds = curr_ts - plugin_state->timer_start_timestamp;
|
||||
} else {
|
||||
if(plugin_state->timer_start_timestamp == 0) {
|
||||
// Set starting timestamp if this is first time
|
||||
plugin_state->timer_start_timestamp = curr_ts;
|
||||
} else {
|
||||
// Timer was already running, need to slightly readjust so we don't
|
||||
// count the intervening time
|
||||
plugin_state->timer_start_timestamp = curr_ts - plugin_state->timer_stopped_seconds;
|
||||
}
|
||||
}
|
||||
plugin_state->timer_running = !plugin_state->timer_running;
|
||||
}
|
||||
|
||||
void timer_reset_seconds(ClockState* plugin_state) {
|
||||
if(plugin_state->timer_start_timestamp != 0) {
|
||||
// Reset seconds
|
||||
plugin_state->timer_running = false;
|
||||
plugin_state->timer_start_timestamp = 0;
|
||||
plugin_state->timer_stopped_seconds = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t clock_app(void* p) {
|
||||
UNUSED(p);
|
||||
ClockState* plugin_state = malloc(sizeof(ClockState));
|
||||
|
||||
plugin_state->event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
|
||||
if(plugin_state->event_queue == NULL) {
|
||||
FURI_LOG_E(TAG, "Cannot create event queue");
|
||||
free(plugin_state);
|
||||
return 255;
|
||||
}
|
||||
//FURI_LOG_D(TAG, "Event queue created");
|
||||
|
||||
plugin_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
if(plugin_state->mutex == NULL) {
|
||||
FURI_LOG_E(TAG, "Cannot create mutex");
|
||||
furi_message_queue_free(plugin_state->event_queue);
|
||||
free(plugin_state);
|
||||
return 255;
|
||||
}
|
||||
//FURI_LOG_D(TAG, "Mutex created");
|
||||
|
||||
clock_state_init(plugin_state);
|
||||
|
||||
notif = furi_record_open(RECORD_NOTIFICATION);
|
||||
float tmpBrightness = notif->settings.display_brightness;
|
||||
brightness = tmpBrightness * 100; // Keep current brightness by default
|
||||
|
||||
notification_message(notif, &sequence_display_backlight_enforce_on);
|
||||
notification_message(notif, &led_off);
|
||||
|
||||
// Set system callbacks
|
||||
ViewPort* view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(view_port, clock_render_callback, plugin_state);
|
||||
view_port_input_callback_set(view_port, clock_input_callback, plugin_state->event_queue);
|
||||
|
||||
FuriTimer* timer =
|
||||
furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, plugin_state->event_queue);
|
||||
|
||||
if(timer == NULL) {
|
||||
FURI_LOG_E(TAG, "Cannot create timer");
|
||||
furi_mutex_free(plugin_state->mutex);
|
||||
furi_message_queue_free(plugin_state->event_queue);
|
||||
free(plugin_state);
|
||||
return 255;
|
||||
}
|
||||
//FURI_LOG_D(TAG, "Timer created");
|
||||
|
||||
// Open GUI and register view_port
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||
|
||||
furi_timer_start(timer, furi_kernel_get_tick_frequency());
|
||||
//FURI_LOG_D(TAG, "Timer started");
|
||||
|
||||
// Main loop
|
||||
PluginEvent event;
|
||||
for(bool processing = true; processing;) {
|
||||
FuriStatus event_status = furi_message_queue_get(plugin_state->event_queue, &event, 100);
|
||||
|
||||
if(event_status != FuriStatusOk) continue;
|
||||
|
||||
if(furi_mutex_acquire(plugin_state->mutex, FuriWaitForever) != FuriStatusOk) continue;
|
||||
// press events
|
||||
if(event.type == EventTypeKey) {
|
||||
if(event.input.type == InputTypeShort) {
|
||||
switch(event.input.key) {
|
||||
case InputKeyLeft:
|
||||
// Reset seconds
|
||||
timer_reset_seconds(plugin_state);
|
||||
break;
|
||||
case InputKeyOk:
|
||||
// Toggle timer
|
||||
timer_start_stop(plugin_state);
|
||||
break;
|
||||
case InputKeyBack:
|
||||
// Exit the plugin
|
||||
processing = false;
|
||||
break;
|
||||
case InputKeyUp:
|
||||
handle_up();
|
||||
break;
|
||||
case InputKeyDown:
|
||||
handle_down();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} /*else if(event.type == EventTypeTick) {
|
||||
furi_hal_rtc_get_datetime(&plugin_state->datetime);
|
||||
}*/
|
||||
|
||||
furi_mutex_release(plugin_state->mutex);
|
||||
view_port_update(view_port);
|
||||
}
|
||||
|
||||
furi_timer_free(timer);
|
||||
view_port_enabled_set(view_port, false);
|
||||
gui_remove_view_port(gui, view_port);
|
||||
furi_record_close(RECORD_GUI);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
view_port_free(view_port);
|
||||
furi_message_queue_free(plugin_state->event_queue);
|
||||
furi_mutex_free(plugin_state->mutex);
|
||||
free(plugin_state);
|
||||
|
||||
set_backlight_brightness(tmpBrightness);
|
||||
notification_message(notif, &sequence_display_backlight_enforce_auto);
|
||||
notification_message(notif, &led_reset);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <input/input.h>
|
||||
#include <locale/locale.h>
|
||||
|
||||
#define TAG "Clock"
|
||||
|
||||
#define CLOCK_ISO_DATE_FORMAT "%.4d-%.2d-%.2d"
|
||||
#define CLOCK_RFC_DATE_FORMAT "%.2d-%.2d-%.4d"
|
||||
#define CLOCK_TIME_FORMAT "%.2d:%.2d:%.2d"
|
||||
|
||||
#define MERIDIAN_FORMAT "%s"
|
||||
#define MERIDIAN_STRING_AM "AM"
|
||||
#define MERIDIAN_STRING_PM "PM"
|
||||
|
||||
#define TIME_LEN 12
|
||||
#define DATE_LEN 14
|
||||
#define MERIDIAN_LEN 3
|
||||
|
||||
typedef enum {
|
||||
EventTypeTick,
|
||||
EventTypeKey,
|
||||
} EventType;
|
||||
|
||||
typedef struct {
|
||||
EventType type;
|
||||
InputEvent input;
|
||||
} PluginEvent;
|
||||
|
||||
typedef struct {
|
||||
LocaleDateFormat date_format;
|
||||
LocaleTimeFormat time_format;
|
||||
DateTime datetime;
|
||||
FuriMutex* mutex;
|
||||
FuriMessageQueue* event_queue;
|
||||
uint32_t timer_start_timestamp;
|
||||
uint32_t timer_stopped_seconds;
|
||||
bool timer_running;
|
||||
} ClockState;
|
||||
@@ -1,15 +0,0 @@
|
||||
App(
|
||||
appid="subghz_playlist",
|
||||
name="Sub-GHz Playlist",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="playlist_app",
|
||||
requires=["storage", "gui", "dialogs", "subghz"],
|
||||
stack_size=2 * 1024,
|
||||
order=14,
|
||||
fap_icon="subplaylist_10px.png",
|
||||
fap_category="Sub-GHz",
|
||||
fap_icon_assets="images",
|
||||
fap_author="@darmiel",
|
||||
fap_version="1.0",
|
||||
fap_description="App works with list of sub-ghz files from .txt file that contains paths to target files.",
|
||||
)
|
||||
@@ -1,81 +0,0 @@
|
||||
#include <gui/gui.h>
|
||||
|
||||
#define WIDTH 128
|
||||
#define HEIGHT 64
|
||||
|
||||
void draw_centered_boxed_str(Canvas* canvas, int x, int y, int height, int pad, const char* text) {
|
||||
// get width of text
|
||||
int w = canvas_string_width(canvas, text);
|
||||
canvas_draw_rframe(canvas, x, y, w + pad, height, 2);
|
||||
canvas_draw_str_aligned(canvas, x + pad / 2, y + height / 2, AlignLeft, AlignCenter, text);
|
||||
}
|
||||
|
||||
void draw_corner_aligned(Canvas* canvas, int width, int height, Align horizontal, Align vertical) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
switch(horizontal) {
|
||||
case AlignLeft:
|
||||
switch(vertical) {
|
||||
case AlignTop:
|
||||
canvas_draw_rbox(canvas, 0, 0, width, height, 3);
|
||||
canvas_draw_box(canvas, 0, 0, width, 3);
|
||||
canvas_draw_box(canvas, 0, 0, 3, height);
|
||||
break;
|
||||
case AlignCenter:
|
||||
canvas_draw_rbox(canvas, 0, HEIGHT - height / 2, width, height, 3);
|
||||
canvas_draw_box(canvas, 0, HEIGHT - height / 2, 3, height);
|
||||
break;
|
||||
case AlignBottom:
|
||||
canvas_draw_rbox(canvas, 0, HEIGHT - height, width, height, 3);
|
||||
canvas_draw_box(canvas, 0, HEIGHT - height, 3, height);
|
||||
canvas_draw_box(canvas, 0, HEIGHT - 3, width, 3);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case AlignRight:
|
||||
switch(vertical) {
|
||||
case AlignTop:
|
||||
canvas_draw_rbox(canvas, WIDTH - width, 0, width, height, 3);
|
||||
canvas_draw_box(canvas, WIDTH - width, 0, width, 3); // bottom corner
|
||||
canvas_draw_box(canvas, WIDTH - 3, 0, 3, height); // right corner
|
||||
break;
|
||||
case AlignCenter:
|
||||
canvas_draw_rbox(canvas, WIDTH - width, HEIGHT / 2 - height / 2, width, height, 3);
|
||||
canvas_draw_box(canvas, WIDTH - 3, HEIGHT / 2 - height / 2, 3, height); // right corner
|
||||
break;
|
||||
case AlignBottom:
|
||||
canvas_draw_rbox(canvas, WIDTH - width, HEIGHT - height, width, height, 3);
|
||||
canvas_draw_box(canvas, WIDTH - 3, HEIGHT - height, 3, height); // right corner
|
||||
canvas_draw_box(canvas, WIDTH - width, HEIGHT - 3, width, 3); // bottom corner
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case AlignCenter:
|
||||
switch(vertical) {
|
||||
case AlignTop:
|
||||
canvas_draw_rbox(canvas, WIDTH / 2 - width / 2, 0, width, height, 3);
|
||||
canvas_draw_box(canvas, WIDTH / 2 - width / 2, 0, width, 3); // bottom corner
|
||||
canvas_draw_box(canvas, WIDTH / 2 - 3, 0, 3, height); // right corner
|
||||
break;
|
||||
case AlignCenter:
|
||||
canvas_draw_rbox(
|
||||
canvas, WIDTH / 2 - width / 2, HEIGHT / 2 - height / 2, width, height, 3);
|
||||
canvas_draw_box(
|
||||
canvas, WIDTH / 2 - 3, HEIGHT / 2 - height / 2, 3, height); // right corner
|
||||
break;
|
||||
case AlignBottom:
|
||||
canvas_draw_rbox(canvas, WIDTH / 2 - width / 2, HEIGHT - height, width, height, 3);
|
||||
canvas_draw_box(canvas, WIDTH / 2 - 3, HEIGHT - height, 3, height); // right corner
|
||||
canvas_draw_box(canvas, WIDTH / 2 - width / 2, HEIGHT - 3, width, 3); // bottom corner
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#include <gui/gui.h>
|
||||
|
||||
void draw_centered_boxed_str(Canvas* canvas, int x, int y, int height, int pad, const char* text);
|
||||
|
||||
void draw_corner_aligned(Canvas* canvas, int width, int height, Align horizontal, Align vertical);
|
||||
@@ -1,64 +0,0 @@
|
||||
#include "radio_device_loader.h"
|
||||
|
||||
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
|
||||
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
|
||||
|
||||
static void radio_device_loader_power_on() {
|
||||
uint8_t attempts = 0;
|
||||
while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
|
||||
furi_hal_power_enable_otg();
|
||||
//CC1101 power-up time
|
||||
furi_delay_ms(10);
|
||||
}
|
||||
}
|
||||
|
||||
static void radio_device_loader_power_off() {
|
||||
if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
|
||||
}
|
||||
|
||||
bool radio_device_loader_is_connect_external(const char* name) {
|
||||
bool is_connect = false;
|
||||
bool is_otg_enabled = furi_hal_power_is_otg_enabled();
|
||||
|
||||
if(!is_otg_enabled) {
|
||||
radio_device_loader_power_on();
|
||||
}
|
||||
|
||||
const SubGhzDevice* device = subghz_devices_get_by_name(name);
|
||||
if(device) {
|
||||
is_connect = subghz_devices_is_connect(device);
|
||||
}
|
||||
|
||||
if(!is_otg_enabled) {
|
||||
radio_device_loader_power_off();
|
||||
}
|
||||
return is_connect;
|
||||
}
|
||||
|
||||
const SubGhzDevice* radio_device_loader_set(
|
||||
const SubGhzDevice* current_radio_device,
|
||||
SubGhzRadioDeviceType radio_device_type) {
|
||||
const SubGhzDevice* radio_device;
|
||||
|
||||
if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
|
||||
radio_device_loader_is_connect_external(SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
|
||||
radio_device_loader_power_on();
|
||||
radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
|
||||
subghz_devices_begin(radio_device);
|
||||
} else if(current_radio_device == NULL) {
|
||||
radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
|
||||
} else {
|
||||
radio_device_loader_end(current_radio_device);
|
||||
radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
|
||||
}
|
||||
|
||||
return radio_device;
|
||||
}
|
||||
|
||||
void radio_device_loader_end(const SubGhzDevice* radio_device) {
|
||||
furi_assert(radio_device);
|
||||
radio_device_loader_power_off();
|
||||
if(radio_device != subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME)) {
|
||||
subghz_devices_end(radio_device);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <lib/subghz/devices/devices.h>
|
||||
|
||||
/** SubGhzRadioDeviceType */
|
||||
typedef enum {
|
||||
SubGhzRadioDeviceTypeInternal,
|
||||
SubGhzRadioDeviceTypeExternalCC1101,
|
||||
} SubGhzRadioDeviceType;
|
||||
|
||||
const SubGhzDevice* radio_device_loader_set(
|
||||
const SubGhzDevice* current_radio_device,
|
||||
SubGhzRadioDeviceType radio_device_type);
|
||||
|
||||
void radio_device_loader_end(const SubGhzDevice* radio_device);
|
||||
@@ -1,843 +0,0 @@
|
||||
#include <furi.h>
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
#include <lib/toolbox/path.h>
|
||||
#include <subghz_playlist_icons.h>
|
||||
#include <assets_icons.h>
|
||||
|
||||
#include <lib/subghz/protocols/protocol_items.h>
|
||||
#include <flipper_format/flipper_format_i.h>
|
||||
|
||||
#include "helpers/radio_device_loader.h"
|
||||
|
||||
#include "flipper_format_stream.h"
|
||||
#include "flipper_format_stream_i.h"
|
||||
|
||||
#include <lib/subghz/transmitter.h>
|
||||
#include <lib/subghz/protocols/raw.h>
|
||||
|
||||
#include "playlist_file.h"
|
||||
#include "canvas_helper.h"
|
||||
|
||||
#define TAG "Playlist"
|
||||
|
||||
#define STATE_NONE 0
|
||||
#define STATE_OVERVIEW 1
|
||||
#define STATE_SENDING 2
|
||||
|
||||
#define WIDTH 128
|
||||
#define HEIGHT 64
|
||||
|
||||
typedef struct {
|
||||
int current_count; // number of processed files
|
||||
int total_count; // number of items in the playlist
|
||||
|
||||
int playlist_repetitions; // number of times to repeat the whole playlist
|
||||
int current_playlist_repetition; // current playlist repetition
|
||||
|
||||
// last 3 files
|
||||
FuriString* prev_0_path; // current file
|
||||
FuriString* prev_1_path; // previous file
|
||||
FuriString* prev_2_path; // previous previous file
|
||||
FuriString* prev_3_path; // you get the idea
|
||||
|
||||
int state; // current state
|
||||
|
||||
ViewPort* view_port;
|
||||
} DisplayMeta;
|
||||
|
||||
typedef struct {
|
||||
FuriThread* thread;
|
||||
Storage* storage;
|
||||
FlipperFormat* format;
|
||||
|
||||
DisplayMeta* meta;
|
||||
|
||||
FuriString* file_path; // path to the playlist file
|
||||
const SubGhzDevice* radio_device;
|
||||
|
||||
bool ctl_request_exit; // can be set to true if the worker should exit
|
||||
bool ctl_pause; // can be set to true if the worker should pause
|
||||
bool ctl_request_skip; // can be set to true if the worker should skip the current file
|
||||
bool ctl_request_prev; // can be set to true if the worker should go to the previous file
|
||||
|
||||
bool is_running; // indicates if the worker is running
|
||||
} PlaylistWorker;
|
||||
|
||||
typedef struct {
|
||||
FuriMutex* mutex;
|
||||
FuriMessageQueue* input_queue;
|
||||
ViewPort* view_port;
|
||||
Gui* gui;
|
||||
|
||||
DisplayMeta* meta;
|
||||
PlaylistWorker* worker;
|
||||
|
||||
FuriString* file_path; // Path to the playlist file
|
||||
} Playlist;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void meta_set_state(DisplayMeta* meta, int state) {
|
||||
meta->state = state;
|
||||
view_port_update(meta->view_port);
|
||||
}
|
||||
|
||||
static FuriHalSubGhzPreset str_to_preset(FuriString* preset) {
|
||||
if(furi_string_cmp_str(preset, "FuriHalSubGhzPresetOok270Async") == 0) {
|
||||
return FuriHalSubGhzPresetOok270Async;
|
||||
}
|
||||
if(furi_string_cmp_str(preset, "FuriHalSubGhzPresetOok650Async") == 0) {
|
||||
return FuriHalSubGhzPresetOok650Async;
|
||||
}
|
||||
if(furi_string_cmp_str(preset, "FuriHalSubGhzPreset2FSKDev238Async") == 0) {
|
||||
return FuriHalSubGhzPreset2FSKDev238Async;
|
||||
}
|
||||
if(furi_string_cmp_str(preset, "FuriHalSubGhzPreset2FSKDev476Async") == 0) {
|
||||
return FuriHalSubGhzPreset2FSKDev476Async;
|
||||
}
|
||||
if(furi_string_cmp_str(preset, "FuriHalSubGhzPresetMSK99_97KbAsync") == 0) {
|
||||
return FuriHalSubGhzPresetMSK99_97KbAsync;
|
||||
}
|
||||
if(furi_string_cmp_str(preset, "FuriHalSubGhzPresetMSK99_97KbAsync") == 0) {
|
||||
return FuriHalSubGhzPresetMSK99_97KbAsync;
|
||||
}
|
||||
return FuriHalSubGhzPresetCustom;
|
||||
}
|
||||
|
||||
// -4: missing protocol
|
||||
// -3: missing preset
|
||||
// -2: transmit error
|
||||
// -1: error
|
||||
// 0: ok
|
||||
// 1: resend
|
||||
// 2: exited
|
||||
static int playlist_worker_process(
|
||||
PlaylistWorker* worker,
|
||||
FlipperFormat* fff_file,
|
||||
FlipperFormat* fff_data,
|
||||
const char* path,
|
||||
FuriString* preset,
|
||||
FuriString* protocol) {
|
||||
// actual sending of .sub file
|
||||
|
||||
if(!flipper_format_file_open_existing(fff_file, path)) {
|
||||
FURI_LOG_E(TAG, " (TX) Failed to open %s", path);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// read frequency or default to 433.92MHz
|
||||
uint32_t frequency = 0;
|
||||
if(!flipper_format_read_uint32(fff_file, "Frequency", &frequency, 1)) {
|
||||
FURI_LOG_W(TAG, " (TX) Missing Frequency, defaulting to 433.92MHz");
|
||||
frequency = 433920000;
|
||||
}
|
||||
if(!subghz_devices_is_frequency_valid(worker->radio_device, frequency)) {
|
||||
FURI_LOG_E(
|
||||
TAG, " (TX) The SubGhz device used does not support the frequency %lu", frequency);
|
||||
return -2;
|
||||
}
|
||||
|
||||
// check if preset is present
|
||||
if(!flipper_format_read_string(fff_file, "Preset", preset)) {
|
||||
FURI_LOG_E(TAG, " (TX) Missing Preset");
|
||||
return -3;
|
||||
}
|
||||
|
||||
// check if protocol is present
|
||||
if(!flipper_format_read_string(fff_file, "Protocol", protocol)) {
|
||||
FURI_LOG_E(TAG, " (TX) Missing Protocol");
|
||||
return -4;
|
||||
}
|
||||
|
||||
if(!furi_string_cmp_str(protocol, "RAW")) {
|
||||
subghz_protocol_raw_gen_fff_data(
|
||||
fff_data, path, subghz_devices_get_name(worker->radio_device));
|
||||
} else {
|
||||
stream_copy_full(
|
||||
flipper_format_get_raw_stream(fff_file), flipper_format_get_raw_stream(fff_data));
|
||||
}
|
||||
flipper_format_file_close(fff_file);
|
||||
flipper_format_free(fff_file);
|
||||
|
||||
// (try to) send file
|
||||
SubGhzEnvironment* environment = subghz_environment_alloc();
|
||||
subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry);
|
||||
SubGhzTransmitter* transmitter =
|
||||
subghz_transmitter_alloc_init(environment, furi_string_get_cstr(protocol));
|
||||
|
||||
subghz_transmitter_deserialize(transmitter, fff_data);
|
||||
|
||||
subghz_devices_load_preset(worker->radio_device, str_to_preset(preset), NULL);
|
||||
// there is no check for a custom preset
|
||||
frequency = subghz_devices_set_frequency(worker->radio_device, frequency);
|
||||
|
||||
// Set device to TX and check frequency is alowed to TX
|
||||
if(!subghz_devices_set_tx(worker->radio_device)) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
" (TX) The SubGhz device used does not support the frequency for transmitеing, %lu",
|
||||
frequency);
|
||||
return -5;
|
||||
}
|
||||
FURI_LOG_D(TAG, " (TX) Start sending ...");
|
||||
int status = 0;
|
||||
|
||||
subghz_devices_start_async_tx(worker->radio_device, subghz_transmitter_yield, transmitter);
|
||||
while(!subghz_devices_is_async_complete_tx(worker->radio_device)) {
|
||||
if(worker->ctl_request_exit) {
|
||||
FURI_LOG_D(TAG, " (TX) Requested to exit. Cancelling sending...");
|
||||
status = 2;
|
||||
break;
|
||||
}
|
||||
if(worker->ctl_pause) {
|
||||
FURI_LOG_D(TAG, " (TX) Requested to pause. Cancelling and resending...");
|
||||
status = 1;
|
||||
break;
|
||||
}
|
||||
if(worker->ctl_request_skip) {
|
||||
worker->ctl_request_skip = false;
|
||||
FURI_LOG_D(TAG, " (TX) Requested to skip. Cancelling and resending...");
|
||||
status = 0;
|
||||
break;
|
||||
}
|
||||
if(worker->ctl_request_prev) {
|
||||
worker->ctl_request_prev = false;
|
||||
FURI_LOG_D(TAG, " (TX) Requested to prev. Cancelling and resending...");
|
||||
status = 3;
|
||||
break;
|
||||
}
|
||||
furi_delay_ms(50);
|
||||
}
|
||||
|
||||
FURI_LOG_D(TAG, " (TX) Done sending.");
|
||||
|
||||
subghz_devices_stop_async_tx(worker->radio_device);
|
||||
subghz_devices_idle(worker->radio_device);
|
||||
|
||||
subghz_transmitter_free(transmitter);
|
||||
subghz_environment_free(environment);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// true - the worker can continue
|
||||
// false - the worker should exit
|
||||
static bool playlist_worker_wait_pause(PlaylistWorker* worker) {
|
||||
// wait if paused
|
||||
while(worker->ctl_pause && !worker->ctl_request_exit) {
|
||||
furi_delay_ms(50);
|
||||
}
|
||||
// exit loop if requested to stop
|
||||
if(worker->ctl_request_exit) {
|
||||
FURI_LOG_D(TAG, "Requested to exit. Exiting loop...");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void updatePlayListView(PlaylistWorker* worker, const char* str) {
|
||||
furi_string_reset(worker->meta->prev_3_path);
|
||||
furi_string_set(worker->meta->prev_3_path, furi_string_get_cstr(worker->meta->prev_2_path));
|
||||
|
||||
furi_string_reset(worker->meta->prev_2_path);
|
||||
furi_string_set(worker->meta->prev_2_path, furi_string_get_cstr(worker->meta->prev_1_path));
|
||||
|
||||
furi_string_reset(worker->meta->prev_1_path);
|
||||
furi_string_set(worker->meta->prev_1_path, furi_string_get_cstr(worker->meta->prev_0_path));
|
||||
|
||||
furi_string_reset(worker->meta->prev_0_path);
|
||||
furi_string_set(worker->meta->prev_0_path, str);
|
||||
|
||||
view_port_update(worker->meta->view_port);
|
||||
}
|
||||
|
||||
static bool playlist_worker_play_playlist_once(
|
||||
PlaylistWorker* worker,
|
||||
Storage* storage,
|
||||
FlipperFormat* fff_head,
|
||||
FlipperFormat* fff_data,
|
||||
FuriString* data,
|
||||
FuriString* preset,
|
||||
FuriString* protocol) {
|
||||
//
|
||||
if(!flipper_format_rewind(fff_head)) {
|
||||
FURI_LOG_E(TAG, "Failed to rewind file");
|
||||
return false;
|
||||
}
|
||||
|
||||
while(flipper_format_read_string(fff_head, "sub", data)) {
|
||||
if(!playlist_worker_wait_pause(worker)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// update state to sending
|
||||
meta_set_state(worker->meta, STATE_SENDING);
|
||||
|
||||
++worker->meta->current_count;
|
||||
const char* str = furi_string_get_cstr(data);
|
||||
|
||||
// it's not fancy, but it works for now :)
|
||||
updatePlayListView(worker, str);
|
||||
|
||||
for(int i = 0; i < 1; i++) {
|
||||
if(!playlist_worker_wait_pause(worker)) {
|
||||
break;
|
||||
}
|
||||
|
||||
view_port_update(worker->meta->view_port);
|
||||
|
||||
FURI_LOG_D(TAG, "(worker) Sending %s", str);
|
||||
|
||||
FlipperFormat* fff_file = flipper_format_file_alloc(storage);
|
||||
|
||||
int status =
|
||||
playlist_worker_process(worker, fff_file, fff_data, str, preset, protocol);
|
||||
|
||||
// if there was an error, fff_file is not already freed
|
||||
if(status < 0) {
|
||||
flipper_format_file_close(fff_file);
|
||||
flipper_format_free(fff_file);
|
||||
}
|
||||
|
||||
// re-send file is paused mid-send
|
||||
if(status == 1) {
|
||||
i -= 1;
|
||||
// errored, skip to next file
|
||||
} else if(status < 0) {
|
||||
break;
|
||||
// exited, exit loop
|
||||
} else if(status == 2) {
|
||||
return false;
|
||||
} else if(status == 3) {
|
||||
//aqui rebobinamos y avanzamos de nuevo el fichero n-1 veces
|
||||
//decrementamos el contador de ficheros enviados
|
||||
worker->meta->current_count--;
|
||||
if(worker->meta->current_count > 0) {
|
||||
worker->meta->current_count--;
|
||||
}
|
||||
//rebobinamos el fichero
|
||||
if(!flipper_format_rewind(fff_head)) {
|
||||
FURI_LOG_E(TAG, "Failed to rewind file");
|
||||
return false;
|
||||
}
|
||||
//avanzamos el fichero n-1 veces
|
||||
for(int j = 0; j < worker->meta->current_count; j++) {
|
||||
flipper_format_read_string(fff_head, "sub", data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // end of loop
|
||||
return true;
|
||||
}
|
||||
|
||||
static int32_t playlist_worker_thread(void* ctx) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* fff_head = flipper_format_file_alloc(storage);
|
||||
|
||||
PlaylistWorker* worker = ctx;
|
||||
if(!flipper_format_file_open_existing(fff_head, furi_string_get_cstr(worker->file_path))) {
|
||||
FURI_LOG_E(TAG, "Failed to open %s", furi_string_get_cstr(worker->file_path));
|
||||
worker->is_running = false;
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
flipper_format_free(fff_head);
|
||||
return 0;
|
||||
}
|
||||
|
||||
playlist_worker_wait_pause(worker);
|
||||
FlipperFormat* fff_data = flipper_format_string_alloc();
|
||||
|
||||
FuriString* data;
|
||||
FuriString* preset;
|
||||
FuriString* protocol;
|
||||
data = furi_string_alloc();
|
||||
preset = furi_string_alloc();
|
||||
protocol = furi_string_alloc();
|
||||
|
||||
for(int i = 0; i < MAX(1, worker->meta->playlist_repetitions); i++) {
|
||||
// infinite repetitions if playlist_repetitions is 0
|
||||
if(worker->meta->playlist_repetitions <= 0) {
|
||||
--i;
|
||||
}
|
||||
++worker->meta->current_playlist_repetition;
|
||||
// send playlist
|
||||
worker->meta->current_count = 0;
|
||||
if(worker->ctl_request_exit) {
|
||||
break;
|
||||
}
|
||||
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"Sending playlist (i %d rep %d b %d)",
|
||||
i,
|
||||
worker->meta->current_playlist_repetition,
|
||||
worker->meta->playlist_repetitions);
|
||||
|
||||
if(!playlist_worker_play_playlist_once(
|
||||
worker, storage, fff_head, fff_data, data, preset, protocol)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
flipper_format_free(fff_head);
|
||||
|
||||
furi_string_free(data);
|
||||
furi_string_free(preset);
|
||||
furi_string_free(protocol);
|
||||
|
||||
flipper_format_free(fff_data);
|
||||
|
||||
FURI_LOG_D(TAG, "Done reading. Read %d data lines.", worker->meta->current_count);
|
||||
worker->is_running = false;
|
||||
|
||||
// update state to overview
|
||||
meta_set_state(worker->meta, STATE_OVERVIEW);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void playlist_meta_reset(DisplayMeta* instance) {
|
||||
instance->current_count = 0;
|
||||
instance->current_playlist_repetition = 0;
|
||||
|
||||
furi_string_reset(instance->prev_0_path);
|
||||
furi_string_reset(instance->prev_1_path);
|
||||
furi_string_reset(instance->prev_2_path);
|
||||
furi_string_reset(instance->prev_3_path);
|
||||
}
|
||||
|
||||
DisplayMeta* playlist_meta_alloc() {
|
||||
DisplayMeta* instance = malloc(sizeof(DisplayMeta));
|
||||
instance->prev_0_path = furi_string_alloc();
|
||||
instance->prev_1_path = furi_string_alloc();
|
||||
instance->prev_2_path = furi_string_alloc();
|
||||
instance->prev_3_path = furi_string_alloc();
|
||||
playlist_meta_reset(instance);
|
||||
instance->state = STATE_NONE;
|
||||
instance->playlist_repetitions = 1;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void playlist_meta_free(DisplayMeta* instance) {
|
||||
furi_string_free(instance->prev_0_path);
|
||||
furi_string_free(instance->prev_1_path);
|
||||
furi_string_free(instance->prev_2_path);
|
||||
furi_string_free(instance->prev_3_path);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
PlaylistWorker* playlist_worker_alloc(DisplayMeta* meta) {
|
||||
PlaylistWorker* instance = malloc(sizeof(PlaylistWorker));
|
||||
|
||||
instance->thread = furi_thread_alloc();
|
||||
furi_thread_set_name(instance->thread, "PlaylistWorker");
|
||||
furi_thread_set_stack_size(instance->thread, 2048);
|
||||
furi_thread_set_context(instance->thread, instance);
|
||||
furi_thread_set_callback(instance->thread, playlist_worker_thread);
|
||||
|
||||
instance->meta = meta;
|
||||
instance->ctl_pause = true; // require the user to manually start the worker
|
||||
|
||||
instance->file_path = furi_string_alloc();
|
||||
|
||||
subghz_devices_init();
|
||||
|
||||
instance->radio_device =
|
||||
radio_device_loader_set(instance->radio_device, SubGhzRadioDeviceTypeExternalCC1101);
|
||||
|
||||
subghz_devices_reset(instance->radio_device);
|
||||
subghz_devices_idle(instance->radio_device);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
void playlist_worker_free(PlaylistWorker* instance) {
|
||||
furi_assert(instance);
|
||||
furi_thread_free(instance->thread);
|
||||
furi_string_free(instance->file_path);
|
||||
|
||||
subghz_devices_sleep(instance->radio_device);
|
||||
radio_device_loader_end(instance->radio_device);
|
||||
|
||||
subghz_devices_deinit();
|
||||
|
||||
free(instance);
|
||||
}
|
||||
|
||||
void playlist_worker_stop(PlaylistWorker* worker) {
|
||||
furi_assert(worker);
|
||||
furi_assert(worker->is_running);
|
||||
|
||||
worker->ctl_request_exit = true;
|
||||
furi_thread_join(worker->thread);
|
||||
}
|
||||
|
||||
bool playlist_worker_running(PlaylistWorker* worker) {
|
||||
furi_assert(worker);
|
||||
return worker->is_running;
|
||||
}
|
||||
|
||||
void playlist_worker_start(PlaylistWorker* instance, const char* file_path) {
|
||||
furi_assert(instance);
|
||||
furi_assert(!instance->is_running);
|
||||
|
||||
furi_string_set(instance->file_path, file_path);
|
||||
instance->is_running = true;
|
||||
|
||||
// reset meta (current/total)
|
||||
playlist_meta_reset(instance->meta);
|
||||
|
||||
FURI_LOG_D(TAG, "Starting thread...");
|
||||
furi_thread_start(instance->thread);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void render_callback(Canvas* canvas, void* ctx) {
|
||||
Playlist* app = ctx;
|
||||
furi_check(furi_mutex_acquire(app->mutex, FuriWaitForever) == FuriStatusOk);
|
||||
|
||||
canvas_clear(canvas);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
FuriString* temp_str;
|
||||
temp_str = furi_string_alloc();
|
||||
|
||||
switch(app->meta->state) {
|
||||
case STATE_NONE:
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, WIDTH / 2, HEIGHT / 2, AlignCenter, AlignCenter, "No playlist loaded");
|
||||
break;
|
||||
|
||||
case STATE_OVERVIEW:
|
||||
// draw file name
|
||||
{
|
||||
path_extract_filename(app->file_path, temp_str, true);
|
||||
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
draw_centered_boxed_str(canvas, 1, 1, 15, 6, furi_string_get_cstr(temp_str));
|
||||
}
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
// draw loaded count
|
||||
{
|
||||
furi_string_printf(temp_str, "%d Items in playlist", app->meta->total_count);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 1, 19, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
|
||||
|
||||
if(app->meta->playlist_repetitions <= 0) {
|
||||
furi_string_set(temp_str, "Repeat: inf");
|
||||
} else if(app->meta->playlist_repetitions == 1) {
|
||||
furi_string_set(temp_str, "Repeat: no");
|
||||
} else {
|
||||
furi_string_printf(temp_str, "Repeat: %dx", app->meta->playlist_repetitions);
|
||||
}
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 1, 29, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
|
||||
}
|
||||
|
||||
// draw buttons
|
||||
draw_corner_aligned(canvas, 40, 15, AlignCenter, AlignBottom);
|
||||
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_str_aligned(canvas, WIDTH / 2 - 7, HEIGHT - 11, AlignLeft, AlignTop, "Start");
|
||||
canvas_draw_disc(canvas, WIDTH / 2 - 14, HEIGHT - 8, 3);
|
||||
|
||||
//
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
draw_corner_aligned(canvas, 20, 15, AlignLeft, AlignBottom);
|
||||
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_str_aligned(canvas, 4, HEIGHT - 11, AlignLeft, AlignTop, "R-");
|
||||
|
||||
//
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
draw_corner_aligned(canvas, 20, 15, AlignRight, AlignBottom);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_str_aligned(canvas, WIDTH - 4, HEIGHT - 11, AlignRight, AlignTop, "R+");
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
break;
|
||||
case STATE_SENDING:
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
if(app->worker->ctl_pause) {
|
||||
canvas_draw_icon(canvas, 2, HEIGHT - 8, &I_ButtonRight_4x7);
|
||||
} else {
|
||||
canvas_draw_box(canvas, 2, HEIGHT - 8, 2, 7);
|
||||
canvas_draw_box(canvas, 5, HEIGHT - 8, 2, 7);
|
||||
}
|
||||
|
||||
// draw progress text
|
||||
{
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
furi_string_printf(
|
||||
temp_str, "[%d/%d]", app->meta->current_count, app->meta->total_count);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 11, HEIGHT - 8, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
|
||||
|
||||
int h = canvas_string_width(canvas, furi_string_get_cstr(temp_str));
|
||||
int xs = 11 + h + 2;
|
||||
int w = WIDTH - xs - 1;
|
||||
canvas_draw_box(canvas, xs, HEIGHT - 5, w, 1);
|
||||
|
||||
float progress = (float)app->meta->current_count / (float)app->meta->total_count;
|
||||
int wp = (int)(progress * w);
|
||||
canvas_draw_box(canvas, xs + wp - 1, HEIGHT - 7, 2, 5);
|
||||
}
|
||||
|
||||
{
|
||||
if(app->meta->playlist_repetitions <= 0) {
|
||||
furi_string_printf(temp_str, "[%d/Inf]", app->meta->current_playlist_repetition);
|
||||
} else {
|
||||
furi_string_printf(
|
||||
temp_str,
|
||||
"[%d/%d]",
|
||||
app->meta->current_playlist_repetition,
|
||||
app->meta->playlist_repetitions);
|
||||
}
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
int w = canvas_string_width(canvas, furi_string_get_cstr(temp_str));
|
||||
draw_corner_aligned(canvas, w + 6, 13, AlignRight, AlignTop);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, WIDTH - 3, 3, AlignRight, AlignTop, furi_string_get_cstr(temp_str));
|
||||
}
|
||||
|
||||
// draw last and current file
|
||||
{
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
// current
|
||||
if(!furi_string_empty(app->meta->prev_0_path)) {
|
||||
path_extract_filename(app->meta->prev_0_path, temp_str, true);
|
||||
int w = canvas_string_width(canvas, furi_string_get_cstr(temp_str));
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_rbox(canvas, 1, 1, w + 4, 12, 2);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 3, 3, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
|
||||
}
|
||||
|
||||
// last 3
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
if(!furi_string_empty(app->meta->prev_1_path)) {
|
||||
path_extract_filename(app->meta->prev_1_path, temp_str, true);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 3, 15, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
|
||||
}
|
||||
|
||||
if(!furi_string_empty(app->meta->prev_2_path)) {
|
||||
path_extract_filename(app->meta->prev_2_path, temp_str, true);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 3, 26, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
|
||||
}
|
||||
|
||||
if(!furi_string_empty(app->meta->prev_3_path)) {
|
||||
path_extract_filename(app->meta->prev_3_path, temp_str, true);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 3, 37, AlignLeft, AlignTop, furi_string_get_cstr(temp_str));
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
furi_string_free(temp_str);
|
||||
furi_mutex_release(app->mutex);
|
||||
}
|
||||
|
||||
static void input_callback(InputEvent* event, void* ctx) {
|
||||
Playlist* app = ctx;
|
||||
furi_message_queue_put(app->input_queue, event, 0);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Playlist* playlist_alloc(DisplayMeta* meta) {
|
||||
Playlist* app = malloc(sizeof(Playlist));
|
||||
app->file_path = furi_string_alloc();
|
||||
furi_string_set(app->file_path, PLAYLIST_FOLDER);
|
||||
|
||||
app->meta = meta;
|
||||
app->worker = NULL;
|
||||
|
||||
app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
app->input_queue = furi_message_queue_alloc(32, sizeof(InputEvent));
|
||||
|
||||
// view port
|
||||
app->view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(app->view_port, render_callback, app);
|
||||
view_port_input_callback_set(app->view_port, input_callback, app);
|
||||
|
||||
// gui
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
void playlist_start_worker(Playlist* app, DisplayMeta* meta) {
|
||||
app->worker = playlist_worker_alloc(meta);
|
||||
|
||||
// count playlist items
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
app->meta->total_count =
|
||||
playlist_count_playlist_items(storage, furi_string_get_cstr(app->file_path));
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
// start thread
|
||||
playlist_worker_start(app->worker, furi_string_get_cstr(app->file_path));
|
||||
}
|
||||
|
||||
void playlist_free(Playlist* app) {
|
||||
furi_string_free(app->file_path);
|
||||
|
||||
gui_remove_view_port(app->gui, app->view_port);
|
||||
furi_record_close(RECORD_GUI);
|
||||
view_port_free(app->view_port);
|
||||
|
||||
furi_message_queue_free(app->input_queue);
|
||||
furi_mutex_free(app->mutex);
|
||||
|
||||
playlist_meta_free(app->meta);
|
||||
|
||||
free(app);
|
||||
}
|
||||
|
||||
int32_t playlist_app(char* p) {
|
||||
// create playlist folder
|
||||
{
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
if(!storage_simply_mkdir(storage, PLAYLIST_FOLDER)) {
|
||||
FURI_LOG_E(TAG, "Could not create folder %s", PLAYLIST_FOLDER);
|
||||
}
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
// create app
|
||||
DisplayMeta* meta = playlist_meta_alloc();
|
||||
Playlist* app = playlist_alloc(meta);
|
||||
meta->view_port = app->view_port;
|
||||
|
||||
furi_hal_power_suppress_charge_enter();
|
||||
|
||||
// select playlist file
|
||||
if(p && strlen(p)) {
|
||||
furi_string_set(app->file_path, p);
|
||||
} else {
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
dialog_file_browser_set_basic_options(&browser_options, PLAYLIST_EXT, &I_sub1_10px);
|
||||
browser_options.base_path = PLAYLIST_FOLDER;
|
||||
|
||||
const bool res =
|
||||
dialog_file_browser_show(dialogs, app->file_path, app->file_path, &browser_options);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
// check if a file was selected
|
||||
if(!res) {
|
||||
FURI_LOG_E(TAG, "No file selected");
|
||||
goto exit_cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
playlist_start_worker(app, meta);
|
||||
meta_set_state(app->meta, STATE_OVERVIEW);
|
||||
|
||||
bool exit_loop = false;
|
||||
InputEvent input;
|
||||
while(1) { // close application if no file was selected
|
||||
furi_check(
|
||||
furi_message_queue_get(app->input_queue, &input, FuriWaitForever) == FuriStatusOk);
|
||||
|
||||
switch(input.key) {
|
||||
case InputKeyLeft:
|
||||
if(app->meta->state == STATE_OVERVIEW) {
|
||||
if(input.type == InputTypeShort && app->meta->playlist_repetitions > 0) {
|
||||
--app->meta->playlist_repetitions;
|
||||
}
|
||||
} else if(app->meta->state == STATE_SENDING) {
|
||||
if(input.type == InputTypeShort) {
|
||||
app->worker->ctl_request_prev = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case InputKeyRight:
|
||||
if(app->meta->state == STATE_OVERVIEW) {
|
||||
if(input.type == InputTypeShort) {
|
||||
++app->meta->playlist_repetitions;
|
||||
}
|
||||
} else if(app->meta->state == STATE_SENDING) {
|
||||
if(input.type == InputTypeShort) {
|
||||
app->worker->ctl_request_skip = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case InputKeyOk:
|
||||
if(input.type == InputTypeShort) {
|
||||
// toggle pause state
|
||||
if(!app->worker->is_running) {
|
||||
app->worker->ctl_pause = false;
|
||||
app->worker->ctl_request_exit = false;
|
||||
playlist_worker_start(app->worker, furi_string_get_cstr(app->file_path));
|
||||
} else {
|
||||
app->worker->ctl_pause = !app->worker->ctl_pause;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case InputKeyBack:
|
||||
FURI_LOG_D(TAG, "Pressed Back button. Application will exit");
|
||||
exit_loop = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
furi_mutex_release(app->mutex);
|
||||
|
||||
// exit application
|
||||
if(exit_loop == true) {
|
||||
break;
|
||||
}
|
||||
|
||||
view_port_update(app->view_port);
|
||||
}
|
||||
|
||||
exit_cleanup:
|
||||
|
||||
furi_hal_power_suppress_charge_exit();
|
||||
|
||||
if(app->worker != NULL) {
|
||||
if(playlist_worker_running(app->worker)) {
|
||||
FURI_LOG_D(TAG, "Thread is still running. Requesting thread to finish ...");
|
||||
playlist_worker_stop(app->worker);
|
||||
}
|
||||
FURI_LOG_D(TAG, "Freeing Worker ...");
|
||||
playlist_worker_free(app->worker);
|
||||
}
|
||||
|
||||
FURI_LOG_D(TAG, "Freeing Playlist ...");
|
||||
playlist_free(app);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#include <furi.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <flipper_format/flipper_format_i.h>
|
||||
|
||||
int playlist_count_playlist_items(Storage* storage, const char* file_path) {
|
||||
FlipperFormat* format = flipper_format_file_alloc(storage);
|
||||
if(!flipper_format_file_open_existing(format, file_path)) {
|
||||
return -1;
|
||||
}
|
||||
int count = 0;
|
||||
FuriString* data;
|
||||
data = furi_string_alloc();
|
||||
while(flipper_format_read_string(format, "sub", data)) {
|
||||
++count;
|
||||
}
|
||||
flipper_format_file_close(format);
|
||||
flipper_format_free(format);
|
||||
furi_string_free(data);
|
||||
return count;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
|
||||
#define PLAYLIST_FOLDER EXT_PATH("subghz/playlist")
|
||||
#define PLAYLIST_EXT ".txt"
|
||||
|
||||
int playlist_count_playlist_items(Storage* storage, const char* file_path);
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
@@ -1,19 +0,0 @@
|
||||
App(
|
||||
appid="subghz_remote",
|
||||
name="Sub-GHz Remote",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="subghz_remote_app",
|
||||
requires=[
|
||||
"gui",
|
||||
"dialogs",
|
||||
],
|
||||
stack_size=2 * 1024,
|
||||
targets=["f7"],
|
||||
fap_icon="icon.png",
|
||||
fap_author="@gid9798 and @xMasterX",
|
||||
fap_description="SubGhz Remote, uses up to 5 .sub files",
|
||||
fap_category="Sub-GHz",
|
||||
fap_icon_assets="icons",
|
||||
fap_version="1.3",
|
||||
fap_weburl="https://github.com/DarkFlippers/SubGHz_Remote",
|
||||
)
|
||||
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
typedef enum {
|
||||
SubRemEditMenuStateUP = 0,
|
||||
SubRemEditMenuStateDOWN,
|
||||
SubRemEditMenuStateLEFT,
|
||||
SubRemEditMenuStateRIGHT,
|
||||
SubRemEditMenuStateOK,
|
||||
} SubRemEditMenuState;
|
||||
|
||||
typedef enum {
|
||||
// StartSubmenuIndex
|
||||
SubmenuIndexSubRemOpenMapFile = 0,
|
||||
SubmenuIndexSubRemEditMapFile,
|
||||
SubmenuIndexSubRemNewMapFile,
|
||||
#if FURI_DEBUG
|
||||
SubmenuIndexSubRemRemoteView,
|
||||
#endif
|
||||
// SubmenuIndexSubRemAbout,
|
||||
|
||||
// EditSubmenuIndex
|
||||
EditSubmenuIndexEditLabel,
|
||||
EditSubmenuIndexEditFile,
|
||||
|
||||
// SubRemCustomEvent
|
||||
SubRemCustomEventViewRemoteStartUP = 100,
|
||||
SubRemCustomEventViewRemoteStartDOWN,
|
||||
SubRemCustomEventViewRemoteStartLEFT,
|
||||
SubRemCustomEventViewRemoteStartRIGHT,
|
||||
SubRemCustomEventViewRemoteStartOK,
|
||||
SubRemCustomEventViewRemoteBack,
|
||||
SubRemCustomEventViewRemoteStop,
|
||||
SubRemCustomEventViewRemoteForcedStop,
|
||||
|
||||
SubRemCustomEventViewEditMenuBack,
|
||||
SubRemCustomEventViewEditMenuUP,
|
||||
SubRemCustomEventViewEditMenuDOWN,
|
||||
SubRemCustomEventViewEditMenuEdit,
|
||||
SubRemCustomEventViewEditMenuSave,
|
||||
|
||||
SubRemCustomEventSceneEditsubmenu,
|
||||
SubRemCustomEventSceneEditLabelInputDone,
|
||||
SubRemCustomEventSceneEditLabelWidgetAcces,
|
||||
SubRemCustomEventSceneEditLabelWidgetBack,
|
||||
|
||||
SubRemCustomEventSceneEditOpenSubErrorPopup,
|
||||
|
||||
SubRemCustomEventSceneEditPreviewSaved,
|
||||
|
||||
SubRemCustomEventSceneNewName,
|
||||
|
||||
#ifdef FW_ORIGIN_Official
|
||||
SubRemCustomEventSceneFwWarningExit,
|
||||
SubRemCustomEventSceneFwWarningNext,
|
||||
SubRemCustomEventSceneFwWarningContinue,
|
||||
#endif
|
||||
|
||||
} SubRemCustomEvent;
|
||||
@@ -1,192 +0,0 @@
|
||||
#include "subrem_presets.h"
|
||||
|
||||
#define TAG "SubRemPresets"
|
||||
|
||||
SubRemSubFilePreset* subrem_sub_file_preset_alloc(void) {
|
||||
SubRemSubFilePreset* sub_preset = malloc(sizeof(SubRemSubFilePreset));
|
||||
|
||||
sub_preset->fff_data = flipper_format_string_alloc();
|
||||
sub_preset->file_path = furi_string_alloc();
|
||||
sub_preset->protocaol_name = furi_string_alloc();
|
||||
sub_preset->label = furi_string_alloc();
|
||||
|
||||
sub_preset->freq_preset.name = furi_string_alloc();
|
||||
|
||||
sub_preset->type = SubGhzProtocolTypeUnknown;
|
||||
sub_preset->load_state = SubRemLoadSubStateNotSet;
|
||||
|
||||
return sub_preset;
|
||||
}
|
||||
|
||||
void subrem_sub_file_preset_free(SubRemSubFilePreset* sub_preset) {
|
||||
furi_assert(sub_preset);
|
||||
|
||||
furi_string_free(sub_preset->label);
|
||||
furi_string_free(sub_preset->protocaol_name);
|
||||
furi_string_free(sub_preset->file_path);
|
||||
flipper_format_free(sub_preset->fff_data);
|
||||
|
||||
furi_string_free(sub_preset->freq_preset.name);
|
||||
|
||||
free(sub_preset);
|
||||
}
|
||||
|
||||
void subrem_sub_file_preset_reset(SubRemSubFilePreset* sub_preset) {
|
||||
furi_assert(sub_preset);
|
||||
|
||||
furi_string_set_str(sub_preset->label, "");
|
||||
furi_string_reset(sub_preset->protocaol_name);
|
||||
furi_string_reset(sub_preset->file_path);
|
||||
|
||||
Stream* fff_data_stream = flipper_format_get_raw_stream(sub_preset->fff_data);
|
||||
stream_clean(fff_data_stream);
|
||||
|
||||
sub_preset->type = SubGhzProtocolTypeUnknown;
|
||||
sub_preset->load_state = SubRemLoadSubStateNotSet;
|
||||
}
|
||||
|
||||
SubRemLoadSubState subrem_sub_preset_load(
|
||||
SubRemSubFilePreset* sub_preset,
|
||||
SubGhzTxRx* txrx,
|
||||
FlipperFormat* fff_data_file) {
|
||||
furi_assert(sub_preset);
|
||||
furi_assert(txrx);
|
||||
furi_assert(fff_data_file);
|
||||
|
||||
Stream* fff_data_stream = flipper_format_get_raw_stream(sub_preset->fff_data);
|
||||
|
||||
SubRemLoadSubState ret;
|
||||
FuriString* temp_str = furi_string_alloc();
|
||||
uint32_t temp_data32;
|
||||
uint32_t repeat = 200;
|
||||
|
||||
ret = SubRemLoadSubStateError;
|
||||
|
||||
do {
|
||||
stream_clean(fff_data_stream);
|
||||
if(!flipper_format_read_header(fff_data_file, temp_str, &temp_data32)) {
|
||||
FURI_LOG_E(TAG, "Missing or incorrect header");
|
||||
break;
|
||||
}
|
||||
|
||||
if(((!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_KEY_FILE_TYPE)) ||
|
||||
(!strcmp(furi_string_get_cstr(temp_str), SUBGHZ_RAW_FILE_TYPE))) &&
|
||||
temp_data32 == SUBGHZ_KEY_FILE_VERSION) {
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Type or version mismatch");
|
||||
break;
|
||||
}
|
||||
|
||||
SubGhzSetting* setting = subghz_txrx_get_setting(txrx);
|
||||
|
||||
//Load frequency or using default from settings
|
||||
ret = SubRemLoadSubStateErrorFreq;
|
||||
if(!flipper_format_read_uint32(fff_data_file, "Frequency", &temp_data32, 1)) {
|
||||
FURI_LOG_W(TAG, "Cannot read frequency. Set default frequency");
|
||||
sub_preset->freq_preset.frequency = subghz_setting_get_default_frequency(setting);
|
||||
} else if(!subghz_txrx_radio_device_is_frequency_valid(txrx, temp_data32)) {
|
||||
FURI_LOG_E(TAG, "Frequency not supported on chosen radio module");
|
||||
break;
|
||||
}
|
||||
sub_preset->freq_preset.frequency = temp_data32;
|
||||
|
||||
//Load preset
|
||||
ret = SubRemLoadSubStateErrorMod;
|
||||
if(!flipper_format_read_string(fff_data_file, "Preset", temp_str)) {
|
||||
FURI_LOG_E(TAG, "Missing Preset");
|
||||
break;
|
||||
}
|
||||
|
||||
furi_string_set_str(
|
||||
temp_str, subghz_txrx_get_preset_name(txrx, furi_string_get_cstr(temp_str)));
|
||||
if(!strcmp(furi_string_get_cstr(temp_str), "")) {
|
||||
break;
|
||||
}
|
||||
|
||||
if(!strcmp(furi_string_get_cstr(temp_str), "CUSTOM")) {
|
||||
//TODO FL-3551: add Custom_preset_module
|
||||
//delete preset if it already exists
|
||||
subghz_setting_delete_custom_preset(setting, furi_string_get_cstr(temp_str));
|
||||
//load custom preset from file
|
||||
if(!subghz_setting_load_custom_preset(
|
||||
setting, furi_string_get_cstr(temp_str), fff_data_file)) {
|
||||
FURI_LOG_E(TAG, "Missing Custom preset");
|
||||
break;
|
||||
}
|
||||
// FURI_LOG_E(TAG, "CUSTOM preset is not supported");
|
||||
// break;
|
||||
// TODO Custom preset loading logic if need
|
||||
// sub_preset->freq_preset.preset_index =
|
||||
// subghz_setting_get_inx_preset_by_name(setting, furi_string_get_cstr(temp_str));
|
||||
}
|
||||
|
||||
furi_string_set(sub_preset->freq_preset.name, temp_str);
|
||||
|
||||
// Load protocol
|
||||
ret = SubRemLoadSubStateErrorProtocol;
|
||||
if(!flipper_format_read_string(fff_data_file, "Protocol", temp_str)) {
|
||||
FURI_LOG_E(TAG, "Missing Protocol");
|
||||
break;
|
||||
}
|
||||
|
||||
FlipperFormat* fff_data = sub_preset->fff_data;
|
||||
if(!strcmp(furi_string_get_cstr(temp_str), "RAW")) {
|
||||
//if RAW
|
||||
subghz_protocol_raw_gen_fff_data(
|
||||
fff_data,
|
||||
furi_string_get_cstr(sub_preset->file_path),
|
||||
subghz_txrx_radio_device_get_name(txrx));
|
||||
} else {
|
||||
stream_copy_full(
|
||||
flipper_format_get_raw_stream(fff_data_file),
|
||||
flipper_format_get_raw_stream(fff_data));
|
||||
}
|
||||
|
||||
if(subghz_txrx_load_decoder_by_name_protocol(txrx, furi_string_get_cstr(temp_str))) {
|
||||
SubGhzProtocolStatus status =
|
||||
subghz_protocol_decoder_base_deserialize(subghz_txrx_get_decoder(txrx), fff_data);
|
||||
if(status != SubGhzProtocolStatusOk) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Protocol not found");
|
||||
break;
|
||||
}
|
||||
|
||||
const SubGhzProtocol* protocol = subghz_txrx_get_decoder(txrx)->protocol;
|
||||
|
||||
if(protocol->flag & SubGhzProtocolFlag_Send) {
|
||||
if((protocol->type == SubGhzProtocolTypeStatic) ||
|
||||
(protocol->type == SubGhzProtocolTypeDynamic) ||
|
||||
#ifndef FW_ORIGIN_Official
|
||||
(protocol->type == SubGhzProtocolTypeBinRAW) ||
|
||||
#endif
|
||||
(protocol->type == SubGhzProtocolTypeRAW)) {
|
||||
sub_preset->type = protocol->type;
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Unsuported Protocol");
|
||||
break;
|
||||
}
|
||||
|
||||
furi_string_set(sub_preset->protocaol_name, temp_str);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Protocol does not support transmission");
|
||||
break;
|
||||
}
|
||||
|
||||
if(!flipper_format_insert_or_update_uint32(fff_data, "Repeat", &repeat, 1)) {
|
||||
FURI_LOG_E(TAG, "Unable Repeat");
|
||||
break;
|
||||
}
|
||||
|
||||
ret = SubRemLoadSubStateOK;
|
||||
|
||||
#if FURI_DEBUG
|
||||
FURI_LOG_I(TAG, "%-16s - protocol Loaded", furi_string_get_cstr(sub_preset->label));
|
||||
#endif
|
||||
} while(false);
|
||||
|
||||
furi_string_free(temp_str);
|
||||
sub_preset->load_state = ret;
|
||||
return ret;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "subrem_types.h"
|
||||
#include "txrx/subghz_txrx.h"
|
||||
|
||||
#include <flipper_format/flipper_format_i.h>
|
||||
#include <lib/subghz/types.h>
|
||||
|
||||
typedef struct {
|
||||
FuriString* name;
|
||||
uint32_t frequency;
|
||||
// size_t preset_index; // Need for custom preset
|
||||
} FreqPreset;
|
||||
|
||||
// Sub File preset
|
||||
typedef struct {
|
||||
FlipperFormat* fff_data;
|
||||
FreqPreset freq_preset;
|
||||
FuriString* file_path;
|
||||
FuriString* protocaol_name;
|
||||
FuriString* label;
|
||||
SubGhzProtocolType type;
|
||||
SubRemLoadSubState load_state;
|
||||
} SubRemSubFilePreset;
|
||||
|
||||
typedef struct {
|
||||
SubRemSubFilePreset* subs_preset[SubRemSubKeyNameMaxCount];
|
||||
} SubRemMapPreset;
|
||||
|
||||
SubRemSubFilePreset* subrem_sub_file_preset_alloc(void);
|
||||
|
||||
void subrem_sub_file_preset_free(SubRemSubFilePreset* sub_preset);
|
||||
|
||||
void subrem_sub_file_preset_reset(SubRemSubFilePreset* sub_preset);
|
||||
|
||||
SubRemLoadSubState subrem_sub_preset_load(
|
||||
SubRemSubFilePreset* sub_preset,
|
||||
SubGhzTxRx* txrx,
|
||||
FlipperFormat* fff_data_file);
|
||||
@@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
|
||||
#define SUBREM_APP_APP_FILE_VERSION 1
|
||||
#define SUBREM_APP_APP_FILE_TYPE "Flipper SubRem Map file"
|
||||
#define SUBREM_APP_EXTENSION ".txt"
|
||||
|
||||
typedef enum {
|
||||
SubRemSubKeyNameUp = (0U),
|
||||
SubRemSubKeyNameDown,
|
||||
SubRemSubKeyNameLeft,
|
||||
SubRemSubKeyNameRight,
|
||||
SubRemSubKeyNameOk,
|
||||
SubRemSubKeyNameMaxCount,
|
||||
} SubRemSubKeyName;
|
||||
|
||||
typedef enum {
|
||||
SubRemViewIDSubmenu,
|
||||
SubRemViewIDWidget,
|
||||
SubRemViewIDPopup,
|
||||
SubRemViewIDTextInput,
|
||||
SubRemViewIDRemote,
|
||||
SubRemViewIDEditMenu,
|
||||
} SubRemViewID;
|
||||
|
||||
typedef enum {
|
||||
SubRemLoadSubStateNotSet = 0,
|
||||
SubRemLoadSubStatePreloaded,
|
||||
SubRemLoadSubStateError,
|
||||
SubRemLoadSubStateErrorIncorectPath,
|
||||
SubRemLoadSubStateErrorNoFile,
|
||||
SubRemLoadSubStateErrorFreq,
|
||||
SubRemLoadSubStateErrorMod,
|
||||
SubRemLoadSubStateErrorProtocol,
|
||||
SubRemLoadSubStateOK,
|
||||
} SubRemLoadSubState;
|
||||
|
||||
typedef enum {
|
||||
SubRemLoadMapStateBack = 0,
|
||||
SubRemLoadMapStateError,
|
||||
SubRemLoadMapStateErrorOpenError,
|
||||
SubRemLoadMapStateErrorStorage,
|
||||
SubRemLoadMapStateErrorBrokenFile,
|
||||
SubRemLoadMapStateNotAllOK,
|
||||
SubRemLoadMapStateOK,
|
||||
} SubRemLoadMapState;
|
||||
@@ -1,4 +0,0 @@
|
||||
This is part of the official `SubGhz` app from [flipperzero-firmware](https://github.com/flipperdevices/flipperzero-firmware/tree/3217f286f03da119398586daf94c0723d28b872a/applications/main/subghz)
|
||||
|
||||
With changes from [unleashed-firmware
|
||||
](https://github.com/DarkFlippers/unleashed-firmware/tree/3eac6ccd48a3851cf5d63bf7899b387a293e5319/applications/main/subghz)
|
||||
@@ -1,672 +0,0 @@
|
||||
#include "subghz_txrx_i.h"
|
||||
|
||||
#include <lib/subghz/subghz_protocol_registry.h>
|
||||
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
|
||||
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
|
||||
|
||||
#ifndef FW_ORIGIN_Official
|
||||
#include <lib/subghz/blocks/custom_btn.h>
|
||||
#endif
|
||||
|
||||
#define TAG "SubGhz"
|
||||
|
||||
static void subghz_txrx_radio_device_power_on(SubGhzTxRx* instance) {
|
||||
UNUSED(instance);
|
||||
uint8_t attempts = 0;
|
||||
while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
|
||||
furi_hal_power_enable_otg();
|
||||
//CC1101 power-up time
|
||||
furi_delay_ms(10);
|
||||
}
|
||||
}
|
||||
|
||||
static void subghz_txrx_radio_device_power_off(SubGhzTxRx* instance) {
|
||||
UNUSED(instance);
|
||||
if(furi_hal_power_is_otg_enabled()) furi_hal_power_disable_otg();
|
||||
}
|
||||
|
||||
SubGhzTxRx* subghz_txrx_alloc(void) {
|
||||
SubGhzTxRx* instance = malloc(sizeof(SubGhzTxRx));
|
||||
instance->setting = subghz_setting_alloc();
|
||||
subghz_setting_load(instance->setting, EXT_PATH("subghz/assets/setting_user"));
|
||||
|
||||
instance->preset = malloc(sizeof(SubGhzRadioPreset));
|
||||
instance->preset->name = furi_string_alloc();
|
||||
subghz_txrx_set_preset(
|
||||
instance, "AM650", subghz_setting_get_default_frequency(instance->setting), NULL, 0);
|
||||
|
||||
instance->txrx_state = SubGhzTxRxStateSleep;
|
||||
|
||||
subghz_txrx_hopper_set_state(instance, SubGhzHopperStateOFF);
|
||||
subghz_txrx_speaker_set_state(instance, SubGhzSpeakerStateDisable);
|
||||
subghz_txrx_set_debug_pin_state(instance, false);
|
||||
|
||||
instance->worker = subghz_worker_alloc();
|
||||
instance->fff_data = flipper_format_string_alloc();
|
||||
|
||||
instance->environment = subghz_environment_alloc();
|
||||
instance->is_database_loaded =
|
||||
subghz_environment_load_keystore(instance->environment, SUBGHZ_KEYSTORE_DIR_NAME);
|
||||
subghz_environment_load_keystore(instance->environment, SUBGHZ_KEYSTORE_DIR_USER_NAME);
|
||||
subghz_environment_set_alutech_at_4n_rainbow_table_file_name(
|
||||
instance->environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME);
|
||||
subghz_environment_set_nice_flor_s_rainbow_table_file_name(
|
||||
instance->environment, SUBGHZ_NICE_FLOR_S_DIR_NAME);
|
||||
subghz_environment_set_protocol_registry(
|
||||
instance->environment, (void*)&subghz_protocol_registry);
|
||||
instance->receiver = subghz_receiver_alloc_init(instance->environment);
|
||||
|
||||
subghz_worker_set_overrun_callback(
|
||||
instance->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset);
|
||||
subghz_worker_set_pair_callback(
|
||||
instance->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode);
|
||||
subghz_worker_set_context(instance->worker, instance->receiver);
|
||||
|
||||
//set default device Internal
|
||||
subghz_devices_init();
|
||||
instance->radio_device_type = SubGhzRadioDeviceTypeInternal;
|
||||
instance->radio_device_type =
|
||||
subghz_txrx_radio_device_set(instance, SubGhzRadioDeviceTypeExternalCC1101);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
void subghz_txrx_free(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
if(instance->radio_device_type != SubGhzRadioDeviceTypeInternal) {
|
||||
subghz_txrx_radio_device_power_off(instance);
|
||||
subghz_devices_end(instance->radio_device);
|
||||
}
|
||||
|
||||
subghz_devices_deinit();
|
||||
|
||||
subghz_worker_free(instance->worker);
|
||||
subghz_receiver_free(instance->receiver);
|
||||
subghz_environment_free(instance->environment);
|
||||
flipper_format_free(instance->fff_data);
|
||||
furi_string_free(instance->preset->name);
|
||||
subghz_setting_free(instance->setting);
|
||||
|
||||
free(instance->preset);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
bool subghz_txrx_is_database_loaded(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->is_database_loaded;
|
||||
}
|
||||
|
||||
void subghz_txrx_set_preset(
|
||||
SubGhzTxRx* instance,
|
||||
const char* preset_name,
|
||||
uint32_t frequency,
|
||||
uint8_t* preset_data,
|
||||
size_t preset_data_size) {
|
||||
furi_assert(instance);
|
||||
furi_string_set(instance->preset->name, preset_name);
|
||||
SubGhzRadioPreset* preset = instance->preset;
|
||||
preset->frequency = frequency;
|
||||
preset->data = preset_data;
|
||||
preset->data_size = preset_data_size;
|
||||
}
|
||||
|
||||
const char* subghz_txrx_get_preset_name(SubGhzTxRx* instance, const char* preset) {
|
||||
UNUSED(instance);
|
||||
const char* preset_name = "";
|
||||
if(!strcmp(preset, "FuriHalSubGhzPresetOok270Async")) {
|
||||
preset_name = "AM270";
|
||||
} else if(!strcmp(preset, "FuriHalSubGhzPresetOok650Async")) {
|
||||
preset_name = "AM650";
|
||||
} else if(!strcmp(preset, "FuriHalSubGhzPreset2FSKDev238Async")) {
|
||||
preset_name = "FM238";
|
||||
} else if(!strcmp(preset, "FuriHalSubGhzPreset2FSKDev476Async")) {
|
||||
preset_name = "FM476";
|
||||
} else if(!strcmp(preset, "FuriHalSubGhzPresetCustom")) {
|
||||
preset_name = "CUSTOM";
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Unknown preset");
|
||||
}
|
||||
return preset_name;
|
||||
}
|
||||
|
||||
SubGhzRadioPreset subghz_txrx_get_preset(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return *instance->preset;
|
||||
}
|
||||
|
||||
void subghz_txrx_get_frequency_and_modulation(
|
||||
SubGhzTxRx* instance,
|
||||
FuriString* frequency,
|
||||
FuriString* modulation,
|
||||
bool long_name) {
|
||||
furi_assert(instance);
|
||||
SubGhzRadioPreset* preset = instance->preset;
|
||||
if(frequency != NULL) {
|
||||
furi_string_printf(
|
||||
frequency,
|
||||
"%03ld.%02ld",
|
||||
preset->frequency / 1000000 % 1000,
|
||||
preset->frequency / 10000 % 100);
|
||||
}
|
||||
if(modulation != NULL) {
|
||||
if(long_name) {
|
||||
furi_string_printf(modulation, "%s", furi_string_get_cstr(preset->name));
|
||||
} else {
|
||||
furi_string_printf(modulation, "%.2s", furi_string_get_cstr(preset->name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void subghz_txrx_begin(SubGhzTxRx* instance, uint8_t* preset_data) {
|
||||
furi_assert(instance);
|
||||
subghz_devices_reset(instance->radio_device);
|
||||
subghz_devices_idle(instance->radio_device);
|
||||
subghz_devices_load_preset(instance->radio_device, FuriHalSubGhzPresetCustom, preset_data);
|
||||
instance->txrx_state = SubGhzTxRxStateIDLE;
|
||||
}
|
||||
|
||||
static uint32_t subghz_txrx_rx(SubGhzTxRx* instance, uint32_t frequency) {
|
||||
furi_assert(instance);
|
||||
furi_assert(
|
||||
instance->txrx_state != SubGhzTxRxStateRx && instance->txrx_state != SubGhzTxRxStateSleep);
|
||||
|
||||
subghz_devices_idle(instance->radio_device);
|
||||
|
||||
uint32_t value = subghz_devices_set_frequency(instance->radio_device, frequency);
|
||||
subghz_devices_flush_rx(instance->radio_device);
|
||||
subghz_txrx_speaker_on(instance);
|
||||
|
||||
subghz_devices_start_async_rx(
|
||||
instance->radio_device, subghz_worker_rx_callback, instance->worker);
|
||||
subghz_worker_start(instance->worker);
|
||||
instance->txrx_state = SubGhzTxRxStateRx;
|
||||
return value;
|
||||
}
|
||||
|
||||
static void subghz_txrx_idle(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
furi_assert(instance->txrx_state != SubGhzTxRxStateSleep);
|
||||
subghz_devices_idle(instance->radio_device);
|
||||
subghz_txrx_speaker_off(instance);
|
||||
instance->txrx_state = SubGhzTxRxStateIDLE;
|
||||
}
|
||||
|
||||
static void subghz_txrx_rx_end(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
furi_assert(instance->txrx_state == SubGhzTxRxStateRx);
|
||||
|
||||
if(subghz_worker_is_running(instance->worker)) {
|
||||
subghz_worker_stop(instance->worker);
|
||||
subghz_devices_stop_async_rx(instance->radio_device);
|
||||
}
|
||||
subghz_devices_idle(instance->radio_device);
|
||||
subghz_txrx_speaker_off(instance);
|
||||
instance->txrx_state = SubGhzTxRxStateIDLE;
|
||||
}
|
||||
|
||||
void subghz_txrx_sleep(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
subghz_devices_sleep(instance->radio_device);
|
||||
instance->txrx_state = SubGhzTxRxStateSleep;
|
||||
}
|
||||
|
||||
static bool subghz_txrx_tx(SubGhzTxRx* instance, uint32_t frequency) {
|
||||
furi_assert(instance);
|
||||
furi_assert(instance->txrx_state != SubGhzTxRxStateSleep);
|
||||
|
||||
subghz_devices_idle(instance->radio_device);
|
||||
subghz_devices_set_frequency(instance->radio_device, frequency);
|
||||
|
||||
bool ret = subghz_devices_set_tx(instance->radio_device);
|
||||
if(ret) {
|
||||
subghz_txrx_speaker_on(instance);
|
||||
instance->txrx_state = SubGhzTxRxStateTx;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* flipper_format) {
|
||||
furi_assert(instance);
|
||||
furi_assert(flipper_format);
|
||||
|
||||
subghz_txrx_stop(instance);
|
||||
|
||||
SubGhzTxRxStartTxState ret = SubGhzTxRxStartTxStateErrorParserOthers;
|
||||
FuriString* temp_str = furi_string_alloc();
|
||||
uint32_t repeat = 200;
|
||||
do {
|
||||
if(!flipper_format_rewind(flipper_format)) {
|
||||
FURI_LOG_E(TAG, "Rewind error");
|
||||
break;
|
||||
}
|
||||
if(!flipper_format_read_string(flipper_format, "Protocol", temp_str)) {
|
||||
FURI_LOG_E(TAG, "Missing Protocol");
|
||||
break;
|
||||
}
|
||||
if(!flipper_format_insert_or_update_uint32(flipper_format, "Repeat", &repeat, 1)) {
|
||||
FURI_LOG_E(TAG, "Unable Repeat");
|
||||
break;
|
||||
}
|
||||
ret = SubGhzTxRxStartTxStateOk;
|
||||
|
||||
SubGhzRadioPreset* preset = instance->preset;
|
||||
instance->transmitter =
|
||||
subghz_transmitter_alloc_init(instance->environment, furi_string_get_cstr(temp_str));
|
||||
|
||||
if(instance->transmitter) {
|
||||
if(subghz_transmitter_deserialize(instance->transmitter, flipper_format) ==
|
||||
SubGhzProtocolStatusOk) {
|
||||
if(strcmp(furi_string_get_cstr(preset->name), "") != 0) {
|
||||
subghz_txrx_begin(
|
||||
instance,
|
||||
subghz_setting_get_preset_data_by_name(
|
||||
instance->setting, furi_string_get_cstr(preset->name)));
|
||||
if(preset->frequency) {
|
||||
if(!subghz_txrx_tx(instance, preset->frequency)) {
|
||||
FURI_LOG_E(TAG, "Only Rx");
|
||||
ret = SubGhzTxRxStartTxStateErrorOnlyRx;
|
||||
}
|
||||
} else {
|
||||
ret = SubGhzTxRxStartTxStateErrorParserOthers;
|
||||
}
|
||||
|
||||
} else {
|
||||
FURI_LOG_E(
|
||||
TAG, "Unknown name preset \" %s \"", furi_string_get_cstr(preset->name));
|
||||
ret = SubGhzTxRxStartTxStateErrorParserOthers;
|
||||
}
|
||||
|
||||
if(ret == SubGhzTxRxStartTxStateOk) {
|
||||
//Start TX
|
||||
subghz_devices_start_async_tx(
|
||||
instance->radio_device, subghz_transmitter_yield, instance->transmitter);
|
||||
}
|
||||
} else {
|
||||
ret = SubGhzTxRxStartTxStateErrorParserOthers;
|
||||
}
|
||||
} else {
|
||||
ret = SubGhzTxRxStartTxStateErrorParserOthers;
|
||||
}
|
||||
if(ret != SubGhzTxRxStartTxStateOk) {
|
||||
subghz_transmitter_free(instance->transmitter);
|
||||
if(instance->txrx_state != SubGhzTxRxStateIDLE) {
|
||||
subghz_txrx_idle(instance);
|
||||
}
|
||||
}
|
||||
|
||||
} while(false);
|
||||
furi_string_free(temp_str);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void subghz_txrx_rx_start(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
subghz_txrx_stop(instance);
|
||||
subghz_txrx_begin(
|
||||
instance,
|
||||
subghz_setting_get_preset_data_by_name(
|
||||
subghz_txrx_get_setting(instance), furi_string_get_cstr(instance->preset->name)));
|
||||
subghz_txrx_rx(instance, instance->preset->frequency);
|
||||
}
|
||||
|
||||
void subghz_txrx_set_need_save_callback(
|
||||
SubGhzTxRx* instance,
|
||||
SubGhzTxRxNeedSaveCallback callback,
|
||||
void* context) {
|
||||
furi_assert(instance);
|
||||
instance->need_save_callback = callback;
|
||||
instance->need_save_context = context;
|
||||
}
|
||||
|
||||
static void subghz_txrx_tx_stop(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
furi_assert(instance->txrx_state == SubGhzTxRxStateTx);
|
||||
//Stop TX
|
||||
subghz_devices_stop_async_tx(instance->radio_device);
|
||||
subghz_transmitter_stop(instance->transmitter);
|
||||
subghz_transmitter_free(instance->transmitter);
|
||||
|
||||
//if protocol dynamic then we save the last upload
|
||||
if(instance->decoder_result->protocol->type == SubGhzProtocolTypeDynamic) {
|
||||
if(instance->need_save_callback) {
|
||||
instance->need_save_callback(instance->need_save_context);
|
||||
}
|
||||
}
|
||||
subghz_txrx_idle(instance);
|
||||
subghz_txrx_speaker_off(instance);
|
||||
//Todo: Show message
|
||||
}
|
||||
|
||||
FlipperFormat* subghz_txrx_get_fff_data(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->fff_data;
|
||||
}
|
||||
|
||||
SubGhzSetting* subghz_txrx_get_setting(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->setting;
|
||||
}
|
||||
|
||||
void subghz_txrx_stop(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
switch(instance->txrx_state) {
|
||||
case SubGhzTxRxStateTx:
|
||||
subghz_txrx_tx_stop(instance);
|
||||
subghz_txrx_speaker_unmute(instance);
|
||||
break;
|
||||
case SubGhzTxRxStateRx:
|
||||
subghz_txrx_rx_end(instance);
|
||||
subghz_txrx_speaker_mute(instance);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_txrx_hopper_update(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
|
||||
switch(instance->hopper_state) {
|
||||
case SubGhzHopperStateOFF:
|
||||
case SubGhzHopperStatePause:
|
||||
return;
|
||||
case SubGhzHopperStateRSSITimeOut:
|
||||
if(instance->hopper_timeout != 0) {
|
||||
instance->hopper_timeout--;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
float rssi = -127.0f;
|
||||
if(instance->hopper_state != SubGhzHopperStateRSSITimeOut) {
|
||||
// See RSSI Calculation timings in CC1101 17.3 RSSI
|
||||
rssi = subghz_devices_get_rssi(instance->radio_device);
|
||||
|
||||
// Stay if RSSI is high enough
|
||||
if(rssi > -90.0f) {
|
||||
instance->hopper_timeout = 10;
|
||||
instance->hopper_state = SubGhzHopperStateRSSITimeOut;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
instance->hopper_state = SubGhzHopperStateRunning;
|
||||
}
|
||||
// Select next frequency
|
||||
if(instance->hopper_idx_frequency <
|
||||
subghz_setting_get_hopper_frequency_count(instance->setting) - 1) {
|
||||
instance->hopper_idx_frequency++;
|
||||
} else {
|
||||
instance->hopper_idx_frequency = 0;
|
||||
}
|
||||
|
||||
if(instance->txrx_state == SubGhzTxRxStateRx) {
|
||||
subghz_txrx_rx_end(instance);
|
||||
};
|
||||
if(instance->txrx_state == SubGhzTxRxStateIDLE) {
|
||||
subghz_receiver_reset(instance->receiver);
|
||||
instance->preset->frequency =
|
||||
subghz_setting_get_hopper_frequency(instance->setting, instance->hopper_idx_frequency);
|
||||
subghz_txrx_rx(instance, instance->preset->frequency);
|
||||
}
|
||||
}
|
||||
|
||||
SubGhzHopperState subghz_txrx_hopper_get_state(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->hopper_state;
|
||||
}
|
||||
|
||||
void subghz_txrx_hopper_set_state(SubGhzTxRx* instance, SubGhzHopperState state) {
|
||||
furi_assert(instance);
|
||||
instance->hopper_state = state;
|
||||
}
|
||||
|
||||
void subghz_txrx_hopper_unpause(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
if(instance->hopper_state == SubGhzHopperStatePause) {
|
||||
instance->hopper_state = SubGhzHopperStateRunning;
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
if(instance->hopper_state == SubGhzHopperStateRunning) {
|
||||
instance->hopper_state = SubGhzHopperStatePause;
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_txrx_speaker_on(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
if(instance->debug_pin_state) {
|
||||
subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_ibutton);
|
||||
}
|
||||
|
||||
if(instance->speaker_state == SubGhzSpeakerStateEnable) {
|
||||
if(furi_hal_speaker_acquire(30)) {
|
||||
if(!instance->debug_pin_state) {
|
||||
subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_speaker);
|
||||
}
|
||||
} else {
|
||||
instance->speaker_state = SubGhzSpeakerStateDisable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_txrx_speaker_off(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
if(instance->debug_pin_state) {
|
||||
subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
|
||||
}
|
||||
if(instance->speaker_state != SubGhzSpeakerStateDisable) {
|
||||
if(furi_hal_speaker_is_mine()) {
|
||||
if(!instance->debug_pin_state) {
|
||||
subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
|
||||
}
|
||||
furi_hal_speaker_release();
|
||||
if(instance->speaker_state == SubGhzSpeakerStateShutdown)
|
||||
instance->speaker_state = SubGhzSpeakerStateDisable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_txrx_speaker_mute(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
if(instance->debug_pin_state) {
|
||||
subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
|
||||
}
|
||||
if(instance->speaker_state == SubGhzSpeakerStateEnable) {
|
||||
if(furi_hal_speaker_is_mine()) {
|
||||
if(!instance->debug_pin_state) {
|
||||
subghz_devices_set_async_mirror_pin(instance->radio_device, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_txrx_speaker_unmute(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
if(instance->debug_pin_state) {
|
||||
subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_ibutton);
|
||||
}
|
||||
if(instance->speaker_state == SubGhzSpeakerStateEnable) {
|
||||
if(furi_hal_speaker_is_mine()) {
|
||||
if(!instance->debug_pin_state) {
|
||||
subghz_devices_set_async_mirror_pin(instance->radio_device, &gpio_speaker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_txrx_speaker_set_state(SubGhzTxRx* instance, SubGhzSpeakerState state) {
|
||||
furi_assert(instance);
|
||||
instance->speaker_state = state;
|
||||
}
|
||||
|
||||
SubGhzSpeakerState subghz_txrx_speaker_get_state(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->speaker_state;
|
||||
}
|
||||
|
||||
bool subghz_txrx_load_decoder_by_name_protocol(SubGhzTxRx* instance, const char* name_protocol) {
|
||||
furi_assert(instance);
|
||||
furi_assert(name_protocol);
|
||||
bool res = false;
|
||||
instance->decoder_result =
|
||||
subghz_receiver_search_decoder_base_by_name(instance->receiver, name_protocol);
|
||||
if(instance->decoder_result) {
|
||||
res = true;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
SubGhzProtocolDecoderBase* subghz_txrx_get_decoder(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->decoder_result;
|
||||
}
|
||||
|
||||
bool subghz_txrx_protocol_is_serializable(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return (
|
||||
(instance->decoder_result->protocol->flag & SubGhzProtocolFlag_Save) ==
|
||||
SubGhzProtocolFlag_Save);
|
||||
}
|
||||
|
||||
bool subghz_txrx_protocol_is_transmittable(SubGhzTxRx* instance, bool check_type) {
|
||||
furi_assert(instance);
|
||||
const SubGhzProtocol* protocol = instance->decoder_result->protocol;
|
||||
if(check_type) {
|
||||
return (
|
||||
((protocol->flag & SubGhzProtocolFlag_Send) == SubGhzProtocolFlag_Send) &&
|
||||
protocol->encoder->deserialize && protocol->type == SubGhzProtocolTypeStatic);
|
||||
}
|
||||
return (
|
||||
((protocol->flag & SubGhzProtocolFlag_Send) == SubGhzProtocolFlag_Send) &&
|
||||
protocol->encoder->deserialize);
|
||||
}
|
||||
|
||||
void subghz_txrx_receiver_set_filter(SubGhzTxRx* instance, SubGhzProtocolFlag filter) {
|
||||
furi_assert(instance);
|
||||
subghz_receiver_set_filter(instance->receiver, filter);
|
||||
}
|
||||
|
||||
void subghz_txrx_set_rx_calback(
|
||||
SubGhzTxRx* instance,
|
||||
SubGhzReceiverCallback callback,
|
||||
void* context) {
|
||||
subghz_receiver_set_rx_callback(instance->receiver, callback, context);
|
||||
}
|
||||
|
||||
void subghz_txrx_set_raw_file_encoder_worker_callback_end(
|
||||
SubGhzTxRx* instance,
|
||||
SubGhzProtocolEncoderRAWCallbackEnd callback,
|
||||
void* context) {
|
||||
subghz_protocol_raw_file_encoder_worker_set_callback_end(
|
||||
(SubGhzProtocolEncoderRAW*)subghz_transmitter_get_protocol_instance(instance->transmitter),
|
||||
callback,
|
||||
context);
|
||||
}
|
||||
|
||||
bool subghz_txrx_radio_device_is_external_connected(SubGhzTxRx* instance, const char* name) {
|
||||
furi_assert(instance);
|
||||
|
||||
bool is_connect = false;
|
||||
bool is_otg_enabled = furi_hal_power_is_otg_enabled();
|
||||
|
||||
if(!is_otg_enabled) {
|
||||
subghz_txrx_radio_device_power_on(instance);
|
||||
}
|
||||
|
||||
const SubGhzDevice* device = subghz_devices_get_by_name(name);
|
||||
if(device) {
|
||||
is_connect = subghz_devices_is_connect(device);
|
||||
}
|
||||
|
||||
if(!is_otg_enabled) {
|
||||
subghz_txrx_radio_device_power_off(instance);
|
||||
}
|
||||
return is_connect;
|
||||
}
|
||||
|
||||
SubGhzRadioDeviceType
|
||||
subghz_txrx_radio_device_set(SubGhzTxRx* instance, SubGhzRadioDeviceType radio_device_type) {
|
||||
furi_assert(instance);
|
||||
|
||||
if(radio_device_type == SubGhzRadioDeviceTypeExternalCC1101 &&
|
||||
subghz_txrx_radio_device_is_external_connected(instance, SUBGHZ_DEVICE_CC1101_EXT_NAME)) {
|
||||
subghz_txrx_radio_device_power_on(instance);
|
||||
instance->radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_EXT_NAME);
|
||||
subghz_devices_begin(instance->radio_device);
|
||||
instance->radio_device_type = SubGhzRadioDeviceTypeExternalCC1101;
|
||||
} else {
|
||||
subghz_txrx_radio_device_power_off(instance);
|
||||
if(instance->radio_device_type != SubGhzRadioDeviceTypeInternal) {
|
||||
subghz_devices_end(instance->radio_device);
|
||||
}
|
||||
instance->radio_device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
|
||||
instance->radio_device_type = SubGhzRadioDeviceTypeInternal;
|
||||
}
|
||||
|
||||
return instance->radio_device_type;
|
||||
}
|
||||
|
||||
SubGhzRadioDeviceType subghz_txrx_radio_device_get(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->radio_device_type;
|
||||
}
|
||||
|
||||
float subghz_txrx_radio_device_get_rssi(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return subghz_devices_get_rssi(instance->radio_device);
|
||||
}
|
||||
|
||||
const char* subghz_txrx_radio_device_get_name(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return subghz_devices_get_name(instance->radio_device);
|
||||
}
|
||||
|
||||
bool subghz_txrx_radio_device_is_frequency_valid(SubGhzTxRx* instance, uint32_t frequency) {
|
||||
furi_assert(instance);
|
||||
return subghz_devices_is_frequency_valid(instance->radio_device, frequency);
|
||||
}
|
||||
|
||||
bool subghz_txrx_radio_device_is_tx_allowed(SubGhzTxRx* instance, uint32_t frequency) {
|
||||
furi_assert(instance);
|
||||
furi_assert(instance->txrx_state != SubGhzTxRxStateSleep);
|
||||
|
||||
subghz_devices_idle(instance->radio_device);
|
||||
subghz_devices_set_frequency(instance->radio_device, frequency);
|
||||
|
||||
bool ret = subghz_devices_set_tx(instance->radio_device);
|
||||
subghz_devices_idle(instance->radio_device);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void subghz_txrx_set_debug_pin_state(SubGhzTxRx* instance, bool state) {
|
||||
furi_assert(instance);
|
||||
instance->debug_pin_state = state;
|
||||
}
|
||||
|
||||
bool subghz_txrx_get_debug_pin_state(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->debug_pin_state;
|
||||
}
|
||||
|
||||
#ifndef FW_ORIGIN_Official
|
||||
void subghz_txrx_reset_dynamic_and_custom_btns(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
subghz_environment_reset_keeloq(instance->environment);
|
||||
|
||||
subghz_custom_btns_reset();
|
||||
}
|
||||
#endif
|
||||
|
||||
SubGhzReceiver* subghz_txrx_get_receiver(SubGhzTxRx* instance) {
|
||||
furi_assert(instance);
|
||||
return instance->receiver;
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <lib/subghz/subghz_worker.h>
|
||||
#include <lib/subghz/subghz_setting.h>
|
||||
#include <lib/subghz/receiver.h>
|
||||
#include <lib/subghz/transmitter.h>
|
||||
#include <lib/subghz/protocols/raw.h>
|
||||
#include <lib/subghz/devices/devices.h>
|
||||
|
||||
typedef struct SubGhzTxRx SubGhzTxRx;
|
||||
|
||||
typedef void (*SubGhzTxRxNeedSaveCallback)(void* context);
|
||||
|
||||
typedef enum {
|
||||
SubGhzTxRxStartTxStateOk,
|
||||
SubGhzTxRxStartTxStateErrorOnlyRx,
|
||||
SubGhzTxRxStartTxStateErrorParserOthers,
|
||||
} SubGhzTxRxStartTxState;
|
||||
|
||||
// Type from subghz_types.h need for txrx working
|
||||
/** SubGhzTxRx state */
|
||||
typedef enum {
|
||||
SubGhzTxRxStateIDLE,
|
||||
SubGhzTxRxStateRx,
|
||||
SubGhzTxRxStateTx,
|
||||
SubGhzTxRxStateSleep,
|
||||
} SubGhzTxRxState;
|
||||
|
||||
/** SubGhzHopperState state */
|
||||
typedef enum {
|
||||
SubGhzHopperStateOFF,
|
||||
SubGhzHopperStateRunning,
|
||||
SubGhzHopperStatePause,
|
||||
SubGhzHopperStateRSSITimeOut,
|
||||
} SubGhzHopperState;
|
||||
|
||||
/** SubGhzSpeakerState state */
|
||||
typedef enum {
|
||||
SubGhzSpeakerStateDisable,
|
||||
SubGhzSpeakerStateShutdown,
|
||||
SubGhzSpeakerStateEnable,
|
||||
} SubGhzSpeakerState;
|
||||
|
||||
/** SubGhzRadioDeviceType */
|
||||
typedef enum {
|
||||
SubGhzRadioDeviceTypeAuto,
|
||||
SubGhzRadioDeviceTypeInternal,
|
||||
SubGhzRadioDeviceTypeExternalCC1101,
|
||||
} SubGhzRadioDeviceType;
|
||||
|
||||
/**
|
||||
* Allocate SubGhzTxRx
|
||||
*
|
||||
* @return SubGhzTxRx* pointer to SubGhzTxRx
|
||||
*/
|
||||
SubGhzTxRx* subghz_txrx_alloc(void);
|
||||
|
||||
/**
|
||||
* Free SubGhzTxRx
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_free(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Check if the database is loaded
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return bool True if the database is loaded
|
||||
*/
|
||||
bool subghz_txrx_is_database_loaded(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Set preset
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param preset_name Name of preset
|
||||
* @param frequency Frequency in Hz
|
||||
* @param preset_data Data of preset
|
||||
* @param preset_data_size Size of preset data
|
||||
*/
|
||||
void subghz_txrx_set_preset(
|
||||
SubGhzTxRx* instance,
|
||||
const char* preset_name,
|
||||
uint32_t frequency,
|
||||
uint8_t* preset_data,
|
||||
size_t preset_data_size);
|
||||
|
||||
/**
|
||||
* Get name of preset
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param preset String of preset
|
||||
* @return const char* Name of preset
|
||||
*/
|
||||
const char* subghz_txrx_get_preset_name(SubGhzTxRx* instance, const char* preset);
|
||||
|
||||
/**
|
||||
* Get of preset
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return SubGhzRadioPreset Preset
|
||||
*/
|
||||
SubGhzRadioPreset subghz_txrx_get_preset(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Get string frequency and modulation
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param frequency Pointer to a string frequency
|
||||
* @param modulation Pointer to a string modulation
|
||||
*/
|
||||
void subghz_txrx_get_frequency_and_modulation(
|
||||
SubGhzTxRx* instance,
|
||||
FuriString* frequency,
|
||||
FuriString* modulation,
|
||||
bool long_name);
|
||||
|
||||
/**
|
||||
* Start TX CC1101
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param flipper_format Pointer to a FlipperFormat
|
||||
* @return SubGhzTxRxStartTxState
|
||||
*/
|
||||
SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* flipper_format);
|
||||
|
||||
/**
|
||||
* Start RX CC1101
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_rx_start(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Stop TX/RX CC1101
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_stop(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Set sleep mode CC1101
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_sleep(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Update frequency CC1101 in automatic mode (hopper)
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_hopper_update(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Get state hopper
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return SubGhzHopperState
|
||||
*/
|
||||
SubGhzHopperState subghz_txrx_hopper_get_state(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Set state hopper
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param state State hopper
|
||||
*/
|
||||
void subghz_txrx_hopper_set_state(SubGhzTxRx* instance, SubGhzHopperState state);
|
||||
|
||||
/**
|
||||
* Unpause hopper
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_hopper_unpause(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Set pause hopper
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_hopper_pause(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Speaker on
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_speaker_on(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Speaker off
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_speaker_off(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Speaker mute
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_speaker_mute(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Speaker unmute
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
*/
|
||||
void subghz_txrx_speaker_unmute(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Set state speaker
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param state State speaker
|
||||
*/
|
||||
void subghz_txrx_speaker_set_state(SubGhzTxRx* instance, SubGhzSpeakerState state);
|
||||
|
||||
/**
|
||||
* Get state speaker
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return SubGhzSpeakerState
|
||||
*/
|
||||
SubGhzSpeakerState subghz_txrx_speaker_get_state(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* load decoder by name protocol
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param name_protocol Name protocol
|
||||
* @return bool True if the decoder is loaded
|
||||
*/
|
||||
bool subghz_txrx_load_decoder_by_name_protocol(SubGhzTxRx* instance, const char* name_protocol);
|
||||
|
||||
/**
|
||||
* Get decoder
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return SubGhzProtocolDecoderBase* Pointer to a SubGhzProtocolDecoderBase
|
||||
*/
|
||||
SubGhzProtocolDecoderBase* subghz_txrx_get_decoder(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Set callback for save data
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param callback Callback for save data
|
||||
* @param context Context for callback
|
||||
*/
|
||||
void subghz_txrx_set_need_save_callback(
|
||||
SubGhzTxRx* instance,
|
||||
SubGhzTxRxNeedSaveCallback callback,
|
||||
void* context);
|
||||
|
||||
/**
|
||||
* Get pointer to a load data key
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return FlipperFormat*
|
||||
*/
|
||||
FlipperFormat* subghz_txrx_get_fff_data(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Get pointer to a SugGhzSetting
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return SubGhzSetting*
|
||||
*/
|
||||
SubGhzSetting* subghz_txrx_get_setting(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Is it possible to save this protocol
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return bool True if it is possible to save this protocol
|
||||
*/
|
||||
bool subghz_txrx_protocol_is_serializable(SubGhzTxRx* instance);
|
||||
|
||||
/**
|
||||
* Is it possible to send this protocol
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return bool True if it is possible to send this protocol
|
||||
*/
|
||||
bool subghz_txrx_protocol_is_transmittable(SubGhzTxRx* instance, bool check_type);
|
||||
|
||||
/**
|
||||
* Set filter, what types of decoder to use
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param filter Filter
|
||||
*/
|
||||
void subghz_txrx_receiver_set_filter(SubGhzTxRx* instance, SubGhzProtocolFlag filter);
|
||||
|
||||
/**
|
||||
* Set callback for receive data
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param callback Callback for receive data
|
||||
* @param context Context for callback
|
||||
*/
|
||||
void subghz_txrx_set_rx_calback(
|
||||
SubGhzTxRx* instance,
|
||||
SubGhzReceiverCallback callback,
|
||||
void* context);
|
||||
|
||||
/**
|
||||
* Set callback for Raw decoder, end of data transfer
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param callback Callback for Raw decoder, end of data transfer
|
||||
* @param context Context for callback
|
||||
*/
|
||||
void subghz_txrx_set_raw_file_encoder_worker_callback_end(
|
||||
SubGhzTxRx* instance,
|
||||
SubGhzProtocolEncoderRAWCallbackEnd callback,
|
||||
void* context);
|
||||
|
||||
/* Checking if an external radio device is connected
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param name Name of external radio device
|
||||
* @return bool True if is connected to the external radio device
|
||||
*/
|
||||
bool subghz_txrx_radio_device_is_external_connected(SubGhzTxRx* instance, const char* name);
|
||||
|
||||
/* Set the selected radio device to use
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @param radio_device_type Radio device type
|
||||
* @return SubGhzRadioDeviceType Type of installed radio device
|
||||
*/
|
||||
SubGhzRadioDeviceType
|
||||
subghz_txrx_radio_device_set(SubGhzTxRx* instance, SubGhzRadioDeviceType radio_device_type);
|
||||
|
||||
/* Get the selected radio device to use
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return SubGhzRadioDeviceType Type of installed radio device
|
||||
*/
|
||||
SubGhzRadioDeviceType subghz_txrx_radio_device_get(SubGhzTxRx* instance);
|
||||
|
||||
/* Get RSSI the selected radio device to use
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return float RSSI
|
||||
*/
|
||||
float subghz_txrx_radio_device_get_rssi(SubGhzTxRx* instance);
|
||||
|
||||
/* Get name the selected radio device to use
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return const char* Name of installed radio device
|
||||
*/
|
||||
const char* subghz_txrx_radio_device_get_name(SubGhzTxRx* instance);
|
||||
|
||||
/* Get get intelligence whether frequency the selected radio device to use
|
||||
*
|
||||
* @param instance Pointer to a SubGhzTxRx
|
||||
* @return bool True if the frequency is valid
|
||||
*/
|
||||
bool subghz_txrx_radio_device_is_frequency_valid(SubGhzTxRx* instance, uint32_t frequency);
|
||||
|
||||
bool subghz_txrx_radio_device_is_tx_allowed(SubGhzTxRx* instance, uint32_t frequency);
|
||||
|
||||
void subghz_txrx_set_debug_pin_state(SubGhzTxRx* instance, bool state);
|
||||
bool subghz_txrx_get_debug_pin_state(SubGhzTxRx* instance);
|
||||
#ifndef FW_ORIGIN_Official
|
||||
void subghz_txrx_reset_dynamic_and_custom_btns(SubGhzTxRx* instance);
|
||||
#endif
|
||||
SubGhzReceiver* subghz_txrx_get_receiver(SubGhzTxRx* instance); // TODO use only in DecodeRaw
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "subghz_txrx.h"
|
||||
|
||||
struct SubGhzTxRx {
|
||||
SubGhzWorker* worker;
|
||||
|
||||
SubGhzEnvironment* environment;
|
||||
SubGhzReceiver* receiver;
|
||||
SubGhzTransmitter* transmitter;
|
||||
SubGhzProtocolDecoderBase* decoder_result;
|
||||
FlipperFormat* fff_data;
|
||||
|
||||
SubGhzRadioPreset* preset;
|
||||
SubGhzSetting* setting;
|
||||
|
||||
uint8_t hopper_timeout;
|
||||
uint8_t hopper_idx_frequency;
|
||||
bool is_database_loaded;
|
||||
SubGhzHopperState hopper_state;
|
||||
|
||||
SubGhzTxRxState txrx_state;
|
||||
SubGhzSpeakerState speaker_state;
|
||||
const SubGhzDevice* radio_device;
|
||||
SubGhzRadioDeviceType radio_device_type;
|
||||
|
||||
SubGhzTxRxNeedSaveCallback need_save_callback;
|
||||
void* need_save_context;
|
||||
|
||||
bool debug_pin_state;
|
||||
};
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 191 B |
|
Before Width: | Height: | Size: 178 B |
|
Before Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 176 B |
|
Before Width: | Height: | Size: 215 B |
|
Before Width: | Height: | Size: 206 B |
|
Before Width: | Height: | Size: 192 B |
|
Before Width: | Height: | Size: 178 B |
|
Before Width: | Height: | Size: 186 B |
|
Before Width: | Height: | Size: 170 B |
|
Before Width: | Height: | Size: 990 B |
|
Before Width: | Height: | Size: 994 B |
|
Before Width: | Height: | Size: 125 B |
|
Before Width: | Height: | Size: 214 B |
@@ -1,30 +0,0 @@
|
||||
#include "../subghz_remote_app_i.h"
|
||||
|
||||
// Generate scene on_enter handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
|
||||
void (*const subrem_scene_on_enter_handlers[])(void*) = {
|
||||
#include "subrem_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_event handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
|
||||
bool (*const subrem_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
|
||||
#include "subrem_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_exit handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
|
||||
void (*const subrem_scene_on_exit_handlers[])(void* context) = {
|
||||
#include "subrem_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Initialize scene handlers configuration structure
|
||||
const SceneManagerHandlers subrem_scene_handlers = {
|
||||
.on_enter_handlers = subrem_scene_on_enter_handlers,
|
||||
.on_event_handlers = subrem_scene_on_event_handlers,
|
||||
.on_exit_handlers = subrem_scene_on_exit_handlers,
|
||||
.scene_num = SubRemSceneNum,
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/scene_manager.h>
|
||||
|
||||
// Generate scene id and total number
|
||||
#define ADD_SCENE(prefix, name, id) SubRemScene##id,
|
||||
typedef enum {
|
||||
#include "subrem_scene_config.h"
|
||||
SubRemSceneNum,
|
||||
} SubRemScene;
|
||||
#undef ADD_SCENE
|
||||
|
||||
extern const SceneManagerHandlers subrem_scene_handlers;
|
||||
|
||||
// Generate scene on_enter handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
|
||||
#include "subrem_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_event handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) \
|
||||
bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
|
||||
#include "subrem_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_exit handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
|
||||
#include "subrem_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
@@ -1,12 +0,0 @@
|
||||
ADD_SCENE(subrem, start, Start)
|
||||
ADD_SCENE(subrem, open_map_file, OpenMapFile)
|
||||
ADD_SCENE(subrem, remote, Remote)
|
||||
ADD_SCENE(subrem, edit_menu, EditMenu)
|
||||
ADD_SCENE(subrem, edit_submenu, EditSubMenu)
|
||||
ADD_SCENE(subrem, edit_label, EditLabel)
|
||||
ADD_SCENE(subrem, open_sub_file, OpenSubFile)
|
||||
ADD_SCENE(subrem, edit_preview, EditPreview)
|
||||
ADD_SCENE(subrem, enter_new_name, EnterNewName)
|
||||
#ifdef FW_ORIGIN_Official
|
||||
ADD_SCENE(subrem, fw_warning, FwWarning)
|
||||
#endif
|
||||
@@ -1,133 +0,0 @@
|
||||
#include "../subghz_remote_app_i.h"
|
||||
|
||||
#include <lib/toolbox/path.h>
|
||||
|
||||
typedef enum {
|
||||
SubRemSceneEditLabelStateTextInput,
|
||||
SubRemSceneEditLabelStateWidget,
|
||||
} SubRemSceneEditLabelState;
|
||||
|
||||
void subrem_scene_edit_label_text_input_callback(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzRemoteApp* app = context;
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, SubRemCustomEventSceneEditLabelInputDone);
|
||||
}
|
||||
|
||||
void subrem_scene_edit_label_widget_callback(GuiButtonType result, InputType type, void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzRemoteApp* app = context;
|
||||
if((result == GuiButtonTypeCenter) && (type == InputTypeShort)) {
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, SubRemCustomEventSceneEditLabelWidgetAcces);
|
||||
} else if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) {
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, SubRemCustomEventSceneEditLabelWidgetBack);
|
||||
}
|
||||
}
|
||||
|
||||
void subrem_scene_edit_label_on_enter(void* context) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
|
||||
SubRemSubFilePreset* sub_preset = app->map_preset->subs_preset[app->chosen_sub];
|
||||
|
||||
FuriString* temp_str = furi_string_alloc();
|
||||
|
||||
if(furi_string_empty(sub_preset->label)) {
|
||||
if(furi_string_empty(sub_preset->file_path)) {
|
||||
path_extract_filename(sub_preset->file_path, temp_str, true);
|
||||
strcpy(app->file_name_tmp, furi_string_get_cstr(temp_str));
|
||||
} else {
|
||||
strcpy(app->file_name_tmp, "");
|
||||
}
|
||||
} else {
|
||||
strcpy(app->file_name_tmp, furi_string_get_cstr(sub_preset->label));
|
||||
}
|
||||
|
||||
TextInput* text_input = app->text_input;
|
||||
text_input_set_header_text(text_input, "Label name");
|
||||
text_input_set_result_callback(
|
||||
text_input,
|
||||
subrem_scene_edit_label_text_input_callback,
|
||||
app,
|
||||
app->file_name_tmp,
|
||||
25,
|
||||
false);
|
||||
#ifndef FW_ORIGIN_Official
|
||||
text_input_set_minimum_length(app->text_input, 0);
|
||||
#endif
|
||||
widget_add_string_element(
|
||||
app->widget, 63, 12, AlignCenter, AlignCenter, FontPrimary, "Empty Label Name");
|
||||
widget_add_string_element(
|
||||
app->widget, 63, 32, AlignCenter, AlignCenter, FontSecondary, "Continue?");
|
||||
|
||||
widget_add_button_element(
|
||||
app->widget, GuiButtonTypeCenter, "Ok", subrem_scene_edit_label_widget_callback, app);
|
||||
widget_add_button_element(
|
||||
app->widget, GuiButtonTypeLeft, "Back", subrem_scene_edit_label_widget_callback, app);
|
||||
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateTextInput);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput);
|
||||
|
||||
furi_string_free(temp_str);
|
||||
}
|
||||
|
||||
bool subrem_scene_edit_label_on_event(void* context, SceneManagerEvent event) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
|
||||
FuriString* label = app->map_preset->subs_preset[app->chosen_sub]->label;
|
||||
|
||||
if(event.type == SceneManagerEventTypeBack) {
|
||||
if(scene_manager_get_scene_state(app->scene_manager, SubRemSceneEditLabel) ==
|
||||
SubRemSceneEditLabelStateWidget) {
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateTextInput);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput);
|
||||
return true;
|
||||
} else if(
|
||||
scene_manager_get_scene_state(app->scene_manager, SubRemSceneEditLabel) ==
|
||||
SubRemSceneEditLabelStateTextInput) {
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
return true;
|
||||
}
|
||||
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
return true;
|
||||
} else if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == SubRemCustomEventSceneEditLabelInputDone) {
|
||||
if(strcmp(app->file_name_tmp, "") == 0) {
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateWidget);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget);
|
||||
|
||||
} else {
|
||||
furi_string_set(label, app->file_name_tmp);
|
||||
app->map_not_saved = true;
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
}
|
||||
return true;
|
||||
} else if(event.event == SubRemCustomEventSceneEditLabelWidgetAcces) {
|
||||
furi_string_set(label, app->file_name_tmp);
|
||||
app->map_not_saved = true;
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
|
||||
return true;
|
||||
} else if(event.event == SubRemCustomEventSceneEditLabelWidgetBack) {
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, SubRemSceneEditLabel, SubRemSceneEditLabelStateTextInput);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDTextInput);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void subrem_scene_edit_label_on_exit(void* context) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
|
||||
// Clear view
|
||||
text_input_reset(app->text_input);
|
||||
widget_reset(app->widget);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
#include "../subghz_remote_app_i.h"
|
||||
|
||||
void subrem_scene_edit_menu_callback(SubRemCustomEvent event, void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzRemoteApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, event);
|
||||
}
|
||||
|
||||
void subrem_scene_edit_menu_widget_callback(GuiButtonType result, InputType type, void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzRemoteApp* app = context;
|
||||
if((result == GuiButtonTypeRight) && (type == InputTypeShort)) {
|
||||
app->map_not_saved = false;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventViewEditMenuBack);
|
||||
} else if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) {
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDEditMenu);
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t subrem_scene_edit_menu_state_to_index(SubRemEditMenuState event_id) {
|
||||
uint8_t ret = 0;
|
||||
|
||||
if(event_id == SubRemEditMenuStateUP) {
|
||||
ret = SubRemSubKeyNameUp;
|
||||
} else if(event_id == SubRemEditMenuStateDOWN) {
|
||||
ret = SubRemSubKeyNameDown;
|
||||
} else if(event_id == SubRemEditMenuStateLEFT) {
|
||||
ret = SubRemSubKeyNameLeft;
|
||||
} else if(event_id == SubRemEditMenuStateRIGHT) {
|
||||
ret = SubRemSubKeyNameRight;
|
||||
} else if(event_id == SubRemEditMenuStateOK) {
|
||||
ret = SubRemSubKeyNameOk;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void subrem_scene_edit_menu_update_data(SubGhzRemoteApp* app) {
|
||||
furi_assert(app);
|
||||
uint8_t index = subrem_scene_edit_menu_state_to_index(
|
||||
scene_manager_get_scene_state(app->scene_manager, SubRemSceneEditMenu));
|
||||
|
||||
subrem_view_edit_menu_add_data_to_show(
|
||||
app->subrem_edit_menu,
|
||||
index,
|
||||
app->map_preset->subs_preset[index]->label,
|
||||
app->map_preset->subs_preset[index]->file_path,
|
||||
app->map_preset->subs_preset[index]->load_state);
|
||||
}
|
||||
|
||||
void subrem_scene_edit_menu_on_enter(void* context) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
|
||||
subrem_view_edit_menu_set_callback(
|
||||
app->subrem_edit_menu, subrem_scene_edit_menu_callback, app);
|
||||
|
||||
subrem_scene_edit_menu_update_data(app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDEditMenu);
|
||||
|
||||
Widget* widget = app->widget;
|
||||
|
||||
widget_add_string_element(
|
||||
widget, 63, 12, AlignCenter, AlignBottom, FontPrimary, "Changes are not saved");
|
||||
widget_add_string_element(
|
||||
widget, 63, 32, AlignCenter, AlignBottom, FontPrimary, "do you want to exit?");
|
||||
|
||||
widget_add_button_element(
|
||||
widget, GuiButtonTypeRight, "Yes", subrem_scene_edit_menu_widget_callback, app);
|
||||
widget_add_button_element(
|
||||
widget, GuiButtonTypeLeft, "No", subrem_scene_edit_menu_widget_callback, app);
|
||||
}
|
||||
|
||||
bool subrem_scene_edit_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeBack) {
|
||||
// Catch widget backEvent
|
||||
return true;
|
||||
}
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == SubRemCustomEventViewEditMenuBack) {
|
||||
if(app->map_not_saved) {
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDWidget);
|
||||
} else if(!scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, SubRemSceneStart)) {
|
||||
scene_manager_stop(app->scene_manager);
|
||||
view_dispatcher_stop(app->view_dispatcher);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if(
|
||||
event.event == SubRemCustomEventViewEditMenuUP ||
|
||||
event.event == SubRemCustomEventViewEditMenuDOWN) {
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager,
|
||||
SubRemSceneEditMenu,
|
||||
subrem_view_edit_menu_get_index(app->subrem_edit_menu));
|
||||
subrem_scene_edit_menu_update_data(app);
|
||||
|
||||
return true;
|
||||
} else if(event.event == SubRemCustomEventViewEditMenuEdit) {
|
||||
app->chosen_sub = subrem_view_edit_menu_get_index(app->subrem_edit_menu);
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, SubRemSceneEditSubMenu, EditSubmenuIndexEditLabel);
|
||||
scene_manager_next_scene(app->scene_manager, SubRemSceneEditSubMenu);
|
||||
|
||||
return true;
|
||||
} else if(event.event == SubRemCustomEventViewEditMenuSave) {
|
||||
scene_manager_next_scene(app->scene_manager, SubRemSceneEditPreview);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void subrem_scene_edit_menu_on_exit(void* context) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
widget_reset(app->widget);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
#include "../subghz_remote_app_i.h"
|
||||
#include "../views/remote.h"
|
||||
|
||||
#define TAG "SubRemScenRemote"
|
||||
|
||||
void subghz_scene_edit_preview_save_popup_callback(void* context) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
view_dispatcher_send_custom_event(
|
||||
app->view_dispatcher, SubRemCustomEventSceneEditPreviewSaved);
|
||||
}
|
||||
|
||||
void subrem_scene_edit_preview_callback(SubRemCustomEvent event, void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzRemoteApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, event);
|
||||
}
|
||||
|
||||
void subrem_scene_edit_preview_on_enter(void* context) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
|
||||
// Setup view
|
||||
Popup* popup = app->popup;
|
||||
popup_set_icon(popup, 36, 5, &I_DolphinDone_80x58);
|
||||
popup_set_header(popup, "Saved!", 13, 22, AlignLeft, AlignBottom);
|
||||
popup_set_timeout(popup, 1500);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_callback(popup, subghz_scene_edit_preview_save_popup_callback);
|
||||
popup_enable_timeout(popup);
|
||||
|
||||
subrem_view_remote_update_data_labels(app->subrem_remote_view, app->map_preset->subs_preset);
|
||||
subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateOFF, 0);
|
||||
|
||||
subrem_view_remote_set_callback(
|
||||
app->subrem_remote_view, subrem_scene_edit_preview_callback, app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDRemote);
|
||||
}
|
||||
|
||||
bool subrem_scene_edit_preview_on_event(void* context, SceneManagerEvent event) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeBack ||
|
||||
(event.type == SceneManagerEventTypeCustom &&
|
||||
(event.event == SubRemCustomEventViewRemoteStartLEFT ||
|
||||
event.event == SubRemCustomEventViewRemoteForcedStop))) {
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
return true;
|
||||
} else if(
|
||||
event.type == SceneManagerEventTypeCustom &&
|
||||
(event.event == SubRemCustomEventViewRemoteStartRIGHT ||
|
||||
event.event == SubRemCustomEventViewRemoteStartOK)) {
|
||||
if(subrem_save_map_to_file(app)) {
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDPopup);
|
||||
app->map_not_saved = false;
|
||||
return true;
|
||||
}
|
||||
// TODO error screen
|
||||
return true;
|
||||
} else if(
|
||||
event.type == SceneManagerEventTypeCustom &&
|
||||
event.event == SubRemCustomEventSceneEditPreviewSaved) {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, SubRemSceneEditMenu);
|
||||
}
|
||||
// } else if(event.type == SceneManagerEventTypeTick) {
|
||||
// }
|
||||
return false;
|
||||
}
|
||||
|
||||
void subrem_scene_edit_preview_on_exit(void* context) {
|
||||
SubGhzRemoteApp* app = context;
|
||||
|
||||
subrem_view_remote_set_state(app->subrem_remote_view, SubRemViewRemoteStateIdle, 0);
|
||||
popup_reset(app->popup);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#include "../subghz_remote_app_i.h"
|
||||
#include "../helpers/subrem_custom_event.h"
|
||||
|
||||
void subrem_scene_edit_submenu_text_input_callback(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzRemoteApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, SubRemCustomEventSceneEditsubmenu);
|
||||
}
|
||||
|
||||
void subrem_scene_edit_submenu_callback(void* context, uint32_t index) {
|
||||
furi_assert(context);
|
||||
SubGhzRemoteApp* app = context;
|
||||
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void subrem_scene_edit_submenu_on_enter(void* context) {
|
||||
furi_assert(context);
|
||||
|
||||
SubGhzRemoteApp* app = context;
|
||||
Submenu* submenu = app->submenu;
|
||||
submenu_add_item(
|
||||
submenu, "Edit Label", EditSubmenuIndexEditLabel, subrem_scene_edit_submenu_callback, app);
|
||||
submenu_add_item(
|
||||
submenu, "Edit File", EditSubmenuIndexEditFile, subrem_scene_edit_submenu_callback, app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, SubRemViewIDSubmenu);
|
||||
}
|
||||
|
||||
bool subrem_scene_edit_submenu_on_event(void* context, SceneManagerEvent event) {
|
||||
furi_assert(context);
|
||||
|
||||
SubGhzRemoteApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == EditSubmenuIndexEditLabel) {
|
||||
scene_manager_next_scene(app->scene_manager, SubRemSceneEditLabel);
|
||||
consumed = true;
|
||||
} else if(event.event == EditSubmenuIndexEditFile) {
|
||||
scene_manager_next_scene(app->scene_manager, SubRemSceneOpenSubFile);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void subrem_scene_edit_submenu_on_exit(void* context) {
|
||||
furi_assert(context);
|
||||
|
||||
SubGhzRemoteApp* app = context;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||