diff --git a/ReadMe.md b/ReadMe.md index 30498db81..86cd2304d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -109,6 +109,7 @@ Also check changelog in releases for latest updates! - BH1750 - Lightmeter [(by oleksiikutuzov)](https://github.com/oleksiikutuzov/flipperzero-lightmeter) - iButton Fuzzer [(by xMasterX)](https://github.com/xMasterX/ibutton-fuzzer) - HEX Viewer [(by QtRoS)](https://github.com/QtRoS/flipper-zero-hex-viewer) +- POCSAG Pager [(by xMasterX & Shmuma)](https://github.com/xMasterX/flipper-pager) Games: - DOOM (fixed) [(by p4nic4ttack)](https://github.com/p4nic4ttack/doom-flipper-zero/) diff --git a/applications/plugins/pocsag_pager/application.fam b/applications/plugins/pocsag_pager/application.fam new file mode 100644 index 000000000..aafb6a5a3 --- /dev/null +++ b/applications/plugins/pocsag_pager/application.fam @@ -0,0 +1,13 @@ +App( + appid="pocsag_pager", + name="POCSAG Pager", + apptype=FlipperAppType.PLUGIN, + entry_point="pocsag_pager_app", + cdefines=["APP_POCSAG_PAGER"], + requires=["gui"], + stack_size=4 * 1024, + order=50, + fap_icon="pocsag_pager_10px.png", + fap_category="Tools", + fap_icon_assets="images", +) diff --git a/applications/plugins/pocsag_pager/helpers/pocsag_pager_event.h b/applications/plugins/pocsag_pager/helpers/pocsag_pager_event.h new file mode 100644 index 000000000..8bcf64a30 --- /dev/null +++ b/applications/plugins/pocsag_pager/helpers/pocsag_pager_event.h @@ -0,0 +1,14 @@ +#pragma once + +typedef enum { + //PCSGCustomEvent + PCSGCustomEventStartId = 100, + + PCSGCustomEventSceneSettingLock, + + PCSGCustomEventViewReceiverOK, + PCSGCustomEventViewReceiverConfig, + PCSGCustomEventViewReceiverBack, + PCSGCustomEventViewReceiverOffDisplay, + PCSGCustomEventViewReceiverUnlock, +} PCSGCustomEvent; diff --git a/applications/plugins/pocsag_pager/helpers/pocsag_pager_types.h b/applications/plugins/pocsag_pager/helpers/pocsag_pager_types.h new file mode 100644 index 000000000..fabd7f321 --- /dev/null +++ b/applications/plugins/pocsag_pager/helpers/pocsag_pager_types.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#define PCSG_VERSION_APP "0.1" +#define PCSG_DEVELOPED "@xMasterX & @Shmuma" +#define PCSG_GITHUB "https://github.com/xMasterX/flipper-pager" + +#define PCSG_KEY_FILE_VERSION 1 +#define PCSG_KEY_FILE_TYPE "Flipper POCSAG Pager Key File" + +/** PCSGRxKeyState state */ +typedef enum { + PCSGRxKeyStateIDLE, + PCSGRxKeyStateBack, + PCSGRxKeyStateStart, + PCSGRxKeyStateAddKey, +} PCSGRxKeyState; + +/** PCSGHopperState state */ +typedef enum { + PCSGHopperStateOFF, + PCSGHopperStateRunnig, + PCSGHopperStatePause, + PCSGHopperStateRSSITimeOut, +} PCSGHopperState; + +/** PCSGLock */ +typedef enum { + PCSGLockOff, + PCSGLockOn, +} PCSGLock; + +typedef enum { + POCSAGPagerViewVariableItemList, + POCSAGPagerViewSubmenu, + POCSAGPagerViewReceiver, + POCSAGPagerViewReceiverInfo, + POCSAGPagerViewWidget, +} POCSAGPagerView; + +/** POCSAGPagerTxRx state */ +typedef enum { + PCSGTxRxStateIDLE, + PCSGTxRxStateRx, + PCSGTxRxStateTx, + PCSGTxRxStateSleep, +} PCSGTxRxState; diff --git a/applications/plugins/pocsag_pager/images/Lock_7x8.png b/applications/plugins/pocsag_pager/images/Lock_7x8.png new file mode 100644 index 000000000..f7c9ca2c7 Binary files /dev/null and b/applications/plugins/pocsag_pager/images/Lock_7x8.png differ diff --git a/applications/plugins/pocsag_pager/images/Message_8x7.png b/applications/plugins/pocsag_pager/images/Message_8x7.png new file mode 100644 index 000000000..642688cd5 Binary files /dev/null and b/applications/plugins/pocsag_pager/images/Message_8x7.png differ diff --git a/applications/plugins/pocsag_pager/images/Pin_back_arrow_10x8.png b/applications/plugins/pocsag_pager/images/Pin_back_arrow_10x8.png new file mode 100644 index 000000000..3bafabd14 Binary files /dev/null and b/applications/plugins/pocsag_pager/images/Pin_back_arrow_10x8.png differ diff --git a/applications/plugins/pocsag_pager/images/Quest_7x8.png b/applications/plugins/pocsag_pager/images/Quest_7x8.png new file mode 100644 index 000000000..6825247fb Binary files /dev/null and b/applications/plugins/pocsag_pager/images/Quest_7x8.png differ diff --git a/applications/plugins/pocsag_pager/images/Scanning_123x52.png b/applications/plugins/pocsag_pager/images/Scanning_123x52.png new file mode 100644 index 000000000..ec785948d Binary files /dev/null and b/applications/plugins/pocsag_pager/images/Scanning_123x52.png differ diff --git a/applications/plugins/pocsag_pager/images/Unlock_7x8.png b/applications/plugins/pocsag_pager/images/Unlock_7x8.png new file mode 100644 index 000000000..9d82b4daf Binary files /dev/null and b/applications/plugins/pocsag_pager/images/Unlock_7x8.png differ diff --git a/applications/plugins/pocsag_pager/images/WarningDolphin_45x42.png b/applications/plugins/pocsag_pager/images/WarningDolphin_45x42.png new file mode 100644 index 000000000..d766ffbb4 Binary files /dev/null and b/applications/plugins/pocsag_pager/images/WarningDolphin_45x42.png differ diff --git a/applications/plugins/pocsag_pager/pocsag_pager_10px.png b/applications/plugins/pocsag_pager/pocsag_pager_10px.png new file mode 100644 index 000000000..a5686c1c0 Binary files /dev/null and b/applications/plugins/pocsag_pager/pocsag_pager_10px.png differ diff --git a/applications/plugins/pocsag_pager/pocsag_pager_app.c b/applications/plugins/pocsag_pager/pocsag_pager_app.c new file mode 100644 index 000000000..a3d45ac1b --- /dev/null +++ b/applications/plugins/pocsag_pager/pocsag_pager_app.c @@ -0,0 +1,210 @@ +#include "pocsag_pager_app_i.h" + +#include +#include +#include +#include "protocols/protocol_items.h" + +// Comment next line to build on OFW +#define IS_UNLEASHED + +static bool pocsag_pager_app_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + POCSAGPagerApp* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool pocsag_pager_app_back_event_callback(void* context) { + furi_assert(context); + POCSAGPagerApp* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static void pocsag_pager_app_tick_event_callback(void* context) { + furi_assert(context); + POCSAGPagerApp* app = context; + scene_manager_handle_tick_event(app->scene_manager); +} + +POCSAGPagerApp* pocsag_pager_app_alloc() { + POCSAGPagerApp* app = malloc(sizeof(POCSAGPagerApp)); + + // GUI + app->gui = furi_record_open(RECORD_GUI); + + // View Dispatcher + app->view_dispatcher = view_dispatcher_alloc(); + app->scene_manager = scene_manager_alloc(&pocsag_pager_scene_handlers, app); + view_dispatcher_enable_queue(app->view_dispatcher); + + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, pocsag_pager_app_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, pocsag_pager_app_back_event_callback); + view_dispatcher_set_tick_event_callback( + app->view_dispatcher, pocsag_pager_app_tick_event_callback, 100); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + // Open Notification record + app->notifications = furi_record_open(RECORD_NOTIFICATION); + + // Variable Item List + app->variable_item_list = variable_item_list_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + POCSAGPagerViewVariableItemList, + variable_item_list_get_view(app->variable_item_list)); + + // SubMenu + app->submenu = submenu_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, POCSAGPagerViewSubmenu, submenu_get_view(app->submenu)); + + // Widget + app->widget = widget_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, POCSAGPagerViewWidget, widget_get_view(app->widget)); + + // Receiver + app->pcsg_receiver = pcsg_view_receiver_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + POCSAGPagerViewReceiver, + pcsg_view_receiver_get_view(app->pcsg_receiver)); + + // Receiver Info + app->pcsg_receiver_info = pcsg_view_receiver_info_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + POCSAGPagerViewReceiverInfo, + pcsg_view_receiver_info_get_view(app->pcsg_receiver_info)); + + //init setting + app->setting = subghz_setting_alloc(); + +//ToDo FIX file name setting +#ifdef IS_UNLEASHED + subghz_setting_load(app->setting, EXT_PATH("pocsag/settings.txt"), true); +#else + subghz_setting_load(app->setting, EXT_PATH("pocsag/settings.txt")); +#endif + //init Worker & Protocol & History + app->lock = PCSGLockOff; + app->txrx = malloc(sizeof(POCSAGPagerTxRx)); + app->txrx->preset = malloc(sizeof(SubGhzRadioPreset)); + app->txrx->preset->name = furi_string_alloc(); + + // Custom Presets load without using config file + + FlipperFormat* temp_fm_preset = flipper_format_string_alloc(); + flipper_format_write_string_cstr( + temp_fm_preset, + (const char*)"Custom_preset_data", + (const char*)"02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 83 10 67 15 24 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00"); + flipper_format_rewind(temp_fm_preset); + subghz_setting_load_custom_preset(app->setting, (const char*)"FM95", temp_fm_preset); + + flipper_format_free(temp_fm_preset); + + FlipperFormat* temp_fm_preset2 = flipper_format_string_alloc(); + flipper_format_write_string_cstr( + temp_fm_preset2, + (const char*)"Custom_preset_data", + (const char*)"02 0D 0B 06 08 32 07 04 14 00 13 02 12 04 11 83 10 67 15 31 18 18 19 16 1D 91 1C 00 1B 07 20 FB 22 10 21 56 00 00 C0 00 00 00 00 00 00 00"); + flipper_format_rewind(temp_fm_preset2); + subghz_setting_load_custom_preset(app->setting, (const char*)"FM150", temp_fm_preset2); + + flipper_format_free(temp_fm_preset2); + + // custom presets loading - end + + pcsg_preset_init(app, "FM95", 439987500, NULL, 0); + + app->txrx->hopper_state = PCSGHopperStateOFF; + app->txrx->history = pcsg_history_alloc(); + app->txrx->worker = subghz_worker_alloc(); + app->txrx->environment = subghz_environment_alloc(); + subghz_environment_set_protocol_registry( + app->txrx->environment, (void*)&pocsag_pager_protocol_registry); + app->txrx->receiver = subghz_receiver_alloc_init(app->txrx->environment); + + subghz_receiver_set_filter(app->txrx->receiver, SubGhzProtocolFlag_Decodable); + subghz_worker_set_overrun_callback( + app->txrx->worker, (SubGhzWorkerOverrunCallback)subghz_receiver_reset); + subghz_worker_set_pair_callback( + app->txrx->worker, (SubGhzWorkerPairCallback)subghz_receiver_decode); + subghz_worker_set_context(app->txrx->worker, app->txrx->receiver); + + furi_hal_power_suppress_charge_enter(); + + scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneStart); + + return app; +} + +void pocsag_pager_app_free(POCSAGPagerApp* app) { + furi_assert(app); + + //CC1101 off + pcsg_sleep(app); + + // Submenu + view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewSubmenu); + submenu_free(app->submenu); + + // Variable Item List + view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewVariableItemList); + variable_item_list_free(app->variable_item_list); + + // Widget + view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewWidget); + widget_free(app->widget); + + // Receiver + view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewReceiver); + pcsg_view_receiver_free(app->pcsg_receiver); + + // Receiver Info + view_dispatcher_remove_view(app->view_dispatcher, POCSAGPagerViewReceiverInfo); + pcsg_view_receiver_info_free(app->pcsg_receiver_info); + + //setting + subghz_setting_free(app->setting); + + //Worker & Protocol & History + subghz_receiver_free(app->txrx->receiver); + subghz_environment_free(app->txrx->environment); + pcsg_history_free(app->txrx->history); + subghz_worker_free(app->txrx->worker); + furi_string_free(app->txrx->preset->name); + free(app->txrx->preset); + free(app->txrx); + + // View dispatcher + view_dispatcher_free(app->view_dispatcher); + scene_manager_free(app->scene_manager); + + // Notifications + furi_record_close(RECORD_NOTIFICATION); + app->notifications = NULL; + + // Close records + furi_record_close(RECORD_GUI); + + furi_hal_power_suppress_charge_exit(); + + free(app); +} + +int32_t pocsag_pager_app(void* p) { + UNUSED(p); + POCSAGPagerApp* pocsag_pager_app = pocsag_pager_app_alloc(); + + view_dispatcher_run(pocsag_pager_app->view_dispatcher); + + pocsag_pager_app_free(pocsag_pager_app); + + return 0; +} diff --git a/applications/plugins/pocsag_pager/pocsag_pager_app_i.c b/applications/plugins/pocsag_pager/pocsag_pager_app_i.c new file mode 100644 index 000000000..ba6e87c28 --- /dev/null +++ b/applications/plugins/pocsag_pager/pocsag_pager_app_i.c @@ -0,0 +1,141 @@ +#include "pocsag_pager_app_i.h" + +#define TAG "POCSAGPager" +#include + +void pcsg_preset_init( + void* context, + const char* preset_name, + uint32_t frequency, + uint8_t* preset_data, + size_t preset_data_size) { + furi_assert(context); + POCSAGPagerApp* app = context; + furi_string_set(app->txrx->preset->name, preset_name); + app->txrx->preset->frequency = frequency; + app->txrx->preset->data = preset_data; + app->txrx->preset->data_size = preset_data_size; +} + +void pcsg_get_frequency_modulation( + POCSAGPagerApp* app, + FuriString* frequency, + FuriString* modulation) { + furi_assert(app); + if(frequency != NULL) { + furi_string_printf( + frequency, + "%03ld.%02ld", + app->txrx->preset->frequency / 1000000 % 1000, + app->txrx->preset->frequency / 10000 % 100); + } + if(modulation != NULL) { + furi_string_printf(modulation, "%.2s", furi_string_get_cstr(app->txrx->preset->name)); + } +} + +void pcsg_begin(POCSAGPagerApp* app, uint8_t* preset_data) { + furi_assert(app); + UNUSED(preset_data); + furi_hal_subghz_reset(); + furi_hal_subghz_idle(); + furi_hal_subghz_load_custom_preset(preset_data); + furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow); + app->txrx->txrx_state = PCSGTxRxStateIDLE; +} + +uint32_t pcsg_rx(POCSAGPagerApp* app, uint32_t frequency) { + furi_assert(app); + if(!furi_hal_subghz_is_frequency_valid(frequency)) { + furi_crash("POCSAGPager: Incorrect RX frequency."); + } + furi_assert( + app->txrx->txrx_state != PCSGTxRxStateRx && app->txrx->txrx_state != PCSGTxRxStateSleep); + + furi_hal_subghz_idle(); + uint32_t value = furi_hal_subghz_set_frequency_and_path(frequency); + furi_hal_gpio_init(&gpio_cc1101_g0, GpioModeInput, GpioPullNo, GpioSpeedLow); + furi_hal_subghz_flush_rx(); + furi_hal_subghz_rx(); + + furi_hal_subghz_start_async_rx(subghz_worker_rx_callback, app->txrx->worker); + subghz_worker_start(app->txrx->worker); + app->txrx->txrx_state = PCSGTxRxStateRx; + return value; +} + +void pcsg_idle(POCSAGPagerApp* app) { + furi_assert(app); + furi_assert(app->txrx->txrx_state != PCSGTxRxStateSleep); + furi_hal_subghz_idle(); + app->txrx->txrx_state = PCSGTxRxStateIDLE; +} + +void pcsg_rx_end(POCSAGPagerApp* app) { + furi_assert(app); + furi_assert(app->txrx->txrx_state == PCSGTxRxStateRx); + if(subghz_worker_is_running(app->txrx->worker)) { + subghz_worker_stop(app->txrx->worker); + furi_hal_subghz_stop_async_rx(); + } + furi_hal_subghz_idle(); + app->txrx->txrx_state = PCSGTxRxStateIDLE; +} + +void pcsg_sleep(POCSAGPagerApp* app) { + furi_assert(app); + furi_hal_subghz_sleep(); + app->txrx->txrx_state = PCSGTxRxStateSleep; +} + +void pcsg_hopper_update(POCSAGPagerApp* app) { + furi_assert(app); + + switch(app->txrx->hopper_state) { + case PCSGHopperStateOFF: + return; + break; + case PCSGHopperStatePause: + return; + break; + case PCSGHopperStateRSSITimeOut: + if(app->txrx->hopper_timeout != 0) { + app->txrx->hopper_timeout--; + return; + } + break; + default: + break; + } + float rssi = -127.0f; + if(app->txrx->hopper_state != PCSGHopperStateRSSITimeOut) { + // See RSSI Calculation timings in CC1101 17.3 RSSI + rssi = furi_hal_subghz_get_rssi(); + + // Stay if RSSI is high enough + if(rssi > -90.0f) { + app->txrx->hopper_timeout = 10; + app->txrx->hopper_state = PCSGHopperStateRSSITimeOut; + return; + } + } else { + app->txrx->hopper_state = PCSGHopperStateRunnig; + } + // Select next frequency + if(app->txrx->hopper_idx_frequency < + subghz_setting_get_hopper_frequency_count(app->setting) - 1) { + app->txrx->hopper_idx_frequency++; + } else { + app->txrx->hopper_idx_frequency = 0; + } + + if(app->txrx->txrx_state == PCSGTxRxStateRx) { + pcsg_rx_end(app); + }; + if(app->txrx->txrx_state == PCSGTxRxStateIDLE) { + subghz_receiver_reset(app->txrx->receiver); + app->txrx->preset->frequency = + subghz_setting_get_hopper_frequency(app->setting, app->txrx->hopper_idx_frequency); + pcsg_rx(app, app->txrx->preset->frequency); + } +} diff --git a/applications/plugins/pocsag_pager/pocsag_pager_app_i.h b/applications/plugins/pocsag_pager/pocsag_pager_app_i.h new file mode 100644 index 000000000..8a0426dc5 --- /dev/null +++ b/applications/plugins/pocsag_pager/pocsag_pager_app_i.h @@ -0,0 +1,72 @@ +#pragma once + +#include "helpers/pocsag_pager_types.h" + +#include "scenes/pocsag_pager_scene.h" +#include +#include +#include +#include +#include +#include +#include +#include "views/pocsag_pager_receiver.h" +#include "views/pocsag_pager_receiver_info.h" +#include "pocsag_pager_history.h" + +#include +#include +#include +#include +#include + +typedef struct POCSAGPagerApp POCSAGPagerApp; + +struct POCSAGPagerTxRx { + SubGhzWorker* worker; + + SubGhzEnvironment* environment; + SubGhzReceiver* receiver; + SubGhzRadioPreset* preset; + PCSGHistory* history; + uint16_t idx_menu_chosen; + PCSGTxRxState txrx_state; + PCSGHopperState hopper_state; + uint8_t hopper_timeout; + uint8_t hopper_idx_frequency; + PCSGRxKeyState rx_key_state; +}; + +typedef struct POCSAGPagerTxRx POCSAGPagerTxRx; + +struct POCSAGPagerApp { + Gui* gui; + ViewDispatcher* view_dispatcher; + POCSAGPagerTxRx* txrx; + SceneManager* scene_manager; + NotificationApp* notifications; + VariableItemList* variable_item_list; + Submenu* submenu; + Widget* widget; + PCSGReceiver* pcsg_receiver; + PCSGReceiverInfo* pcsg_receiver_info; + PCSGLock lock; + SubGhzSetting* setting; +}; + +void pcsg_preset_init( + void* context, + const char* preset_name, + uint32_t frequency, + uint8_t* preset_data, + size_t preset_data_size); +void pcsg_get_frequency_modulation( + POCSAGPagerApp* app, + FuriString* frequency, + FuriString* modulation); +void pcsg_begin(POCSAGPagerApp* app, uint8_t* preset_data); +uint32_t pcsg_rx(POCSAGPagerApp* app, uint32_t frequency); +void pcsg_idle(POCSAGPagerApp* app); +void pcsg_rx_end(POCSAGPagerApp* app); +void pcsg_sleep(POCSAGPagerApp* app); +void pcsg_hopper_update(POCSAGPagerApp* app); diff --git a/applications/plugins/pocsag_pager/pocsag_pager_history.c b/applications/plugins/pocsag_pager/pocsag_pager_history.c new file mode 100644 index 000000000..d5f97b571 --- /dev/null +++ b/applications/plugins/pocsag_pager/pocsag_pager_history.c @@ -0,0 +1,223 @@ +#include "pocsag_pager_history.h" +#include +#include +#include +#include "protocols/pcsg_generic.h" + +#include + +#define PCSG_HISTORY_MAX 50 +#define TAG "PCSGHistory" + +typedef struct { + FuriString* item_str; + FlipperFormat* flipper_string; + uint8_t type; + SubGhzRadioPreset* preset; +} PCSGHistoryItem; + +ARRAY_DEF(PCSGHistoryItemArray, PCSGHistoryItem, M_POD_OPLIST) + +#define M_OPL_PCSGHistoryItemArray_t() ARRAY_OPLIST(PCSGHistoryItemArray, M_POD_OPLIST) + +typedef struct { + PCSGHistoryItemArray_t data; +} PCSGHistoryStruct; + +struct PCSGHistory { + uint32_t last_update_timestamp; + uint16_t last_index_write; + uint8_t code_last_hash_data; + FuriString* tmp_string; + PCSGHistoryStruct* history; +}; + +PCSGHistory* pcsg_history_alloc(void) { + PCSGHistory* instance = malloc(sizeof(PCSGHistory)); + instance->tmp_string = furi_string_alloc(); + instance->history = malloc(sizeof(PCSGHistoryStruct)); + PCSGHistoryItemArray_init(instance->history->data); + return instance; +} + +void pcsg_history_free(PCSGHistory* instance) { + furi_assert(instance); + furi_string_free(instance->tmp_string); + for + M_EACH(item, instance->history->data, PCSGHistoryItemArray_t) { + furi_string_free(item->item_str); + furi_string_free(item->preset->name); + free(item->preset); + flipper_format_free(item->flipper_string); + item->type = 0; + } + PCSGHistoryItemArray_clear(instance->history->data); + free(instance->history); + free(instance); +} + +uint32_t pcsg_history_get_frequency(PCSGHistory* instance, uint16_t idx) { + furi_assert(instance); + PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx); + return item->preset->frequency; +} + +SubGhzRadioPreset* pcsg_history_get_radio_preset(PCSGHistory* instance, uint16_t idx) { + furi_assert(instance); + PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx); + return item->preset; +} + +const char* pcsg_history_get_preset(PCSGHistory* instance, uint16_t idx) { + furi_assert(instance); + PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx); + return furi_string_get_cstr(item->preset->name); +} + +void pcsg_history_reset(PCSGHistory* instance) { + furi_assert(instance); + furi_string_reset(instance->tmp_string); + for + M_EACH(item, instance->history->data, PCSGHistoryItemArray_t) { + furi_string_free(item->item_str); + furi_string_free(item->preset->name); + free(item->preset); + flipper_format_free(item->flipper_string); + item->type = 0; + } + PCSGHistoryItemArray_reset(instance->history->data); + instance->last_index_write = 0; + instance->code_last_hash_data = 0; +} + +uint16_t pcsg_history_get_item(PCSGHistory* instance) { + furi_assert(instance); + return instance->last_index_write; +} + +uint8_t pcsg_history_get_type_protocol(PCSGHistory* instance, uint16_t idx) { + furi_assert(instance); + PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx); + return item->type; +} + +const char* pcsg_history_get_protocol_name(PCSGHistory* instance, uint16_t idx) { + furi_assert(instance); + PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx); + flipper_format_rewind(item->flipper_string); + if(!flipper_format_read_string(item->flipper_string, "Protocol", instance->tmp_string)) { + FURI_LOG_E(TAG, "Missing Protocol"); + furi_string_reset(instance->tmp_string); + } + return furi_string_get_cstr(instance->tmp_string); +} + +FlipperFormat* pcsg_history_get_raw_data(PCSGHistory* instance, uint16_t idx) { + furi_assert(instance); + PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx); + if(item->flipper_string) { + return item->flipper_string; + } else { + return NULL; + } +} +bool pcsg_history_get_text_space_left(PCSGHistory* instance, FuriString* output) { + furi_assert(instance); + if(instance->last_index_write == PCSG_HISTORY_MAX) { + if(output != NULL) furi_string_printf(output, "Memory is FULL"); + return true; + } + if(output != NULL) + furi_string_printf(output, "%02u/%02u", instance->last_index_write, PCSG_HISTORY_MAX); + return false; +} + +void pcsg_history_get_text_item_menu(PCSGHistory* instance, FuriString* output, uint16_t idx) { + PCSGHistoryItem* item = PCSGHistoryItemArray_get(instance->history->data, idx); + furi_string_set(output, item->item_str); +} + +PCSGHistoryStateAddKey + pcsg_history_add_to_history(PCSGHistory* instance, void* context, SubGhzRadioPreset* preset) { + furi_assert(instance); + furi_assert(context); + + if(instance->last_index_write >= PCSG_HISTORY_MAX) return PCSGHistoryStateAddKeyOverflow; + + SubGhzProtocolDecoderBase* decoder_base = context; + if((instance->code_last_hash_data == + subghz_protocol_decoder_base_get_hash_data(decoder_base)) && + ((furi_get_tick() - instance->last_update_timestamp) < 500)) { + instance->last_update_timestamp = furi_get_tick(); + return PCSGHistoryStateAddKeyTimeOut; + } + + instance->code_last_hash_data = subghz_protocol_decoder_base_get_hash_data(decoder_base); + instance->last_update_timestamp = furi_get_tick(); + + FlipperFormat* fff = flipper_format_string_alloc(); + subghz_protocol_decoder_base_serialize(decoder_base, fff, preset); + + do { + if(!flipper_format_rewind(fff)) { + FURI_LOG_E(TAG, "Rewind error"); + break; + } + + } while(false); + flipper_format_free(fff); + + PCSGHistoryItem* item = PCSGHistoryItemArray_push_raw(instance->history->data); + item->preset = malloc(sizeof(SubGhzRadioPreset)); + item->type = decoder_base->protocol->type; + item->preset->frequency = preset->frequency; + item->preset->name = furi_string_alloc(); + furi_string_set(item->preset->name, preset->name); + item->preset->data = preset->data; + item->preset->data_size = preset->data_size; + + item->item_str = furi_string_alloc(); + item->flipper_string = flipper_format_string_alloc(); + subghz_protocol_decoder_base_serialize(decoder_base, item->flipper_string, preset); + + do { + if(!flipper_format_rewind(item->flipper_string)) { + FURI_LOG_E(TAG, "Rewind error"); + break; + } + if(!flipper_format_read_string(item->flipper_string, "Protocol", instance->tmp_string)) { + FURI_LOG_E(TAG, "Missing Protocol"); + break; + } + + if(!flipper_format_rewind(item->flipper_string)) { + FURI_LOG_E(TAG, "Rewind error"); + break; + } + FuriString* temp_ric = furi_string_alloc(); + if(!flipper_format_read_string(item->flipper_string, "Ric", temp_ric)) { + FURI_LOG_E(TAG, "Missing Ric"); + break; + } + + FuriString* temp_message = furi_string_alloc(); + if(!flipper_format_read_string(item->flipper_string, "Message", temp_message)) { + FURI_LOG_E(TAG, "Missing Message"); + break; + } + + furi_string_printf( + item->item_str, + "%s%s", + furi_string_get_cstr(temp_ric), + furi_string_get_cstr(temp_message)); + + furi_string_free(temp_message); + furi_string_free(temp_ric); + + } while(false); + instance->last_index_write++; + return PCSGHistoryStateAddKeyNewDada; + + return PCSGHistoryStateAddKeyUnknown; +} diff --git a/applications/plugins/pocsag_pager/pocsag_pager_history.h b/applications/plugins/pocsag_pager/pocsag_pager_history.h new file mode 100644 index 000000000..7528fcc29 --- /dev/null +++ b/applications/plugins/pocsag_pager/pocsag_pager_history.h @@ -0,0 +1,112 @@ + +#pragma once + +#include +#include +#include +#include +#include + +typedef struct PCSGHistory PCSGHistory; + +/** History state add key */ +typedef enum { + PCSGHistoryStateAddKeyUnknown, + PCSGHistoryStateAddKeyTimeOut, + PCSGHistoryStateAddKeyNewDada, + PCSGHistoryStateAddKeyUpdateData, + PCSGHistoryStateAddKeyOverflow, +} PCSGHistoryStateAddKey; + +/** Allocate PCSGHistory + * + * @return PCSGHistory* + */ +PCSGHistory* pcsg_history_alloc(void); + +/** Free PCSGHistory + * + * @param instance - PCSGHistory instance + */ +void pcsg_history_free(PCSGHistory* instance); + +/** Clear history + * + * @param instance - PCSGHistory instance + */ +void pcsg_history_reset(PCSGHistory* instance); + +/** Get frequency to history[idx] + * + * @param instance - PCSGHistory instance + * @param idx - record index + * @return frequency - frequency Hz + */ +uint32_t pcsg_history_get_frequency(PCSGHistory* instance, uint16_t idx); + +SubGhzRadioPreset* pcsg_history_get_radio_preset(PCSGHistory* instance, uint16_t idx); + +/** Get preset to history[idx] + * + * @param instance - PCSGHistory instance + * @param idx - record index + * @return preset - preset name + */ +const char* pcsg_history_get_preset(PCSGHistory* instance, uint16_t idx); + +/** Get history index write + * + * @param instance - PCSGHistory instance + * @return idx - current record index + */ +uint16_t pcsg_history_get_item(PCSGHistory* instance); + +/** Get type protocol to history[idx] + * + * @param instance - PCSGHistory instance + * @param idx - record index + * @return type - type protocol + */ +uint8_t pcsg_history_get_type_protocol(PCSGHistory* instance, uint16_t idx); + +/** Get name protocol to history[idx] + * + * @param instance - PCSGHistory instance + * @param idx - record index + * @return name - const char* name protocol + */ +const char* pcsg_history_get_protocol_name(PCSGHistory* instance, uint16_t idx); + +/** Get string item menu to history[idx] + * + * @param instance - PCSGHistory instance + * @param output - FuriString* output + * @param idx - record index + */ +void pcsg_history_get_text_item_menu(PCSGHistory* instance, FuriString* output, uint16_t idx); + +/** Get string the remaining number of records to history + * + * @param instance - PCSGHistory instance + * @param output - FuriString* output + * @return bool - is FUUL + */ +bool pcsg_history_get_text_space_left(PCSGHistory* instance, FuriString* output); + +/** Add protocol to history + * + * @param instance - PCSGHistory instance + * @param context - SubGhzProtocolCommon context + * @param preset - SubGhzRadioPreset preset + * @return PCSGHistoryStateAddKey; + */ +PCSGHistoryStateAddKey + pcsg_history_add_to_history(PCSGHistory* instance, void* context, SubGhzRadioPreset* preset); + +/** Get SubGhzProtocolCommonLoad to load into the protocol decoder bin data + * + * @param instance - PCSGHistory instance + * @param idx - record index + * @return SubGhzProtocolCommonLoad* + */ +FlipperFormat* pcsg_history_get_raw_data(PCSGHistory* instance, uint16_t idx); diff --git a/applications/plugins/pocsag_pager/protocols/pcsg_generic.c b/applications/plugins/pocsag_pager/protocols/pcsg_generic.c new file mode 100644 index 000000000..890ed43d7 --- /dev/null +++ b/applications/plugins/pocsag_pager/protocols/pcsg_generic.c @@ -0,0 +1,123 @@ +#include "pcsg_generic.h" +#include +#include +#include "../helpers/pocsag_pager_types.h" + +#define TAG "PCSGBlockGeneric" + +void pcsg_block_generic_get_preset_name(const char* preset_name, FuriString* preset_str) { + const char* preset_name_temp; + if(!strcmp(preset_name, "AM270")) { + preset_name_temp = "FuriHalSubGhzPresetOok270Async"; + } else if(!strcmp(preset_name, "AM650")) { + preset_name_temp = "FuriHalSubGhzPresetOok650Async"; + } else if(!strcmp(preset_name, "FM238")) { + preset_name_temp = "FuriHalSubGhzPreset2FSKDev238Async"; + } else if(!strcmp(preset_name, "FM476")) { + preset_name_temp = "FuriHalSubGhzPreset2FSKDev476Async"; + } else { + preset_name_temp = "FuriHalSubGhzPresetCustom"; + } + furi_string_set(preset_str, preset_name_temp); +} + +bool pcsg_block_generic_serialize( + PCSGBlockGeneric* instance, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset) { + furi_assert(instance); + bool res = false; + FuriString* temp_str; + temp_str = furi_string_alloc(); + do { + stream_clean(flipper_format_get_raw_stream(flipper_format)); + if(!flipper_format_write_header_cstr( + flipper_format, PCSG_KEY_FILE_TYPE, PCSG_KEY_FILE_VERSION)) { + FURI_LOG_E(TAG, "Unable to add header"); + break; + } + + if(!flipper_format_write_uint32(flipper_format, "Frequency", &preset->frequency, 1)) { + FURI_LOG_E(TAG, "Unable to add Frequency"); + break; + } + + pcsg_block_generic_get_preset_name(furi_string_get_cstr(preset->name), temp_str); + if(!flipper_format_write_string_cstr( + flipper_format, "Preset", furi_string_get_cstr(temp_str))) { + FURI_LOG_E(TAG, "Unable to add Preset"); + break; + } + if(!strcmp(furi_string_get_cstr(temp_str), "FuriHalSubGhzPresetCustom")) { + if(!flipper_format_write_string_cstr( + flipper_format, "Custom_preset_module", "CC1101")) { + FURI_LOG_E(TAG, "Unable to add Custom_preset_module"); + break; + } + if(!flipper_format_write_hex( + flipper_format, "Custom_preset_data", preset->data, preset->data_size)) { + FURI_LOG_E(TAG, "Unable to add Custom_preset_data"); + break; + } + } + if(!flipper_format_write_string_cstr(flipper_format, "Protocol", instance->protocol_name)) { + FURI_LOG_E(TAG, "Unable to add Protocol"); + break; + } + + if(!flipper_format_write_string(flipper_format, "Ric", instance->result_ric)) { + FURI_LOG_E(TAG, "Unable to add Ric"); + break; + } + + if(!flipper_format_write_string(flipper_format, "Message", instance->result_msg)) { + FURI_LOG_E(TAG, "Unable to add Message"); + break; + } + + res = true; + } while(false); + furi_string_free(temp_str); + return res; +} + +bool pcsg_block_generic_deserialize(PCSGBlockGeneric* instance, FlipperFormat* flipper_format) { + furi_assert(instance); + bool res = false; + FuriString* temp_data = furi_string_alloc(); + FuriString* temp_data2 = furi_string_alloc(); + + do { + if(!flipper_format_rewind(flipper_format)) { + FURI_LOG_E(TAG, "Rewind error"); + break; + } + + if(!flipper_format_read_string(flipper_format, "Ric", temp_data2)) { + FURI_LOG_E(TAG, "Missing Ric"); + break; + } + if(instance->result_ric != NULL) { + furi_string_set(instance->result_ric, temp_data2); + } else { + instance->result_ric = furi_string_alloc_set(temp_data2); + } + + if(!flipper_format_read_string(flipper_format, "Message", temp_data)) { + FURI_LOG_E(TAG, "Missing Message"); + break; + } + if(instance->result_msg != NULL) { + furi_string_set(instance->result_msg, temp_data); + } else { + instance->result_msg = furi_string_alloc_set(temp_data); + } + + res = true; + } while(0); + + furi_string_free(temp_data); + furi_string_free(temp_data2); + + return res; +} diff --git a/applications/plugins/pocsag_pager/protocols/pcsg_generic.h b/applications/plugins/pocsag_pager/protocols/pcsg_generic.h new file mode 100644 index 000000000..ff925b6c9 --- /dev/null +++ b/applications/plugins/pocsag_pager/protocols/pcsg_generic.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include + +#include +#include "furi.h" +#include "furi_hal.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct PCSGBlockGeneric PCSGBlockGeneric; + +struct PCSGBlockGeneric { + const char* protocol_name; + FuriString* result_ric; + FuriString* result_msg; +}; + +/** + * Get name preset. + * @param preset_name name preset + * @param preset_str Output name preset + */ +void pcsg_block_generic_get_preset_name(const char* preset_name, FuriString* preset_str); + +/** + * Serialize data PCSGBlockGeneric. + * @param instance Pointer to a PCSGBlockGeneric instance + * @param flipper_format Pointer to a FlipperFormat instance + * @param preset The modulation on which the signal was received, SubGhzRadioPreset + * @return true On success + */ +bool pcsg_block_generic_serialize( + PCSGBlockGeneric* instance, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset); + +/** + * Deserialize data PCSGBlockGeneric. + * @param instance Pointer to a PCSGBlockGeneric instance + * @param flipper_format Pointer to a FlipperFormat instance + * @return true On success + */ +bool pcsg_block_generic_deserialize(PCSGBlockGeneric* instance, FlipperFormat* flipper_format); + +float pcsg_block_generic_fahrenheit_to_celsius(float fahrenheit); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/applications/plugins/pocsag_pager/protocols/pocsag.c b/applications/plugins/pocsag_pager/protocols/pocsag.c new file mode 100644 index 000000000..69d09d554 --- /dev/null +++ b/applications/plugins/pocsag_pager/protocols/pocsag.c @@ -0,0 +1,371 @@ +#include "pocsag.h" + +#include +#include +#include + +#define TAG "POCSAG" + +static const SubGhzBlockConst pocsag_const = { + .te_short = 833, + .te_delta = 100, +}; + +// Minimal amount of sync bits (interleaving zeros and ones) +#define POCSAG_MIN_SYNC_BITS 32 +#define POCSAG_CW_BITS 32 +#define POCSAG_CW_MASK 0xFFFFFFFF +#define POCSAG_FRAME_SYNC_CODE 0x7CD215D8 +#define POCSAG_IDLE_CODE_WORD 0x7A89C197 + +#define POCSAG_FUNC_NUM 0 +#define POCSAG_FUNC_ALERT1 1 +#define POCSAG_FUNC_ALERT2 2 +#define POCSAG_FUNC_ALPHANUM 3 + +static const char* func_msg[] = {"\e#Num:\e# ", "\e#Alert\e#", "\e#Alert:\e# ", "\e#Msg:\e# "}; +static const char* bcd_chars = "*U -)("; + +struct SubGhzProtocolDecoderPocsag { + SubGhzProtocolDecoderBase base; + + SubGhzBlockDecoder decoder; + PCSGBlockGeneric generic; + + uint8_t codeword_idx; + uint32_t ric; + uint8_t func; + + // partially decoded character + uint8_t char_bits; + uint8_t char_data; + + // message being decoded + FuriString* msg; + + // Done messages, ready to be serialized/deserialized + FuriString* done_msg; +}; + +typedef struct SubGhzProtocolDecoderPocsag SubGhzProtocolDecoderPocsag; + +typedef enum { + PocsagDecoderStepReset = 0, + PocsagDecoderStepFoundSync, + PocsagDecoderStepFoundPreamble, + PocsagDecoderStepMessage, +} PocsagDecoderStep; + +void* subghz_protocol_decoder_pocsag_alloc(SubGhzEnvironment* environment) { + UNUSED(environment); + + SubGhzProtocolDecoderPocsag* instance = malloc(sizeof(SubGhzProtocolDecoderPocsag)); + instance->base.protocol = &subghz_protocol_pocsag; + instance->generic.protocol_name = instance->base.protocol->name; + instance->msg = furi_string_alloc(); + instance->done_msg = furi_string_alloc(); + if(instance->generic.result_msg == NULL) { + instance->generic.result_msg = furi_string_alloc(); + } + if(instance->generic.result_ric == NULL) { + instance->generic.result_ric = furi_string_alloc(); + } + + return instance; +} + +void subghz_protocol_decoder_pocsag_free(void* context) { + furi_assert(context); + SubGhzProtocolDecoderPocsag* instance = context; + furi_string_free(instance->msg); + furi_string_free(instance->done_msg); + free(instance); +} + +void subghz_protocol_decoder_pocsag_reset(void* context) { + furi_assert(context); + SubGhzProtocolDecoderPocsag* instance = context; + + instance->decoder.parser_step = PocsagDecoderStepReset; + instance->decoder.decode_data = 0UL; + instance->decoder.decode_count_bit = 0; + instance->codeword_idx = 0; + instance->char_bits = 0; + instance->char_data = 0; + furi_string_reset(instance->msg); + furi_string_reset(instance->done_msg); + furi_string_reset(instance->generic.result_msg); + furi_string_reset(instance->generic.result_ric); +} + +static void pocsag_decode_address_word(SubGhzProtocolDecoderPocsag* instance, uint32_t data) { + instance->ric = (data >> 13); + instance->ric = (instance->ric << 3) | (instance->codeword_idx >> 1); + instance->func = (data >> 11) & 0b11; +} + +static bool decode_message_alphanumeric(SubGhzProtocolDecoderPocsag* instance, uint32_t data) { + for(uint8_t i = 0; i < 20; i++) { + instance->char_data >>= 1; + if(data & (1 << 30)) { + instance->char_data |= 1 << 6; + } + instance->char_bits++; + if(instance->char_bits == 7) { + if(instance->char_data == 0) return false; + furi_string_push_back(instance->msg, instance->char_data); + instance->char_data = 0; + instance->char_bits = 0; + } + data <<= 1; + } + return true; +} + +static void decode_message_numeric(SubGhzProtocolDecoderPocsag* instance, uint32_t data) { + // 5 groups with 4 bits each + uint8_t val; + for(uint8_t i = 0; i < 5; i++) { + val = (data >> (27 - i * 4)) & 0b1111; + // reverse the order of 4 bits + val = (val & 0x5) << 1 | (val & 0xA) >> 1; + val = (val & 0x3) << 2 | (val & 0xC) >> 2; + + if(val <= 9) + val += '0'; + else + val = bcd_chars[val - 10]; + furi_string_push_back(instance->msg, val); + } +} + +// decode message word, maintaining instance state for partial decoding. Return true if more data +// might follow or false if end of message reached. +static bool pocsag_decode_message_word(SubGhzProtocolDecoderPocsag* instance, uint32_t data) { + switch(instance->func) { + case POCSAG_FUNC_ALERT2: + case POCSAG_FUNC_ALPHANUM: + return decode_message_alphanumeric(instance, data); + + case POCSAG_FUNC_NUM: + decode_message_numeric(instance, data); + return true; + } + return false; +} + +// Function called when current message got decoded, but other messages might follow +static void pocsag_message_done(SubGhzProtocolDecoderPocsag* instance) { + // append the message to the long-term storage string + furi_string_cat_printf( + instance->generic.result_ric, "\e#RIC: %" PRIu32 "\e# | ", instance->ric); + furi_string_cat_str(instance->generic.result_ric, func_msg[instance->func]); + if(instance->func != POCSAG_FUNC_ALERT1) { + furi_string_cat(instance->done_msg, instance->msg); + } + furi_string_cat_str(instance->done_msg, " "); + + furi_string_cat(instance->generic.result_msg, instance->done_msg); + + // reset the state + instance->char_bits = 0; + instance->char_data = 0; + furi_string_reset(instance->msg); +} + +void subghz_protocol_decoder_pocsag_feed(void* context, bool level, uint32_t duration) { + furi_assert(context); + SubGhzProtocolDecoderPocsag* instance = context; + + // reset state - waiting for 32 bits of interleaving 1s and 0s + if(instance->decoder.parser_step == PocsagDecoderStepReset) { + if(DURATION_DIFF(duration, pocsag_const.te_short) < pocsag_const.te_delta) { + // POCSAG signals are inverted + subghz_protocol_blocks_add_bit(&instance->decoder, !level); + + if(instance->decoder.decode_count_bit == POCSAG_MIN_SYNC_BITS) { + instance->decoder.parser_step = PocsagDecoderStepFoundSync; + } + } else if(instance->decoder.decode_count_bit > 0) { + subghz_protocol_decoder_pocsag_reset(context); + } + return; + } + + int bits_count = duration / pocsag_const.te_short; + uint32_t extra = duration - pocsag_const.te_short * bits_count; + + if(DURATION_DIFF(extra, pocsag_const.te_short) < pocsag_const.te_delta) + bits_count++; + else if(extra > pocsag_const.te_delta) { + // in non-reset state we faced the error signal - we reached the end of the packet, flush data + if(furi_string_size(instance->done_msg) > 0) { + if(instance->base.callback) + instance->base.callback(&instance->base, instance->base.context); + } + subghz_protocol_decoder_pocsag_reset(context); + return; + } + + uint32_t codeword; + + // handle state machine for every incoming bit + while(bits_count-- > 0) { + subghz_protocol_blocks_add_bit(&instance->decoder, !level); + + switch(instance->decoder.parser_step) { + case PocsagDecoderStepFoundSync: + if((instance->decoder.decode_data & POCSAG_CW_MASK) == POCSAG_FRAME_SYNC_CODE) { + instance->decoder.parser_step = PocsagDecoderStepFoundPreamble; + instance->decoder.decode_count_bit = 0; + instance->decoder.decode_data = 0UL; + } + break; + case PocsagDecoderStepFoundPreamble: + // handle codewords + if(instance->decoder.decode_count_bit == POCSAG_CW_BITS) { + codeword = (uint32_t)(instance->decoder.decode_data & POCSAG_CW_MASK); + switch(codeword) { + case POCSAG_IDLE_CODE_WORD: + instance->codeword_idx++; + break; + case POCSAG_FRAME_SYNC_CODE: + instance->codeword_idx = 0; + break; + default: + // Here we expect only address messages + if(codeword >> 31 == 0) { + pocsag_decode_address_word(instance, codeword); + instance->decoder.parser_step = PocsagDecoderStepMessage; + } + instance->codeword_idx++; + } + instance->decoder.decode_count_bit = 0; + instance->decoder.decode_data = 0UL; + } + break; + + case PocsagDecoderStepMessage: + if(instance->decoder.decode_count_bit == POCSAG_CW_BITS) { + codeword = (uint32_t)(instance->decoder.decode_data & POCSAG_CW_MASK); + switch(codeword) { + case POCSAG_IDLE_CODE_WORD: + // Idle during the message stops the message + instance->codeword_idx++; + instance->decoder.parser_step = PocsagDecoderStepFoundPreamble; + pocsag_message_done(instance); + break; + case POCSAG_FRAME_SYNC_CODE: + instance->codeword_idx = 0; + break; + default: + // In this state, both address and message words can arrive + if(codeword >> 31 == 0) { + pocsag_message_done(instance); + pocsag_decode_address_word(instance, codeword); + } else { + if(!pocsag_decode_message_word(instance, codeword)) { + instance->decoder.parser_step = PocsagDecoderStepFoundPreamble; + pocsag_message_done(instance); + } + } + instance->codeword_idx++; + } + instance->decoder.decode_count_bit = 0; + instance->decoder.decode_data = 0UL; + } + break; + } + } +} + +uint8_t subghz_protocol_decoder_pocsag_get_hash_data(void* context) { + furi_assert(context); + SubGhzProtocolDecoderPocsag* instance = context; + uint8_t hash = 0; + for(size_t i = 0; i < furi_string_size(instance->done_msg); i++) + hash ^= furi_string_get_char(instance->done_msg, i); + return hash; +} + +bool subghz_protocol_decoder_pocsag_serialize( + void* context, + FlipperFormat* flipper_format, + SubGhzRadioPreset* preset) { + furi_assert(context); + SubGhzProtocolDecoderPocsag* instance = context; + uint32_t msg_len; + + if(!pcsg_block_generic_serialize(&instance->generic, flipper_format, preset)) return false; + + msg_len = furi_string_size(instance->done_msg); + if(!flipper_format_write_uint32(flipper_format, "MsgLen", &msg_len, 1)) { + FURI_LOG_E(TAG, "Error adding MsgLen"); + return false; + } + + uint8_t* s = (uint8_t*)furi_string_get_cstr(instance->done_msg); + if(!flipper_format_write_hex(flipper_format, "Msg", s, msg_len)) { + FURI_LOG_E(TAG, "Error adding Msg"); + return false; + } + return true; +} + +bool subghz_protocol_decoder_pocsag_deserialize(void* context, FlipperFormat* flipper_format) { + furi_assert(context); + SubGhzProtocolDecoderPocsag* instance = context; + bool ret = false; + uint32_t msg_len; + uint8_t* buf; + + do { + if(!pcsg_block_generic_deserialize(&instance->generic, flipper_format)) { + break; + } + + if(!flipper_format_read_uint32(flipper_format, "MsgLen", &msg_len, 1)) { + FURI_LOG_E(TAG, "Missing MsgLen"); + break; + } + + buf = malloc(msg_len); + if(!flipper_format_read_hex(flipper_format, "Msg", buf, msg_len)) { + FURI_LOG_E(TAG, "Missing Msg"); + free(buf); + break; + } + furi_string_set_strn(instance->done_msg, (const char*)buf, msg_len); + free(buf); + + ret = true; + } while(false); + return ret; +} + +void subhz_protocol_decoder_pocsag_get_string(void* context, FuriString* output) { + furi_assert(context); + SubGhzProtocolDecoderPocsag* instance = context; + furi_string_cat_printf(output, "%s\r\n", instance->generic.protocol_name); + furi_string_cat(output, instance->done_msg); +} + +const SubGhzProtocolDecoder subghz_protocol_pocsag_decoder = { + .alloc = subghz_protocol_decoder_pocsag_alloc, + .free = subghz_protocol_decoder_pocsag_free, + .reset = subghz_protocol_decoder_pocsag_reset, + .feed = subghz_protocol_decoder_pocsag_feed, + .get_hash_data = subghz_protocol_decoder_pocsag_get_hash_data, + .serialize = subghz_protocol_decoder_pocsag_serialize, + .deserialize = subghz_protocol_decoder_pocsag_deserialize, + .get_string = subhz_protocol_decoder_pocsag_get_string, +}; + +const SubGhzProtocol subghz_protocol_pocsag = { + .name = SUBGHZ_PROTOCOL_POCSAG_NAME, + .type = SubGhzProtocolTypeStatic, + .flag = SubGhzProtocolFlag_FM | SubGhzProtocolFlag_Decodable | SubGhzProtocolFlag_Save | + SubGhzProtocolFlag_Load, + + .decoder = &subghz_protocol_pocsag_decoder, +}; diff --git a/applications/plugins/pocsag_pager/protocols/pocsag.h b/applications/plugins/pocsag_pager/protocols/pocsag.h new file mode 100644 index 000000000..559fa3918 --- /dev/null +++ b/applications/plugins/pocsag_pager/protocols/pocsag.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include +#include +#include +#include "pcsg_generic.h" +#include + +#define SUBGHZ_PROTOCOL_POCSAG_NAME "POCSAG" + +extern const SubGhzProtocol subghz_protocol_pocsag; diff --git a/applications/plugins/pocsag_pager/protocols/protocol_items.c b/applications/plugins/pocsag_pager/protocols/protocol_items.c new file mode 100644 index 000000000..7e6ebebbd --- /dev/null +++ b/applications/plugins/pocsag_pager/protocols/protocol_items.c @@ -0,0 +1,9 @@ +#include "protocol_items.h" + +const SubGhzProtocol* pocsag_pager_protocol_registry_items[] = { + &subghz_protocol_pocsag, +}; + +const SubGhzProtocolRegistry pocsag_pager_protocol_registry = { + .items = pocsag_pager_protocol_registry_items, + .size = COUNT_OF(pocsag_pager_protocol_registry_items)}; \ No newline at end of file diff --git a/applications/plugins/pocsag_pager/protocols/protocol_items.h b/applications/plugins/pocsag_pager/protocols/protocol_items.h new file mode 100644 index 000000000..3981cd2e3 --- /dev/null +++ b/applications/plugins/pocsag_pager/protocols/protocol_items.h @@ -0,0 +1,6 @@ +#pragma once +#include "../pocsag_pager_app_i.h" + +#include "pocsag.h" + +extern const SubGhzProtocolRegistry pocsag_pager_protocol_registry; diff --git a/applications/plugins/pocsag_pager/scenes/pocsag_pager_receiver.c b/applications/plugins/pocsag_pager/scenes/pocsag_pager_receiver.c new file mode 100644 index 000000000..3f41b9b5d --- /dev/null +++ b/applications/plugins/pocsag_pager/scenes/pocsag_pager_receiver.c @@ -0,0 +1,207 @@ +#include "../pocsag_pager_app_i.h" +#include "../views/pocsag_pager_receiver.h" + +static const NotificationSequence subghs_sequence_rx = { + &message_green_255, + + &message_vibro_on, + &message_note_c6, + &message_delay_50, + &message_sound_off, + &message_vibro_off, + + &message_delay_50, + NULL, +}; + +static const NotificationSequence subghs_sequence_rx_locked = { + &message_green_255, + + &message_display_backlight_on, + + &message_vibro_on, + &message_note_c6, + &message_delay_50, + &message_sound_off, + &message_vibro_off, + + &message_delay_500, + + &message_display_backlight_off, + NULL, +}; + +static void pocsag_pager_scene_receiver_update_statusbar(void* context) { + POCSAGPagerApp* app = context; + FuriString* history_stat_str; + history_stat_str = furi_string_alloc(); + if(!pcsg_history_get_text_space_left(app->txrx->history, history_stat_str)) { + FuriString* frequency_str; + FuriString* modulation_str; + + frequency_str = furi_string_alloc(); + modulation_str = furi_string_alloc(); + + pcsg_get_frequency_modulation(app, frequency_str, modulation_str); + + pcsg_view_receiver_add_data_statusbar( + app->pcsg_receiver, + furi_string_get_cstr(frequency_str), + furi_string_get_cstr(modulation_str), + furi_string_get_cstr(history_stat_str)); + + furi_string_free(frequency_str); + furi_string_free(modulation_str); + } else { + pcsg_view_receiver_add_data_statusbar( + app->pcsg_receiver, furi_string_get_cstr(history_stat_str), "", ""); + } + furi_string_free(history_stat_str); +} + +void pocsag_pager_scene_receiver_callback(PCSGCustomEvent event, void* context) { + furi_assert(context); + POCSAGPagerApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, event); +} + +static void pocsag_pager_scene_receiver_add_to_history_callback( + SubGhzReceiver* receiver, + SubGhzProtocolDecoderBase* decoder_base, + void* context) { + furi_assert(context); + POCSAGPagerApp* app = context; + FuriString* str_buff; + str_buff = furi_string_alloc(); + + if(pcsg_history_add_to_history(app->txrx->history, decoder_base, app->txrx->preset) == + PCSGHistoryStateAddKeyNewDada) { + furi_string_reset(str_buff); + + pcsg_history_get_text_item_menu( + app->txrx->history, str_buff, pcsg_history_get_item(app->txrx->history) - 1); + pcsg_view_receiver_add_item_to_menu( + app->pcsg_receiver, + furi_string_get_cstr(str_buff), + pcsg_history_get_type_protocol( + app->txrx->history, pcsg_history_get_item(app->txrx->history) - 1)); + + pocsag_pager_scene_receiver_update_statusbar(app); + notification_message(app->notifications, &sequence_blink_green_10); + if(app->lock != PCSGLockOn) { + notification_message(app->notifications, &subghs_sequence_rx); + } else { + notification_message(app->notifications, &subghs_sequence_rx_locked); + } + } + subghz_receiver_reset(receiver); + furi_string_free(str_buff); + app->txrx->rx_key_state = PCSGRxKeyStateAddKey; +} + +void pocsag_pager_scene_receiver_on_enter(void* context) { + POCSAGPagerApp* app = context; + + FuriString* str_buff; + str_buff = furi_string_alloc(); + + if(app->txrx->rx_key_state == PCSGRxKeyStateIDLE) { + pcsg_preset_init(app, "FM95", 439987500, NULL, 0); + pcsg_history_reset(app->txrx->history); + app->txrx->rx_key_state = PCSGRxKeyStateStart; + } + + pcsg_view_receiver_set_lock(app->pcsg_receiver, app->lock); + + //Load history to receiver + pcsg_view_receiver_exit(app->pcsg_receiver); + for(uint8_t i = 0; i < pcsg_history_get_item(app->txrx->history); i++) { + furi_string_reset(str_buff); + pcsg_history_get_text_item_menu(app->txrx->history, str_buff, i); + pcsg_view_receiver_add_item_to_menu( + app->pcsg_receiver, + furi_string_get_cstr(str_buff), + pcsg_history_get_type_protocol(app->txrx->history, i)); + app->txrx->rx_key_state = PCSGRxKeyStateAddKey; + } + furi_string_free(str_buff); + pocsag_pager_scene_receiver_update_statusbar(app); + + pcsg_view_receiver_set_callback(app->pcsg_receiver, pocsag_pager_scene_receiver_callback, app); + subghz_receiver_set_rx_callback( + app->txrx->receiver, pocsag_pager_scene_receiver_add_to_history_callback, app); + + if(app->txrx->txrx_state == PCSGTxRxStateRx) { + pcsg_rx_end(app); + }; + if((app->txrx->txrx_state == PCSGTxRxStateIDLE) || + (app->txrx->txrx_state == PCSGTxRxStateSleep)) { + pcsg_begin( + app, + subghz_setting_get_preset_data_by_name( + app->setting, furi_string_get_cstr(app->txrx->preset->name))); + + pcsg_rx(app, app->txrx->preset->frequency); + } + + pcsg_view_receiver_set_idx_menu(app->pcsg_receiver, app->txrx->idx_menu_chosen); + view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewReceiver); +} + +bool pocsag_pager_scene_receiver_on_event(void* context, SceneManagerEvent event) { + POCSAGPagerApp* app = context; + bool consumed = false; + if(event.type == SceneManagerEventTypeCustom) { + switch(event.event) { + case PCSGCustomEventViewReceiverBack: + // Stop CC1101 Rx + if(app->txrx->txrx_state == PCSGTxRxStateRx) { + pcsg_rx_end(app); + pcsg_sleep(app); + }; + app->txrx->hopper_state = PCSGHopperStateOFF; + app->txrx->idx_menu_chosen = 0; + subghz_receiver_set_rx_callback(app->txrx->receiver, NULL, app); + + app->txrx->rx_key_state = PCSGRxKeyStateIDLE; + pcsg_preset_init(app, "FM95", 439987500, NULL, 0); + scene_manager_search_and_switch_to_previous_scene( + app->scene_manager, POCSAGPagerSceneStart); + consumed = true; + break; + case PCSGCustomEventViewReceiverOK: + app->txrx->idx_menu_chosen = pcsg_view_receiver_get_idx_menu(app->pcsg_receiver); + scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneReceiverInfo); + consumed = true; + break; + case PCSGCustomEventViewReceiverConfig: + app->txrx->idx_menu_chosen = pcsg_view_receiver_get_idx_menu(app->pcsg_receiver); + scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneReceiverConfig); + consumed = true; + break; + case PCSGCustomEventViewReceiverOffDisplay: + notification_message(app->notifications, &sequence_display_backlight_off); + consumed = true; + break; + case PCSGCustomEventViewReceiverUnlock: + app->lock = PCSGLockOff; + consumed = true; + break; + default: + break; + } + } else if(event.type == SceneManagerEventTypeTick) { + if(app->txrx->hopper_state != PCSGHopperStateOFF) { + pcsg_hopper_update(app); + pocsag_pager_scene_receiver_update_statusbar(app); + } + if(app->txrx->txrx_state == PCSGTxRxStateRx) { + notification_message(app->notifications, &sequence_blink_cyan_10); + } + } + return consumed; +} + +void pocsag_pager_scene_receiver_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene.c b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene.c new file mode 100644 index 000000000..4644d99c0 --- /dev/null +++ b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene.c @@ -0,0 +1,30 @@ +#include "../pocsag_pager_app_i.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const pocsag_pager_scene_on_enter_handlers[])(void*) = { +#include "pocsag_pager_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 pocsag_pager_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "pocsag_pager_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 pocsag_pager_scene_on_exit_handlers[])(void* context) = { +#include "pocsag_pager_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers pocsag_pager_scene_handlers = { + .on_enter_handlers = pocsag_pager_scene_on_enter_handlers, + .on_event_handlers = pocsag_pager_scene_on_event_handlers, + .on_exit_handlers = pocsag_pager_scene_on_exit_handlers, + .scene_num = POCSAGPagerSceneNum, +}; diff --git a/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene.h b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene.h new file mode 100644 index 000000000..d5c64f9d9 --- /dev/null +++ b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) POCSAGPagerScene##id, +typedef enum { +#include "pocsag_pager_scene_config.h" + POCSAGPagerSceneNum, +} POCSAGPagerScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers pocsag_pager_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "pocsag_pager_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 "pocsag_pager_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 "pocsag_pager_scene_config.h" +#undef ADD_SCENE diff --git a/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_about.c b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_about.c new file mode 100644 index 000000000..2af33c8bf --- /dev/null +++ b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_about.c @@ -0,0 +1,74 @@ +#include "../pocsag_pager_app_i.h" +#include "../helpers/pocsag_pager_types.h" + +void pocsag_pager_scene_about_widget_callback(GuiButtonType result, InputType type, void* context) { + POCSAGPagerApp* app = context; + if(type == InputTypeShort) { + view_dispatcher_send_custom_event(app->view_dispatcher, result); + } +} + +void pocsag_pager_scene_about_on_enter(void* context) { + POCSAGPagerApp* app = context; + + FuriString* temp_str; + temp_str = furi_string_alloc(); + furi_string_printf(temp_str, "\e#%s\n", "Information"); + + furi_string_cat_printf(temp_str, "Version: %s\n", PCSG_VERSION_APP); + furi_string_cat_printf(temp_str, "Developed by:\n%s\n\n", PCSG_DEVELOPED); + furi_string_cat_printf(temp_str, "Github: %s\n\n", PCSG_GITHUB); + + furi_string_cat_printf(temp_str, "\e#%s\n", "Description"); + furi_string_cat_printf(temp_str, "Receiving POCSAG Pager \nmessages\n\n"); + + furi_string_cat_printf(temp_str, "Supported protocols:\n"); + size_t i = 0; + const char* protocol_name = + subghz_environment_get_protocol_name_registry(app->txrx->environment, i++); + do { + furi_string_cat_printf(temp_str, "%s\n", protocol_name); + protocol_name = subghz_environment_get_protocol_name_registry(app->txrx->environment, i++); + } while(protocol_name != NULL); + + widget_add_text_box_element( + app->widget, + 0, + 0, + 128, + 14, + AlignCenter, + AlignBottom, + "\e#\e! \e!\n", + false); + widget_add_text_box_element( + app->widget, + 0, + 2, + 128, + 14, + AlignCenter, + AlignBottom, + "\e#\e! POCSAG Pager \e!\n", + false); + widget_add_text_scroll_element(app->widget, 0, 16, 128, 50, furi_string_get_cstr(temp_str)); + furi_string_free(temp_str); + + view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewWidget); +} + +bool pocsag_pager_scene_about_on_event(void* context, SceneManagerEvent event) { + POCSAGPagerApp* app = context; + bool consumed = false; + UNUSED(app); + UNUSED(event); + + return consumed; +} + +void pocsag_pager_scene_about_on_exit(void* context) { + POCSAGPagerApp* app = context; + + // Clear views + widget_reset(app->widget); +} diff --git a/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_config.h b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_config.h new file mode 100644 index 000000000..8136af14f --- /dev/null +++ b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_config.h @@ -0,0 +1,5 @@ +ADD_SCENE(pocsag_pager, start, Start) +ADD_SCENE(pocsag_pager, about, About) +ADD_SCENE(pocsag_pager, receiver, Receiver) +ADD_SCENE(pocsag_pager, receiver_config, ReceiverConfig) +ADD_SCENE(pocsag_pager, receiver_info, ReceiverInfo) diff --git a/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_receiver_config.c b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_receiver_config.c new file mode 100644 index 000000000..154e7d270 --- /dev/null +++ b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_receiver_config.c @@ -0,0 +1,221 @@ +#include "../pocsag_pager_app_i.h" + +enum PCSGSettingIndex { + PCSGSettingIndexFrequency, + PCSGSettingIndexHopping, + PCSGSettingIndexModulation, + PCSGSettingIndexLock, +}; + +#define HOPPING_COUNT 2 +const char* const hopping_text[HOPPING_COUNT] = { + "OFF", + "ON", +}; +const uint32_t hopping_value[HOPPING_COUNT] = { + PCSGHopperStateOFF, + PCSGHopperStateRunnig, +}; + +uint8_t pocsag_pager_scene_receiver_config_next_frequency(const uint32_t value, void* context) { + furi_assert(context); + POCSAGPagerApp* app = context; + uint8_t index = 0; + for(uint8_t i = 0; i < subghz_setting_get_frequency_count(app->setting); i++) { + if(value == subghz_setting_get_frequency(app->setting, i)) { + index = i; + break; + } else { + index = subghz_setting_get_frequency_default_index(app->setting); + } + } + return index; +} + +uint8_t pocsag_pager_scene_receiver_config_next_preset(const char* preset_name, void* context) { + furi_assert(context); + POCSAGPagerApp* app = context; + uint8_t index = 0; + for(uint8_t i = 0; i < subghz_setting_get_preset_count(app->setting); i++) { + if(!strcmp(subghz_setting_get_preset_name(app->setting, i), preset_name)) { + index = i; + break; + } else { + // index = subghz_setting_get_frequency_default_index(app ->setting); + } + } + return index; +} + +uint8_t pocsag_pager_scene_receiver_config_hopper_value_index( + const uint32_t value, + const uint32_t values[], + uint8_t values_count, + void* context) { + furi_assert(context); + UNUSED(values_count); + POCSAGPagerApp* app = context; + + if(value == values[0]) { + return 0; + } else { + variable_item_set_current_value_text( + (VariableItem*)scene_manager_get_scene_state( + app->scene_manager, POCSAGPagerSceneReceiverConfig), + " -----"); + return 1; + } +} + +static void pocsag_pager_scene_receiver_config_set_frequency(VariableItem* item) { + POCSAGPagerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + if(app->txrx->hopper_state == PCSGHopperStateOFF) { + char text_buf[10] = {0}; + snprintf( + text_buf, + sizeof(text_buf), + "%lu.%02lu", + subghz_setting_get_frequency(app->setting, index) / 1000000, + (subghz_setting_get_frequency(app->setting, index) % 1000000) / 10000); + variable_item_set_current_value_text(item, text_buf); + app->txrx->preset->frequency = subghz_setting_get_frequency(app->setting, index); + } else { + variable_item_set_current_value_index( + item, subghz_setting_get_frequency_default_index(app->setting)); + } +} + +static void pocsag_pager_scene_receiver_config_set_preset(VariableItem* item) { + POCSAGPagerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text( + item, subghz_setting_get_preset_name(app->setting, index)); + pcsg_preset_init( + app, + subghz_setting_get_preset_name(app->setting, index), + app->txrx->preset->frequency, + subghz_setting_get_preset_data(app->setting, index), + subghz_setting_get_preset_data_size(app->setting, index)); +} + +static void pocsag_pager_scene_receiver_config_set_hopping_running(VariableItem* item) { + POCSAGPagerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + variable_item_set_current_value_text(item, hopping_text[index]); + if(hopping_value[index] == PCSGHopperStateOFF) { + char text_buf[10] = {0}; + snprintf( + text_buf, + sizeof(text_buf), + "%lu.%02lu", + subghz_setting_get_default_frequency(app->setting) / 1000000, + (subghz_setting_get_default_frequency(app->setting) % 1000000) / 10000); + variable_item_set_current_value_text( + (VariableItem*)scene_manager_get_scene_state( + app->scene_manager, POCSAGPagerSceneReceiverConfig), + text_buf); + app->txrx->preset->frequency = subghz_setting_get_default_frequency(app->setting); + variable_item_set_current_value_index( + (VariableItem*)scene_manager_get_scene_state( + app->scene_manager, POCSAGPagerSceneReceiverConfig), + subghz_setting_get_frequency_default_index(app->setting)); + } else { + variable_item_set_current_value_text( + (VariableItem*)scene_manager_get_scene_state( + app->scene_manager, POCSAGPagerSceneReceiverConfig), + " -----"); + variable_item_set_current_value_index( + (VariableItem*)scene_manager_get_scene_state( + app->scene_manager, POCSAGPagerSceneReceiverConfig), + subghz_setting_get_frequency_default_index(app->setting)); + } + + app->txrx->hopper_state = hopping_value[index]; +} + +static void + pocsag_pager_scene_receiver_config_var_list_enter_callback(void* context, uint32_t index) { + furi_assert(context); + POCSAGPagerApp* app = context; + if(index == PCSGSettingIndexLock) { + view_dispatcher_send_custom_event(app->view_dispatcher, PCSGCustomEventSceneSettingLock); + } +} + +void pocsag_pager_scene_receiver_config_on_enter(void* context) { + POCSAGPagerApp* app = context; + VariableItem* item; + uint8_t value_index; + + item = variable_item_list_add( + app->variable_item_list, + "Frequency:", + subghz_setting_get_frequency_count(app->setting), + pocsag_pager_scene_receiver_config_set_frequency, + app); + value_index = + pocsag_pager_scene_receiver_config_next_frequency(app->txrx->preset->frequency, app); + scene_manager_set_scene_state( + app->scene_manager, POCSAGPagerSceneReceiverConfig, (uint32_t)item); + variable_item_set_current_value_index(item, value_index); + char text_buf[10] = {0}; + snprintf( + text_buf, + sizeof(text_buf), + "%lu.%02lu", + subghz_setting_get_frequency(app->setting, value_index) / 1000000, + (subghz_setting_get_frequency(app->setting, value_index) % 1000000) / 10000); + variable_item_set_current_value_text(item, text_buf); + + item = variable_item_list_add( + app->variable_item_list, + "Hopping:", + HOPPING_COUNT, + pocsag_pager_scene_receiver_config_set_hopping_running, + app); + value_index = pocsag_pager_scene_receiver_config_hopper_value_index( + app->txrx->hopper_state, hopping_value, HOPPING_COUNT, app); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text(item, hopping_text[value_index]); + + item = variable_item_list_add( + app->variable_item_list, + "Modulation:", + subghz_setting_get_preset_count(app->setting), + pocsag_pager_scene_receiver_config_set_preset, + app); + value_index = pocsag_pager_scene_receiver_config_next_preset( + furi_string_get_cstr(app->txrx->preset->name), app); + variable_item_set_current_value_index(item, value_index); + variable_item_set_current_value_text( + item, subghz_setting_get_preset_name(app->setting, value_index)); + + variable_item_list_add(app->variable_item_list, "Lock Keyboard", 1, NULL, NULL); + variable_item_list_set_enter_callback( + app->variable_item_list, pocsag_pager_scene_receiver_config_var_list_enter_callback, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewVariableItemList); +} + +bool pocsag_pager_scene_receiver_config_on_event(void* context, SceneManagerEvent event) { + POCSAGPagerApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == PCSGCustomEventSceneSettingLock) { + app->lock = PCSGLockOn; + scene_manager_previous_scene(app->scene_manager); + consumed = true; + } + } + return consumed; +} + +void pocsag_pager_scene_receiver_config_on_exit(void* context) { + POCSAGPagerApp* app = context; + variable_item_list_set_selected_item(app->variable_item_list, 0); + variable_item_list_reset(app->variable_item_list); +} diff --git a/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_receiver_info.c b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_receiver_info.c new file mode 100644 index 000000000..5f17d9fb7 --- /dev/null +++ b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_receiver_info.c @@ -0,0 +1,50 @@ +#include "../pocsag_pager_app_i.h" +#include "../views/pocsag_pager_receiver.h" + +void pocsag_pager_scene_receiver_info_callback(PCSGCustomEvent event, void* context) { + furi_assert(context); + POCSAGPagerApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, event); +} + +static void pocsag_pager_scene_receiver_info_add_to_history_callback( + SubGhzReceiver* receiver, + SubGhzProtocolDecoderBase* decoder_base, + void* context) { + furi_assert(context); + POCSAGPagerApp* app = context; + + if(pcsg_history_add_to_history(app->txrx->history, decoder_base, app->txrx->preset) == + PCSGHistoryStateAddKeyUpdateData) { + pcsg_view_receiver_info_update( + app->pcsg_receiver_info, + pcsg_history_get_raw_data(app->txrx->history, app->txrx->idx_menu_chosen)); + subghz_receiver_reset(receiver); + + notification_message(app->notifications, &sequence_blink_green_10); + app->txrx->rx_key_state = PCSGRxKeyStateAddKey; + } +} + +void pocsag_pager_scene_receiver_info_on_enter(void* context) { + POCSAGPagerApp* app = context; + + subghz_receiver_set_rx_callback( + app->txrx->receiver, pocsag_pager_scene_receiver_info_add_to_history_callback, app); + pcsg_view_receiver_info_update( + app->pcsg_receiver_info, + pcsg_history_get_raw_data(app->txrx->history, app->txrx->idx_menu_chosen)); + view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewReceiverInfo); +} + +bool pocsag_pager_scene_receiver_info_on_event(void* context, SceneManagerEvent event) { + POCSAGPagerApp* app = context; + bool consumed = false; + UNUSED(app); + UNUSED(event); + return consumed; +} + +void pocsag_pager_scene_receiver_info_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_start.c b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_start.c new file mode 100644 index 000000000..d2a94facb --- /dev/null +++ b/applications/plugins/pocsag_pager/scenes/pocsag_pager_scene_start.c @@ -0,0 +1,58 @@ +#include "../pocsag_pager_app_i.h" + +typedef enum { + SubmenuIndexPOCSAGPagerReceiver, + SubmenuIndexPOCSAGPagerAbout, +} SubmenuIndex; + +void pocsag_pager_scene_start_submenu_callback(void* context, uint32_t index) { + POCSAGPagerApp* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +void pocsag_pager_scene_start_on_enter(void* context) { + UNUSED(context); + POCSAGPagerApp* app = context; + Submenu* submenu = app->submenu; + + submenu_add_item( + submenu, + "Receive messages", + SubmenuIndexPOCSAGPagerReceiver, + pocsag_pager_scene_start_submenu_callback, + app); + submenu_add_item( + submenu, + "About", + SubmenuIndexPOCSAGPagerAbout, + pocsag_pager_scene_start_submenu_callback, + app); + + submenu_set_selected_item( + submenu, scene_manager_get_scene_state(app->scene_manager, POCSAGPagerSceneStart)); + + view_dispatcher_switch_to_view(app->view_dispatcher, POCSAGPagerViewSubmenu); +} + +bool pocsag_pager_scene_start_on_event(void* context, SceneManagerEvent event) { + POCSAGPagerApp* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SubmenuIndexPOCSAGPagerAbout) { + scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneAbout); + consumed = true; + } else if(event.event == SubmenuIndexPOCSAGPagerReceiver) { + scene_manager_next_scene(app->scene_manager, POCSAGPagerSceneReceiver); + consumed = true; + } + scene_manager_set_scene_state(app->scene_manager, POCSAGPagerSceneStart, event.event); + } + + return consumed; +} + +void pocsag_pager_scene_start_on_exit(void* context) { + POCSAGPagerApp* app = context; + submenu_reset(app->submenu); +} diff --git a/applications/plugins/pocsag_pager/views/pocsag_pager_receiver.c b/applications/plugins/pocsag_pager/views/pocsag_pager_receiver.c new file mode 100644 index 000000000..d8398cbfe --- /dev/null +++ b/applications/plugins/pocsag_pager/views/pocsag_pager_receiver.c @@ -0,0 +1,440 @@ +#include "pocsag_pager_receiver.h" +#include "../pocsag_pager_app_i.h" +#include +#include + +#include +#include +#include + +#define FRAME_HEIGHT 12 +#define MAX_LEN_PX 112 +#define MENU_ITEMS 4u +#define UNLOCK_CNT 3 + +typedef struct { + FuriString* item_str; + uint8_t type; +} PCSGReceiverMenuItem; + +ARRAY_DEF(PCSGReceiverMenuItemArray, PCSGReceiverMenuItem, M_POD_OPLIST) + +#define M_OPL_PCSGReceiverMenuItemArray_t() ARRAY_OPLIST(PCSGReceiverMenuItemArray, M_POD_OPLIST) + +struct PCSGReceiverHistory { + PCSGReceiverMenuItemArray_t data; +}; + +typedef struct PCSGReceiverHistory PCSGReceiverHistory; + +static const Icon* ReceiverItemIcons[] = { + [SubGhzProtocolTypeUnknown] = &I_Quest_7x8, + [SubGhzProtocolTypeStatic] = &I_Message_8x7, + [SubGhzProtocolTypeDynamic] = &I_Lock_7x8, +}; + +typedef enum { + PCSGReceiverBarShowDefault, + PCSGReceiverBarShowLock, + PCSGReceiverBarShowToUnlockPress, + PCSGReceiverBarShowUnlock, +} PCSGReceiverBarShow; + +struct PCSGReceiver { + PCSGLock lock; + uint8_t lock_count; + FuriTimer* timer; + View* view; + PCSGReceiverCallback callback; + void* context; +}; + +typedef struct { + FuriString* frequency_str; + FuriString* preset_str; + FuriString* history_stat_str; + PCSGReceiverHistory* history; + uint16_t idx; + uint16_t list_offset; + uint16_t history_item; + PCSGReceiverBarShow bar_show; +} PCSGReceiverModel; + +void pcsg_view_receiver_set_lock(PCSGReceiver* pcsg_receiver, PCSGLock lock) { + furi_assert(pcsg_receiver); + pcsg_receiver->lock_count = 0; + if(lock == PCSGLockOn) { + pcsg_receiver->lock = lock; + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { model->bar_show = PCSGReceiverBarShowLock; }, + true); + furi_timer_start(pcsg_receiver->timer, pdMS_TO_TICKS(1000)); + } else { + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { model->bar_show = PCSGReceiverBarShowDefault; }, + true); + } +} + +void pcsg_view_receiver_set_callback( + PCSGReceiver* pcsg_receiver, + PCSGReceiverCallback callback, + void* context) { + furi_assert(pcsg_receiver); + furi_assert(callback); + pcsg_receiver->callback = callback; + pcsg_receiver->context = context; +} + +static void pcsg_view_receiver_update_offset(PCSGReceiver* pcsg_receiver) { + furi_assert(pcsg_receiver); + + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + size_t history_item = model->history_item; + uint16_t bounds = history_item > 3 ? 2 : history_item; + + if(history_item > 3 && model->idx >= (int16_t)(history_item - 1)) { + model->list_offset = model->idx - 3; + } else if(model->list_offset < model->idx - bounds) { + model->list_offset = + CLAMP(model->list_offset + 1, (int16_t)(history_item - bounds), 0); + } else if(model->list_offset > model->idx - bounds) { + model->list_offset = CLAMP(model->idx - 1, (int16_t)(history_item - bounds), 0); + } + }, + true); +} + +void pcsg_view_receiver_add_item_to_menu( + PCSGReceiver* pcsg_receiver, + const char* name, + uint8_t type) { + furi_assert(pcsg_receiver); + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + PCSGReceiverMenuItem* item_menu = + PCSGReceiverMenuItemArray_push_raw(model->history->data); + item_menu->item_str = furi_string_alloc_set(name); + item_menu->type = type; + if((model->idx == model->history_item - 1)) { + model->history_item++; + model->idx++; + } else { + model->history_item++; + } + }, + true); + pcsg_view_receiver_update_offset(pcsg_receiver); +} + +void pcsg_view_receiver_add_data_statusbar( + PCSGReceiver* pcsg_receiver, + const char* frequency_str, + const char* preset_str, + const char* history_stat_str) { + furi_assert(pcsg_receiver); + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + furi_string_set_str(model->frequency_str, frequency_str); + furi_string_set_str(model->preset_str, preset_str); + furi_string_set_str(model->history_stat_str, history_stat_str); + }, + true); +} + +static void pcsg_view_receiver_draw_frame(Canvas* canvas, uint16_t idx, bool scrollbar) { + canvas_set_color(canvas, ColorBlack); + canvas_draw_box(canvas, 0, 0 + idx * FRAME_HEIGHT, scrollbar ? 122 : 127, FRAME_HEIGHT); + + canvas_set_color(canvas, ColorWhite); + canvas_draw_dot(canvas, 0, 0 + idx * FRAME_HEIGHT); + canvas_draw_dot(canvas, 1, 0 + idx * FRAME_HEIGHT); + canvas_draw_dot(canvas, 0, (0 + idx * FRAME_HEIGHT) + 1); + + canvas_draw_dot(canvas, 0, (0 + idx * FRAME_HEIGHT) + 11); + canvas_draw_dot(canvas, scrollbar ? 121 : 126, 0 + idx * FRAME_HEIGHT); + canvas_draw_dot(canvas, scrollbar ? 121 : 126, (0 + idx * FRAME_HEIGHT) + 11); +} + +void pcsg_view_receiver_draw(Canvas* canvas, PCSGReceiverModel* model) { + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontSecondary); + + elements_button_left(canvas, "Config"); + canvas_draw_line(canvas, 46, 51, 125, 51); + + bool scrollbar = model->history_item > 4; + FuriString* str_buff; + str_buff = furi_string_alloc(); + + PCSGReceiverMenuItem* item_menu; + + for(size_t i = 0; i < MIN(model->history_item, MENU_ITEMS); ++i) { + size_t idx = CLAMP((uint16_t)(i + model->list_offset), model->history_item, 0); + item_menu = PCSGReceiverMenuItemArray_get(model->history->data, idx); + furi_string_set(str_buff, item_menu->item_str); + furi_string_replace_all(str_buff, "#", ""); + elements_string_fit_width(canvas, str_buff, scrollbar ? MAX_LEN_PX - 7 : MAX_LEN_PX); + if(model->idx == idx) { + pcsg_view_receiver_draw_frame(canvas, i, scrollbar); + } else { + canvas_set_color(canvas, ColorBlack); + } + canvas_draw_icon(canvas, 4, 2 + i * FRAME_HEIGHT, ReceiverItemIcons[item_menu->type]); + canvas_draw_str(canvas, 15, 9 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buff)); + furi_string_reset(str_buff); + } + if(scrollbar) { + elements_scrollbar_pos(canvas, 128, 0, 49, model->idx, model->history_item); + } + furi_string_free(str_buff); + + canvas_set_color(canvas, ColorBlack); + + if(model->history_item == 0) { + canvas_draw_icon(canvas, 0, 0, &I_Scanning_123x52); + canvas_set_font(canvas, FontPrimary); + canvas_draw_str(canvas, 63, 46, "Scanning..."); + canvas_draw_line(canvas, 46, 51, 125, 51); + canvas_set_font(canvas, FontSecondary); + } + + switch(model->bar_show) { + case PCSGReceiverBarShowLock: + canvas_draw_icon(canvas, 64, 55, &I_Lock_7x8); + canvas_draw_str(canvas, 74, 62, "Locked"); + break; + case PCSGReceiverBarShowToUnlockPress: + canvas_draw_str(canvas, 44, 62, furi_string_get_cstr(model->frequency_str)); + canvas_draw_str(canvas, 79, 62, furi_string_get_cstr(model->preset_str)); + canvas_draw_str(canvas, 96, 62, furi_string_get_cstr(model->history_stat_str)); + canvas_set_font(canvas, FontSecondary); + elements_bold_rounded_frame(canvas, 14, 8, 99, 48); + elements_multiline_text(canvas, 65, 26, "To unlock\npress:"); + canvas_draw_icon(canvas, 65, 42, &I_Pin_back_arrow_10x8); + canvas_draw_icon(canvas, 80, 42, &I_Pin_back_arrow_10x8); + canvas_draw_icon(canvas, 95, 42, &I_Pin_back_arrow_10x8); + canvas_draw_icon(canvas, 16, 13, &I_WarningDolphin_45x42); + canvas_draw_dot(canvas, 17, 61); + break; + case PCSGReceiverBarShowUnlock: + canvas_draw_icon(canvas, 64, 55, &I_Unlock_7x8); + canvas_draw_str(canvas, 74, 62, "Unlocked"); + break; + default: + canvas_draw_str(canvas, 44, 62, furi_string_get_cstr(model->frequency_str)); + canvas_draw_str(canvas, 79, 62, furi_string_get_cstr(model->preset_str)); + canvas_draw_str(canvas, 96, 62, furi_string_get_cstr(model->history_stat_str)); + break; + } +} + +static void pcsg_view_receiver_timer_callback(void* context) { + furi_assert(context); + PCSGReceiver* pcsg_receiver = context; + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { model->bar_show = PCSGReceiverBarShowDefault; }, + true); + if(pcsg_receiver->lock_count < UNLOCK_CNT) { + pcsg_receiver->callback(PCSGCustomEventViewReceiverOffDisplay, pcsg_receiver->context); + } else { + pcsg_receiver->lock = PCSGLockOff; + pcsg_receiver->callback(PCSGCustomEventViewReceiverUnlock, pcsg_receiver->context); + } + pcsg_receiver->lock_count = 0; +} + +bool pcsg_view_receiver_input(InputEvent* event, void* context) { + furi_assert(context); + PCSGReceiver* pcsg_receiver = context; + + if(pcsg_receiver->lock == PCSGLockOn) { + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { model->bar_show = PCSGReceiverBarShowToUnlockPress; }, + true); + if(pcsg_receiver->lock_count == 0) { + furi_timer_start(pcsg_receiver->timer, pdMS_TO_TICKS(1000)); + } + if(event->key == InputKeyBack && event->type == InputTypeShort) { + pcsg_receiver->lock_count++; + } + if(pcsg_receiver->lock_count >= UNLOCK_CNT) { + pcsg_receiver->callback(PCSGCustomEventViewReceiverUnlock, pcsg_receiver->context); + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { model->bar_show = PCSGReceiverBarShowUnlock; }, + true); + pcsg_receiver->lock = PCSGLockOff; + furi_timer_start(pcsg_receiver->timer, pdMS_TO_TICKS(650)); + } + + return true; + } + + if(event->key == InputKeyBack && event->type == InputTypeShort) { + pcsg_receiver->callback(PCSGCustomEventViewReceiverBack, pcsg_receiver->context); + } else if( + event->key == InputKeyUp && + (event->type == InputTypeShort || event->type == InputTypeRepeat)) { + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + if(model->idx != 0) model->idx--; + }, + true); + } else if( + event->key == InputKeyDown && + (event->type == InputTypeShort || event->type == InputTypeRepeat)) { + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + if(model->idx != model->history_item - 1) model->idx++; + }, + true); + } else if(event->key == InputKeyLeft && event->type == InputTypeShort) { + pcsg_receiver->callback(PCSGCustomEventViewReceiverConfig, pcsg_receiver->context); + } else if(event->key == InputKeyOk && event->type == InputTypeShort) { + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + if(model->history_item != 0) { + pcsg_receiver->callback(PCSGCustomEventViewReceiverOK, pcsg_receiver->context); + } + }, + false); + } + + pcsg_view_receiver_update_offset(pcsg_receiver); + + return true; +} + +void pcsg_view_receiver_enter(void* context) { + furi_assert(context); +} + +void pcsg_view_receiver_exit(void* context) { + furi_assert(context); + PCSGReceiver* pcsg_receiver = context; + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + furi_string_reset(model->frequency_str); + furi_string_reset(model->preset_str); + furi_string_reset(model->history_stat_str); + for + M_EACH(item_menu, model->history->data, PCSGReceiverMenuItemArray_t) { + furi_string_free(item_menu->item_str); + item_menu->type = 0; + } + PCSGReceiverMenuItemArray_reset(model->history->data); + model->idx = 0; + model->list_offset = 0; + model->history_item = 0; + }, + false); + furi_timer_stop(pcsg_receiver->timer); +} + +PCSGReceiver* pcsg_view_receiver_alloc() { + PCSGReceiver* pcsg_receiver = malloc(sizeof(PCSGReceiver)); + + // View allocation and configuration + pcsg_receiver->view = view_alloc(); + + pcsg_receiver->lock = PCSGLockOff; + pcsg_receiver->lock_count = 0; + view_allocate_model(pcsg_receiver->view, ViewModelTypeLocking, sizeof(PCSGReceiverModel)); + view_set_context(pcsg_receiver->view, pcsg_receiver); + view_set_draw_callback(pcsg_receiver->view, (ViewDrawCallback)pcsg_view_receiver_draw); + view_set_input_callback(pcsg_receiver->view, pcsg_view_receiver_input); + view_set_enter_callback(pcsg_receiver->view, pcsg_view_receiver_enter); + view_set_exit_callback(pcsg_receiver->view, pcsg_view_receiver_exit); + + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + model->frequency_str = furi_string_alloc(); + model->preset_str = furi_string_alloc(); + model->history_stat_str = furi_string_alloc(); + model->bar_show = PCSGReceiverBarShowDefault; + model->history = malloc(sizeof(PCSGReceiverHistory)); + PCSGReceiverMenuItemArray_init(model->history->data); + }, + true); + pcsg_receiver->timer = + furi_timer_alloc(pcsg_view_receiver_timer_callback, FuriTimerTypeOnce, pcsg_receiver); + return pcsg_receiver; +} + +void pcsg_view_receiver_free(PCSGReceiver* pcsg_receiver) { + furi_assert(pcsg_receiver); + + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + furi_string_free(model->frequency_str); + furi_string_free(model->preset_str); + furi_string_free(model->history_stat_str); + for + M_EACH(item_menu, model->history->data, PCSGReceiverMenuItemArray_t) { + furi_string_free(item_menu->item_str); + item_menu->type = 0; + } + PCSGReceiverMenuItemArray_clear(model->history->data); + free(model->history); + }, + false); + furi_timer_free(pcsg_receiver->timer); + view_free(pcsg_receiver->view); + free(pcsg_receiver); +} + +View* pcsg_view_receiver_get_view(PCSGReceiver* pcsg_receiver) { + furi_assert(pcsg_receiver); + return pcsg_receiver->view; +} + +uint16_t pcsg_view_receiver_get_idx_menu(PCSGReceiver* pcsg_receiver) { + furi_assert(pcsg_receiver); + uint32_t idx = 0; + with_view_model( + pcsg_receiver->view, PCSGReceiverModel * model, { idx = model->idx; }, false); + return idx; +} + +void pcsg_view_receiver_set_idx_menu(PCSGReceiver* pcsg_receiver, uint16_t idx) { + furi_assert(pcsg_receiver); + with_view_model( + pcsg_receiver->view, + PCSGReceiverModel * model, + { + model->idx = idx; + if(model->idx > 2) model->list_offset = idx - 2; + }, + true); + pcsg_view_receiver_update_offset(pcsg_receiver); +} diff --git a/applications/plugins/pocsag_pager/views/pocsag_pager_receiver.h b/applications/plugins/pocsag_pager/views/pocsag_pager_receiver.h new file mode 100644 index 000000000..5ea2d4859 --- /dev/null +++ b/applications/plugins/pocsag_pager/views/pocsag_pager_receiver.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include "../helpers/pocsag_pager_types.h" +#include "../helpers/pocsag_pager_event.h" + +typedef struct PCSGReceiver PCSGReceiver; + +typedef void (*PCSGReceiverCallback)(PCSGCustomEvent event, void* context); + +void pcsg_view_receiver_set_lock(PCSGReceiver* pcsg_receiver, PCSGLock keyboard); + +void pcsg_view_receiver_set_callback( + PCSGReceiver* pcsg_receiver, + PCSGReceiverCallback callback, + void* context); + +PCSGReceiver* pcsg_view_receiver_alloc(); + +void pcsg_view_receiver_free(PCSGReceiver* pcsg_receiver); + +View* pcsg_view_receiver_get_view(PCSGReceiver* pcsg_receiver); + +void pcsg_view_receiver_add_data_statusbar( + PCSGReceiver* pcsg_receiver, + const char* frequency_str, + const char* preset_str, + const char* history_stat_str); + +void pcsg_view_receiver_add_item_to_menu( + PCSGReceiver* pcsg_receiver, + const char* name, + uint8_t type); + +uint16_t pcsg_view_receiver_get_idx_menu(PCSGReceiver* pcsg_receiver); + +void pcsg_view_receiver_set_idx_menu(PCSGReceiver* pcsg_receiver, uint16_t idx); + +void pcsg_view_receiver_exit(void* context); diff --git a/applications/plugins/pocsag_pager/views/pocsag_pager_receiver_info.c b/applications/plugins/pocsag_pager/views/pocsag_pager_receiver_info.c new file mode 100644 index 000000000..4811f3902 --- /dev/null +++ b/applications/plugins/pocsag_pager/views/pocsag_pager_receiver_info.c @@ -0,0 +1,137 @@ +#include "pocsag_pager_receiver.h" +#include "../pocsag_pager_app_i.h" +#include "pocsag_pager_icons.h" +#include "../protocols/pcsg_generic.h" +#include +#include + +#define abs(x) ((x) > 0 ? (x) : -(x)) + +struct PCSGReceiverInfo { + View* view; +}; + +typedef struct { + FuriString* protocol_name; + PCSGBlockGeneric* generic; +} PCSGReceiverInfoModel; + +void pcsg_view_receiver_info_update(PCSGReceiverInfo* pcsg_receiver_info, FlipperFormat* fff) { + furi_assert(pcsg_receiver_info); + furi_assert(fff); + + with_view_model( + pcsg_receiver_info->view, + PCSGReceiverInfoModel * model, + { + flipper_format_rewind(fff); + flipper_format_read_string(fff, "Protocol", model->protocol_name); + + pcsg_block_generic_deserialize(model->generic, fff); + }, + true); +} + +void pcsg_view_receiver_info_draw(Canvas* canvas, PCSGReceiverInfoModel* model) { + canvas_clear(canvas); + canvas_set_color(canvas, ColorBlack); + canvas_set_font(canvas, FontSecondary); + if(model->generic->result_ric != NULL) { + elements_text_box( + canvas, + 0, + 0, + 128, + 64, + AlignLeft, + AlignTop, + furi_string_get_cstr(model->generic->result_ric), + false); + } + if(model->generic->result_msg != NULL) { + elements_text_box( + canvas, + 0, + 12, + 128, + 64, + AlignLeft, + AlignTop, + furi_string_get_cstr(model->generic->result_msg), + false); + } +} + +bool pcsg_view_receiver_info_input(InputEvent* event, void* context) { + furi_assert(context); + //PCSGReceiverInfo* pcsg_receiver_info = context; + + if(event->key == InputKeyBack) { + return false; + } + + return true; +} + +void pcsg_view_receiver_info_enter(void* context) { + furi_assert(context); +} + +void pcsg_view_receiver_info_exit(void* context) { + furi_assert(context); + PCSGReceiverInfo* pcsg_receiver_info = context; + + with_view_model( + pcsg_receiver_info->view, + PCSGReceiverInfoModel * model, + { furi_string_reset(model->protocol_name); }, + false); +} + +PCSGReceiverInfo* pcsg_view_receiver_info_alloc() { + PCSGReceiverInfo* pcsg_receiver_info = malloc(sizeof(PCSGReceiverInfo)); + + // View allocation and configuration + pcsg_receiver_info->view = view_alloc(); + + view_allocate_model( + pcsg_receiver_info->view, ViewModelTypeLocking, sizeof(PCSGReceiverInfoModel)); + view_set_context(pcsg_receiver_info->view, pcsg_receiver_info); + view_set_draw_callback( + pcsg_receiver_info->view, (ViewDrawCallback)pcsg_view_receiver_info_draw); + view_set_input_callback(pcsg_receiver_info->view, pcsg_view_receiver_info_input); + view_set_enter_callback(pcsg_receiver_info->view, pcsg_view_receiver_info_enter); + view_set_exit_callback(pcsg_receiver_info->view, pcsg_view_receiver_info_exit); + + with_view_model( + pcsg_receiver_info->view, + PCSGReceiverInfoModel * model, + { + model->generic = malloc(sizeof(PCSGBlockGeneric)); + model->protocol_name = furi_string_alloc(); + }, + true); + + return pcsg_receiver_info; +} + +void pcsg_view_receiver_info_free(PCSGReceiverInfo* pcsg_receiver_info) { + furi_assert(pcsg_receiver_info); + + with_view_model( + pcsg_receiver_info->view, + PCSGReceiverInfoModel * model, + { + furi_string_free(model->protocol_name); + free(model->generic); + }, + false); + + view_free(pcsg_receiver_info->view); + free(pcsg_receiver_info); +} + +View* pcsg_view_receiver_info_get_view(PCSGReceiverInfo* pcsg_receiver_info) { + furi_assert(pcsg_receiver_info); + return pcsg_receiver_info->view; +} diff --git a/applications/plugins/pocsag_pager/views/pocsag_pager_receiver_info.h b/applications/plugins/pocsag_pager/views/pocsag_pager_receiver_info.h new file mode 100644 index 000000000..dfc85ec88 --- /dev/null +++ b/applications/plugins/pocsag_pager/views/pocsag_pager_receiver_info.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "../helpers/pocsag_pager_types.h" +#include "../helpers/pocsag_pager_event.h" +#include + +typedef struct PCSGReceiverInfo PCSGReceiverInfo; + +void pcsg_view_receiver_info_update(PCSGReceiverInfo* pcsg_receiver_info, FlipperFormat* fff); + +PCSGReceiverInfo* pcsg_view_receiver_info_alloc(); + +void pcsg_view_receiver_info_free(PCSGReceiverInfo* pcsg_receiver_info); + +View* pcsg_view_receiver_info_get_view(PCSGReceiverInfo* pcsg_receiver_info);