[FL-2627] Flipper applications: SDK, build and debug system (#1387)
* Added support for running applications from SD card (FAPs - Flipper Application Packages) * Added plugin_dist target for fbt to build FAPs * All apps of type FlipperAppType.EXTERNAL and FlipperAppType.PLUGIN are built as FAPs by default * Updated VSCode configuration for new fbt features - re-deploy stock configuration to use them * Added debugging support for FAPs with fbt debug & VSCode * Added public firmware API with automated versioning Co-authored-by: hedger <hedger@users.noreply.github.com> Co-authored-by: SG <who.just.the.doctor@gmail.com> Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
App(
|
||||
appid="basic_plugins",
|
||||
name="Basic applications for plug-in menu",
|
||||
apptype=FlipperAppType.METAPACKAGE,
|
||||
provides=[
|
||||
"music_player",
|
||||
"snake_game",
|
||||
"bt_hid",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
App(
|
||||
appid="bt_hid",
|
||||
name="Bluetooth Remote",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="bt_hid_app",
|
||||
stack_size=1 * 1024,
|
||||
cdefines=["APP_BLE_HID"],
|
||||
requires=[
|
||||
"bt",
|
||||
"gui",
|
||||
],
|
||||
order=10,
|
||||
fap_icon="bt_remote_10px.png",
|
||||
fap_category="Tools",
|
||||
)
|
||||
@@ -0,0 +1,197 @@
|
||||
#include "bt_hid.h"
|
||||
#include <furi_hal_bt.h>
|
||||
#include <notification/notification_messages.h>
|
||||
|
||||
#define TAG "BtHidApp"
|
||||
|
||||
enum BtDebugSubmenuIndex {
|
||||
BtHidSubmenuIndexKeynote,
|
||||
BtHidSubmenuIndexKeyboard,
|
||||
BtHidSubmenuIndexMedia,
|
||||
BtHidSubmenuIndexMouse,
|
||||
};
|
||||
|
||||
void bt_hid_submenu_callback(void* context, uint32_t index) {
|
||||
furi_assert(context);
|
||||
BtHid* app = context;
|
||||
if(index == BtHidSubmenuIndexKeynote) {
|
||||
app->view_id = BtHidViewKeynote;
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewKeynote);
|
||||
} else if(index == BtHidSubmenuIndexKeyboard) {
|
||||
app->view_id = BtHidViewKeyboard;
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewKeyboard);
|
||||
} else if(index == BtHidSubmenuIndexMedia) {
|
||||
app->view_id = BtHidViewMedia;
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewMedia);
|
||||
} else if(index == BtHidSubmenuIndexMouse) {
|
||||
app->view_id = BtHidViewMouse;
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewMouse);
|
||||
}
|
||||
}
|
||||
|
||||
void bt_hid_dialog_callback(DialogExResult result, void* context) {
|
||||
furi_assert(context);
|
||||
BtHid* app = context;
|
||||
if(result == DialogExResultLeft) {
|
||||
view_dispatcher_stop(app->view_dispatcher);
|
||||
} else if(result == DialogExResultRight) {
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id); // Show last view
|
||||
} else if(result == DialogExResultCenter) {
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, BtHidViewSubmenu);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t bt_hid_exit_confirm_view(void* context) {
|
||||
UNUSED(context);
|
||||
return BtHidViewExitConfirm;
|
||||
}
|
||||
|
||||
uint32_t bt_hid_exit(void* context) {
|
||||
UNUSED(context);
|
||||
return VIEW_NONE;
|
||||
}
|
||||
|
||||
void bt_hid_connection_status_changed_callback(BtStatus status, void* context) {
|
||||
furi_assert(context);
|
||||
BtHid* bt_hid = context;
|
||||
bool connected = (status == BtStatusConnected);
|
||||
if(connected) {
|
||||
notification_internal_message(bt_hid->notifications, &sequence_set_blue_255);
|
||||
} else {
|
||||
notification_internal_message(bt_hid->notifications, &sequence_reset_blue);
|
||||
}
|
||||
bt_hid_keynote_set_connected_status(bt_hid->bt_hid_keynote, connected);
|
||||
bt_hid_keyboard_set_connected_status(bt_hid->bt_hid_keyboard, connected);
|
||||
bt_hid_media_set_connected_status(bt_hid->bt_hid_media, connected);
|
||||
bt_hid_mouse_set_connected_status(bt_hid->bt_hid_mouse, connected);
|
||||
}
|
||||
|
||||
BtHid* bt_hid_app_alloc() {
|
||||
BtHid* app = malloc(sizeof(BtHid));
|
||||
|
||||
// Gui
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
|
||||
// Bt
|
||||
app->bt = furi_record_open(RECORD_BT);
|
||||
|
||||
// Notifications
|
||||
app->notifications = furi_record_open(RECORD_NOTIFICATION);
|
||||
|
||||
// View dispatcher
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
view_dispatcher_enable_queue(app->view_dispatcher);
|
||||
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||
|
||||
// Submenu view
|
||||
app->submenu = submenu_alloc();
|
||||
submenu_add_item(
|
||||
app->submenu, "Keynote", BtHidSubmenuIndexKeynote, bt_hid_submenu_callback, app);
|
||||
submenu_add_item(
|
||||
app->submenu, "Keyboard", BtHidSubmenuIndexKeyboard, bt_hid_submenu_callback, app);
|
||||
submenu_add_item(app->submenu, "Media", BtHidSubmenuIndexMedia, bt_hid_submenu_callback, app);
|
||||
submenu_add_item(app->submenu, "Mouse", BtHidSubmenuIndexMouse, bt_hid_submenu_callback, app);
|
||||
view_set_previous_callback(submenu_get_view(app->submenu), bt_hid_exit);
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, BtHidViewSubmenu, submenu_get_view(app->submenu));
|
||||
|
||||
// Dialog view
|
||||
app->dialog = dialog_ex_alloc();
|
||||
dialog_ex_set_result_callback(app->dialog, bt_hid_dialog_callback);
|
||||
dialog_ex_set_context(app->dialog, app);
|
||||
dialog_ex_set_left_button_text(app->dialog, "Exit");
|
||||
dialog_ex_set_right_button_text(app->dialog, "Stay");
|
||||
dialog_ex_set_center_button_text(app->dialog, "Menu");
|
||||
dialog_ex_set_header(app->dialog, "Close Current App?", 16, 12, AlignLeft, AlignTop);
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, BtHidViewExitConfirm, dialog_ex_get_view(app->dialog));
|
||||
|
||||
// Keynote view
|
||||
app->bt_hid_keynote = bt_hid_keynote_alloc();
|
||||
view_set_previous_callback(
|
||||
bt_hid_keynote_get_view(app->bt_hid_keynote), bt_hid_exit_confirm_view);
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, BtHidViewKeynote, bt_hid_keynote_get_view(app->bt_hid_keynote));
|
||||
|
||||
// Keyboard view
|
||||
app->bt_hid_keyboard = bt_hid_keyboard_alloc();
|
||||
view_set_previous_callback(
|
||||
bt_hid_keyboard_get_view(app->bt_hid_keyboard), bt_hid_exit_confirm_view);
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, BtHidViewKeyboard, bt_hid_keyboard_get_view(app->bt_hid_keyboard));
|
||||
|
||||
// Media view
|
||||
app->bt_hid_media = bt_hid_media_alloc();
|
||||
view_set_previous_callback(bt_hid_media_get_view(app->bt_hid_media), bt_hid_exit_confirm_view);
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, BtHidViewMedia, bt_hid_media_get_view(app->bt_hid_media));
|
||||
|
||||
// Mouse view
|
||||
app->bt_hid_mouse = bt_hid_mouse_alloc();
|
||||
view_set_previous_callback(bt_hid_mouse_get_view(app->bt_hid_mouse), bt_hid_exit_confirm_view);
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, BtHidViewMouse, bt_hid_mouse_get_view(app->bt_hid_mouse));
|
||||
|
||||
// TODO switch to menu after Media is done
|
||||
app->view_id = BtHidViewSubmenu;
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
void bt_hid_app_free(BtHid* app) {
|
||||
furi_assert(app);
|
||||
|
||||
// Reset notification
|
||||
notification_internal_message(app->notifications, &sequence_reset_blue);
|
||||
|
||||
// Free views
|
||||
view_dispatcher_remove_view(app->view_dispatcher, BtHidViewSubmenu);
|
||||
submenu_free(app->submenu);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, BtHidViewExitConfirm);
|
||||
dialog_ex_free(app->dialog);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, BtHidViewKeynote);
|
||||
bt_hid_keynote_free(app->bt_hid_keynote);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, BtHidViewKeyboard);
|
||||
bt_hid_keyboard_free(app->bt_hid_keyboard);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, BtHidViewMedia);
|
||||
bt_hid_media_free(app->bt_hid_media);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, BtHidViewMouse);
|
||||
bt_hid_mouse_free(app->bt_hid_mouse);
|
||||
view_dispatcher_free(app->view_dispatcher);
|
||||
|
||||
// Close records
|
||||
furi_record_close(RECORD_GUI);
|
||||
app->gui = NULL;
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
app->notifications = NULL;
|
||||
furi_record_close(RECORD_BT);
|
||||
app->bt = NULL;
|
||||
|
||||
// Free rest
|
||||
free(app);
|
||||
}
|
||||
|
||||
int32_t bt_hid_app(void* p) {
|
||||
UNUSED(p);
|
||||
// Switch profile to Hid
|
||||
BtHid* app = bt_hid_app_alloc();
|
||||
bt_set_status_changed_callback(app->bt, bt_hid_connection_status_changed_callback, app);
|
||||
// Change profile
|
||||
if(!bt_set_profile(app->bt, BtProfileHidKeyboard)) {
|
||||
FURI_LOG_E(TAG, "Failed to switch profile");
|
||||
bt_hid_app_free(app);
|
||||
return -1;
|
||||
}
|
||||
furi_hal_bt_start_advertising();
|
||||
|
||||
view_dispatcher_run(app->view_dispatcher);
|
||||
|
||||
bt_set_status_changed_callback(app->bt, NULL, NULL);
|
||||
// Change back profile to Serial
|
||||
bt_set_profile(app->bt, BtProfileSerial);
|
||||
|
||||
bt_hid_app_free(app);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <bt/bt_service/bt.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <notification/notification.h>
|
||||
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/modules/dialog_ex.h>
|
||||
#include "views/bt_hid_keynote.h"
|
||||
#include "views/bt_hid_keyboard.h"
|
||||
#include "views/bt_hid_media.h"
|
||||
#include "views/bt_hid_mouse.h"
|
||||
|
||||
typedef struct {
|
||||
Bt* bt;
|
||||
Gui* gui;
|
||||
NotificationApp* notifications;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
Submenu* submenu;
|
||||
DialogEx* dialog;
|
||||
BtHidKeynote* bt_hid_keynote;
|
||||
BtHidKeyboard* bt_hid_keyboard;
|
||||
BtHidMedia* bt_hid_media;
|
||||
BtHidMouse* bt_hid_mouse;
|
||||
uint32_t view_id;
|
||||
} BtHid;
|
||||
|
||||
typedef enum {
|
||||
BtHidViewSubmenu,
|
||||
BtHidViewKeynote,
|
||||
BtHidViewKeyboard,
|
||||
BtHidViewMedia,
|
||||
BtHidViewMouse,
|
||||
BtHidViewExitConfirm,
|
||||
} BtHidView;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 151 B |
@@ -0,0 +1,384 @@
|
||||
#include "bt_hid_keyboard.h"
|
||||
#include <furi.h>
|
||||
#include <furi_hal_bt_hid.h>
|
||||
#include <furi_hal_usb_hid.h>
|
||||
#include <gui/elements.h>
|
||||
#include <gui/icon_i.h>
|
||||
|
||||
struct BtHidKeyboard {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
bool shift;
|
||||
bool alt;
|
||||
bool ctrl;
|
||||
bool gui;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t last_key_code;
|
||||
uint16_t modifier_code;
|
||||
bool ok_pressed;
|
||||
bool back_pressed;
|
||||
bool connected;
|
||||
char key_string[5];
|
||||
} BtHidKeyboardModel;
|
||||
|
||||
typedef struct {
|
||||
uint8_t width;
|
||||
char* key;
|
||||
const Icon* icon;
|
||||
char* shift_key;
|
||||
uint8_t value;
|
||||
} BtHidKeyboardKey;
|
||||
|
||||
typedef struct {
|
||||
int8_t x;
|
||||
int8_t y;
|
||||
} BtHidKeyboardPoint;
|
||||
|
||||
// 4 BY 12
|
||||
#define MARGIN_TOP 0
|
||||
#define MARGIN_LEFT 4
|
||||
#define KEY_WIDTH 9
|
||||
#define KEY_HEIGHT 12
|
||||
#define KEY_PADDING 1
|
||||
#define ROW_COUNT 6
|
||||
#define COLUMN_COUNT 12
|
||||
|
||||
// 0 width items are not drawn, but there value is used
|
||||
const BtHidKeyboardKey bt_hid_keyboard_keyset[ROW_COUNT][COLUMN_COUNT] = {
|
||||
{
|
||||
{.width = 1, .icon = NULL, .key = "1", .shift_key = "!", .value = HID_KEYBOARD_1},
|
||||
{.width = 1, .icon = NULL, .key = "2", .shift_key = "@", .value = HID_KEYBOARD_2},
|
||||
{.width = 1, .icon = NULL, .key = "3", .shift_key = "#", .value = HID_KEYBOARD_3},
|
||||
{.width = 1, .icon = NULL, .key = "4", .shift_key = "$", .value = HID_KEYBOARD_4},
|
||||
{.width = 1, .icon = NULL, .key = "5", .shift_key = "%", .value = HID_KEYBOARD_5},
|
||||
{.width = 1, .icon = NULL, .key = "6", .shift_key = "^", .value = HID_KEYBOARD_6},
|
||||
{.width = 1, .icon = NULL, .key = "7", .shift_key = "&", .value = HID_KEYBOARD_7},
|
||||
{.width = 1, .icon = NULL, .key = "8", .shift_key = "*", .value = HID_KEYBOARD_8},
|
||||
{.width = 1, .icon = NULL, .key = "9", .shift_key = "(", .value = HID_KEYBOARD_9},
|
||||
{.width = 1, .icon = NULL, .key = "0", .shift_key = ")", .value = HID_KEYBOARD_0},
|
||||
{.width = 2, .icon = &I_Pin_arrow_left_9x7, .value = HID_KEYBOARD_DELETE},
|
||||
{.width = 0, .value = HID_KEYBOARD_DELETE},
|
||||
},
|
||||
{
|
||||
{.width = 1, .icon = NULL, .key = "q", .shift_key = "Q", .value = HID_KEYBOARD_Q},
|
||||
{.width = 1, .icon = NULL, .key = "w", .shift_key = "W", .value = HID_KEYBOARD_W},
|
||||
{.width = 1, .icon = NULL, .key = "e", .shift_key = "E", .value = HID_KEYBOARD_E},
|
||||
{.width = 1, .icon = NULL, .key = "r", .shift_key = "R", .value = HID_KEYBOARD_R},
|
||||
{.width = 1, .icon = NULL, .key = "t", .shift_key = "T", .value = HID_KEYBOARD_T},
|
||||
{.width = 1, .icon = NULL, .key = "y", .shift_key = "Y", .value = HID_KEYBOARD_Y},
|
||||
{.width = 1, .icon = NULL, .key = "u", .shift_key = "U", .value = HID_KEYBOARD_U},
|
||||
{.width = 1, .icon = NULL, .key = "i", .shift_key = "I", .value = HID_KEYBOARD_I},
|
||||
{.width = 1, .icon = NULL, .key = "o", .shift_key = "O", .value = HID_KEYBOARD_O},
|
||||
{.width = 1, .icon = NULL, .key = "p", .shift_key = "P", .value = HID_KEYBOARD_P},
|
||||
{.width = 1, .icon = NULL, .key = "[", .shift_key = "{", .value = HID_KEYBOARD_OPEN_BRACKET},
|
||||
{.width = 1,
|
||||
.icon = NULL,
|
||||
.key = "]",
|
||||
.shift_key = "}",
|
||||
.value = HID_KEYBOARD_CLOSE_BRACKET},
|
||||
},
|
||||
{
|
||||
{.width = 1, .icon = NULL, .key = "a", .shift_key = "A", .value = HID_KEYBOARD_A},
|
||||
{.width = 1, .icon = NULL, .key = "s", .shift_key = "S", .value = HID_KEYBOARD_S},
|
||||
{.width = 1, .icon = NULL, .key = "d", .shift_key = "D", .value = HID_KEYBOARD_D},
|
||||
{.width = 1, .icon = NULL, .key = "f", .shift_key = "F", .value = HID_KEYBOARD_F},
|
||||
{.width = 1, .icon = NULL, .key = "g", .shift_key = "G", .value = HID_KEYBOARD_G},
|
||||
{.width = 1, .icon = NULL, .key = "h", .shift_key = "H", .value = HID_KEYBOARD_H},
|
||||
{.width = 1, .icon = NULL, .key = "j", .shift_key = "J", .value = HID_KEYBOARD_J},
|
||||
{.width = 1, .icon = NULL, .key = "k", .shift_key = "K", .value = HID_KEYBOARD_K},
|
||||
{.width = 1, .icon = NULL, .key = "l", .shift_key = "L", .value = HID_KEYBOARD_L},
|
||||
{.width = 1, .icon = NULL, .key = ";", .shift_key = ":", .value = HID_KEYBOARD_SEMICOLON},
|
||||
{.width = 2, .icon = &I_Pin_arrow_right_9x7, .value = HID_KEYBOARD_RETURN},
|
||||
{.width = 0, .value = HID_KEYBOARD_RETURN},
|
||||
},
|
||||
{
|
||||
{.width = 1, .icon = NULL, .key = "z", .shift_key = "Z", .value = HID_KEYBOARD_Z},
|
||||
{.width = 1, .icon = NULL, .key = "x", .shift_key = "X", .value = HID_KEYBOARD_X},
|
||||
{.width = 1, .icon = NULL, .key = "c", .shift_key = "C", .value = HID_KEYBOARD_C},
|
||||
{.width = 1, .icon = NULL, .key = "v", .shift_key = "V", .value = HID_KEYBOARD_V},
|
||||
{.width = 1, .icon = NULL, .key = "b", .shift_key = "B", .value = HID_KEYBOARD_B},
|
||||
{.width = 1, .icon = NULL, .key = "n", .shift_key = "N", .value = HID_KEYBOARD_N},
|
||||
{.width = 1, .icon = NULL, .key = "m", .shift_key = "M", .value = HID_KEYBOARD_M},
|
||||
{.width = 1, .icon = NULL, .key = "/", .shift_key = "?", .value = HID_KEYBOARD_SLASH},
|
||||
{.width = 1, .icon = NULL, .key = "\\", .shift_key = "|", .value = HID_KEYBOARD_BACKSLASH},
|
||||
{.width = 1, .icon = NULL, .key = "`", .shift_key = "~", .value = HID_KEYBOARD_GRAVE_ACCENT},
|
||||
{.width = 1, .icon = &I_ButtonUp_7x4, .value = HID_KEYBOARD_UP_ARROW},
|
||||
{.width = 1, .icon = NULL, .key = "-", .shift_key = "_", .value = HID_KEYBOARD_MINUS},
|
||||
},
|
||||
{
|
||||
{.width = 1, .icon = &I_Pin_arrow_up7x9, .value = HID_KEYBOARD_L_SHIFT},
|
||||
{.width = 1, .icon = NULL, .key = ",", .shift_key = "<", .value = HID_KEYPAD_COMMA},
|
||||
{.width = 1, .icon = NULL, .key = ".", .shift_key = ">", .value = HID_KEYBOARD_DOT},
|
||||
{.width = 4, .icon = NULL, .key = " ", .value = HID_KEYBOARD_SPACEBAR},
|
||||
{.width = 0, .value = HID_KEYBOARD_SPACEBAR},
|
||||
{.width = 0, .value = HID_KEYBOARD_SPACEBAR},
|
||||
{.width = 0, .value = HID_KEYBOARD_SPACEBAR},
|
||||
{.width = 1, .icon = NULL, .key = "'", .shift_key = "\"", .value = HID_KEYBOARD_APOSTROPHE},
|
||||
{.width = 1, .icon = NULL, .key = "=", .shift_key = "+", .value = HID_KEYBOARD_EQUAL_SIGN},
|
||||
{.width = 1, .icon = &I_ButtonLeft_4x7, .value = HID_KEYBOARD_LEFT_ARROW},
|
||||
{.width = 1, .icon = &I_ButtonDown_7x4, .value = HID_KEYBOARD_DOWN_ARROW},
|
||||
{.width = 1, .icon = &I_ButtonRight_4x7, .value = HID_KEYBOARD_RIGHT_ARROW},
|
||||
},
|
||||
{
|
||||
{.width = 3, .icon = NULL, .key = "Ctrl", .value = HID_KEYBOARD_L_CTRL},
|
||||
{.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_CTRL},
|
||||
{.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_CTRL},
|
||||
{.width = 3, .icon = NULL, .key = "Alt", .value = HID_KEYBOARD_L_ALT},
|
||||
{.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_ALT},
|
||||
{.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_ALT},
|
||||
{.width = 3, .icon = NULL, .key = "Cmd", .value = HID_KEYBOARD_L_GUI},
|
||||
{.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_GUI},
|
||||
{.width = 0, .icon = NULL, .value = HID_KEYBOARD_L_GUI},
|
||||
{.width = 3, .icon = NULL, .key = "Tab", .value = HID_KEYBOARD_TAB},
|
||||
{.width = 0, .icon = NULL, .value = HID_KEYBOARD_TAB},
|
||||
{.width = 0, .icon = NULL, .value = HID_KEYBOARD_TAB},
|
||||
},
|
||||
};
|
||||
|
||||
static void bt_hid_keyboard_to_upper(char* str) {
|
||||
while(*str) {
|
||||
*str = toupper((unsigned char)*str);
|
||||
str++;
|
||||
}
|
||||
}
|
||||
|
||||
static void bt_hid_keyboard_draw_key(
|
||||
Canvas* canvas,
|
||||
BtHidKeyboardModel* model,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
BtHidKeyboardKey key,
|
||||
bool selected) {
|
||||
if(!key.width) return;
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
uint8_t keyWidth = KEY_WIDTH * key.width + KEY_PADDING * (key.width - 1);
|
||||
if(selected) {
|
||||
// Draw a filled box
|
||||
elements_slightly_rounded_box(
|
||||
canvas,
|
||||
MARGIN_LEFT + x * (KEY_WIDTH + KEY_PADDING),
|
||||
MARGIN_TOP + y * (KEY_HEIGHT + KEY_PADDING),
|
||||
keyWidth,
|
||||
KEY_HEIGHT);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
} else {
|
||||
// Draw a framed box
|
||||
elements_slightly_rounded_frame(
|
||||
canvas,
|
||||
MARGIN_LEFT + x * (KEY_WIDTH + KEY_PADDING),
|
||||
MARGIN_TOP + y * (KEY_HEIGHT + KEY_PADDING),
|
||||
keyWidth,
|
||||
KEY_HEIGHT);
|
||||
}
|
||||
if(key.icon != NULL) {
|
||||
// Draw the icon centered on the button
|
||||
canvas_draw_icon(
|
||||
canvas,
|
||||
MARGIN_LEFT + x * (KEY_WIDTH + KEY_PADDING) + keyWidth / 2 - key.icon->width / 2,
|
||||
MARGIN_TOP + y * (KEY_HEIGHT + KEY_PADDING) + KEY_HEIGHT / 2 - key.icon->height / 2,
|
||||
key.icon);
|
||||
} else {
|
||||
// If shift is toggled use the shift key when available
|
||||
strcpy(model->key_string, (model->shift && key.shift_key != 0) ? key.shift_key : key.key);
|
||||
// Upper case if ctrl or alt was toggled true
|
||||
if((model->ctrl && key.value == HID_KEYBOARD_L_CTRL) ||
|
||||
(model->alt && key.value == HID_KEYBOARD_L_ALT) ||
|
||||
(model->gui && key.value == HID_KEYBOARD_L_GUI)) {
|
||||
bt_hid_keyboard_to_upper(model->key_string);
|
||||
}
|
||||
canvas_draw_str_aligned(
|
||||
canvas,
|
||||
MARGIN_LEFT + x * (KEY_WIDTH + KEY_PADDING) + keyWidth / 2 + 1,
|
||||
MARGIN_TOP + y * (KEY_HEIGHT + KEY_PADDING) + KEY_HEIGHT / 2,
|
||||
AlignCenter,
|
||||
AlignCenter,
|
||||
model->key_string);
|
||||
}
|
||||
}
|
||||
|
||||
static void bt_hid_keyboard_draw_callback(Canvas* canvas, void* context) {
|
||||
furi_assert(context);
|
||||
BtHidKeyboardModel* model = context;
|
||||
|
||||
// Header
|
||||
if(!model->connected) {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Keyboard");
|
||||
elements_multiline_text_aligned(
|
||||
canvas, 4, 60, AlignLeft, AlignBottom, "Waiting for Connection...");
|
||||
return; // Dont render the keyboard if we are not yet connected
|
||||
}
|
||||
|
||||
canvas_set_font(canvas, FontKeyboard);
|
||||
// Start shifting the all keys up if on the next row (Scrolling)
|
||||
uint8_t initY = model->y - 4 > 0 ? model->y - 4 : 0;
|
||||
for(uint8_t y = initY; y < ROW_COUNT; y++) {
|
||||
const BtHidKeyboardKey* keyboardKeyRow = bt_hid_keyboard_keyset[y];
|
||||
uint8_t x = 0;
|
||||
for(uint8_t i = 0; i < COLUMN_COUNT; i++) {
|
||||
BtHidKeyboardKey key = keyboardKeyRow[i];
|
||||
// Select when the button is hovered
|
||||
// Select if the button is hovered within its width
|
||||
// Select if back is clicked and its the backspace key
|
||||
// Deselect when the button clicked or not hovered
|
||||
bool keySelected = (x <= model->x && model->x < (x + key.width)) && y == model->y;
|
||||
bool backSelected = model->back_pressed && key.value == HID_KEYBOARD_DELETE;
|
||||
bt_hid_keyboard_draw_key(
|
||||
canvas,
|
||||
model,
|
||||
x,
|
||||
y - initY,
|
||||
key,
|
||||
(!model->ok_pressed && keySelected) || backSelected);
|
||||
x += key.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t bt_hid_keyboard_get_selected_key(BtHidKeyboardModel* model) {
|
||||
BtHidKeyboardKey key = bt_hid_keyboard_keyset[model->y][model->x];
|
||||
// Use upper case if shift is toggled
|
||||
bool useUppercase = model->shift;
|
||||
// Check if the key has an upper case version
|
||||
bool hasUppercase = key.shift_key != 0;
|
||||
if(useUppercase && hasUppercase)
|
||||
return key.value;
|
||||
else
|
||||
return key.value;
|
||||
}
|
||||
|
||||
static void bt_hid_keyboard_get_select_key(BtHidKeyboardModel* model, BtHidKeyboardPoint delta) {
|
||||
// Keep going until a valid spot is found, this allows for nulls and zero width keys in the map
|
||||
do {
|
||||
if(((int8_t)model->y) + delta.y < 0)
|
||||
model->y = ROW_COUNT - 1;
|
||||
else
|
||||
model->y = (model->y + delta.y) % ROW_COUNT;
|
||||
} while(delta.y != 0 && bt_hid_keyboard_keyset[model->y][model->x].value == 0);
|
||||
|
||||
do {
|
||||
if(((int8_t)model->x) + delta.x < 0)
|
||||
model->x = COLUMN_COUNT - 1;
|
||||
else
|
||||
model->x = (model->x + delta.x) % COLUMN_COUNT;
|
||||
} while(delta.x != 0 && bt_hid_keyboard_keyset[model->y][model->x].width ==
|
||||
0); // Skip zero width keys, pretend they are one key
|
||||
}
|
||||
|
||||
static void bt_hid_keyboard_process(BtHidKeyboard* bt_hid_keyboard, InputEvent* event) {
|
||||
with_view_model(
|
||||
bt_hid_keyboard->view, (BtHidKeyboardModel * model) {
|
||||
if(event->key == InputKeyOk) {
|
||||
if(event->type == InputTypePress) {
|
||||
model->ok_pressed = true;
|
||||
} else if(event->type == InputTypeLong || event->type == InputTypeShort) {
|
||||
model->last_key_code = bt_hid_keyboard_get_selected_key(model);
|
||||
|
||||
// Toggle the modifier key when clicked, and click the key
|
||||
if(model->last_key_code == HID_KEYBOARD_L_SHIFT) {
|
||||
model->shift = !model->shift;
|
||||
if(model->shift)
|
||||
model->modifier_code |= KEY_MOD_LEFT_SHIFT;
|
||||
else
|
||||
model->modifier_code &= ~KEY_MOD_LEFT_SHIFT;
|
||||
} else if(model->last_key_code == HID_KEYBOARD_L_ALT) {
|
||||
model->alt = !model->alt;
|
||||
if(model->alt)
|
||||
model->modifier_code |= KEY_MOD_LEFT_ALT;
|
||||
else
|
||||
model->modifier_code &= ~KEY_MOD_LEFT_ALT;
|
||||
} else if(model->last_key_code == HID_KEYBOARD_L_CTRL) {
|
||||
model->ctrl = !model->ctrl;
|
||||
if(model->ctrl)
|
||||
model->modifier_code |= KEY_MOD_LEFT_CTRL;
|
||||
else
|
||||
model->modifier_code &= ~KEY_MOD_LEFT_CTRL;
|
||||
} else if(model->last_key_code == HID_KEYBOARD_L_GUI) {
|
||||
model->gui = !model->gui;
|
||||
if(model->gui)
|
||||
model->modifier_code |= KEY_MOD_LEFT_GUI;
|
||||
else
|
||||
model->modifier_code &= ~KEY_MOD_LEFT_GUI;
|
||||
}
|
||||
furi_hal_bt_hid_kb_press(model->modifier_code | model->last_key_code);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
// Release happens after short and long presses
|
||||
furi_hal_bt_hid_kb_release(model->modifier_code | model->last_key_code);
|
||||
model->ok_pressed = false;
|
||||
}
|
||||
} else if(event->key == InputKeyBack) {
|
||||
// If back is pressed for a short time, backspace
|
||||
if(event->type == InputTypePress) {
|
||||
model->back_pressed = true;
|
||||
} else if(event->type == InputTypeShort) {
|
||||
furi_hal_bt_hid_kb_press(HID_KEYBOARD_DELETE);
|
||||
furi_hal_bt_hid_kb_release(HID_KEYBOARD_DELETE);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
model->back_pressed = false;
|
||||
}
|
||||
} else if(event->type == InputTypePress || event->type == InputTypeRepeat) {
|
||||
// Cycle the selected keys
|
||||
if(event->key == InputKeyUp) {
|
||||
bt_hid_keyboard_get_select_key(model, (BtHidKeyboardPoint){.x = 0, .y = -1});
|
||||
} else if(event->key == InputKeyDown) {
|
||||
bt_hid_keyboard_get_select_key(model, (BtHidKeyboardPoint){.x = 0, .y = 1});
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
bt_hid_keyboard_get_select_key(model, (BtHidKeyboardPoint){.x = -1, .y = 0});
|
||||
} else if(event->key == InputKeyRight) {
|
||||
bt_hid_keyboard_get_select_key(model, (BtHidKeyboardPoint){.x = 1, .y = 0});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static bool bt_hid_keyboard_input_callback(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
BtHidKeyboard* bt_hid_keyboard = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypeLong && event->key == InputKeyBack) {
|
||||
furi_hal_bt_hid_kb_release_all();
|
||||
} else {
|
||||
bt_hid_keyboard_process(bt_hid_keyboard, event);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
BtHidKeyboard* bt_hid_keyboard_alloc() {
|
||||
BtHidKeyboard* bt_hid_keyboard = malloc(sizeof(BtHidKeyboard));
|
||||
bt_hid_keyboard->view = view_alloc();
|
||||
view_set_context(bt_hid_keyboard->view, bt_hid_keyboard);
|
||||
view_allocate_model(bt_hid_keyboard->view, ViewModelTypeLocking, sizeof(BtHidKeyboardModel));
|
||||
view_set_draw_callback(bt_hid_keyboard->view, bt_hid_keyboard_draw_callback);
|
||||
view_set_input_callback(bt_hid_keyboard->view, bt_hid_keyboard_input_callback);
|
||||
|
||||
return bt_hid_keyboard;
|
||||
}
|
||||
|
||||
void bt_hid_keyboard_free(BtHidKeyboard* bt_hid_keyboard) {
|
||||
furi_assert(bt_hid_keyboard);
|
||||
view_free(bt_hid_keyboard->view);
|
||||
free(bt_hid_keyboard);
|
||||
}
|
||||
|
||||
View* bt_hid_keyboard_get_view(BtHidKeyboard* bt_hid_keyboard) {
|
||||
furi_assert(bt_hid_keyboard);
|
||||
return bt_hid_keyboard->view;
|
||||
}
|
||||
|
||||
void bt_hid_keyboard_set_connected_status(BtHidKeyboard* bt_hid_keyboard, bool connected) {
|
||||
furi_assert(bt_hid_keyboard);
|
||||
with_view_model(
|
||||
bt_hid_keyboard->view, (BtHidKeyboardModel * model) {
|
||||
model->connected = connected;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
typedef struct BtHidKeyboard BtHidKeyboard;
|
||||
|
||||
BtHidKeyboard* bt_hid_keyboard_alloc();
|
||||
|
||||
void bt_hid_keyboard_free(BtHidKeyboard* bt_hid_keyboard);
|
||||
|
||||
View* bt_hid_keyboard_get_view(BtHidKeyboard* bt_hid_keyboard);
|
||||
|
||||
void bt_hid_keyboard_set_connected_status(BtHidKeyboard* bt_hid_keyboard, bool connected);
|
||||
@@ -0,0 +1,204 @@
|
||||
#include "bt_hid_keynote.h"
|
||||
#include <furi.h>
|
||||
#include <furi_hal_bt_hid.h>
|
||||
#include <furi_hal_usb_hid.h>
|
||||
#include <gui/elements.h>
|
||||
|
||||
struct BtHidKeynote {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
bool left_pressed;
|
||||
bool up_pressed;
|
||||
bool right_pressed;
|
||||
bool down_pressed;
|
||||
bool ok_pressed;
|
||||
bool back_pressed;
|
||||
bool connected;
|
||||
} BtHidKeynoteModel;
|
||||
|
||||
static void bt_hid_keynote_draw_arrow(Canvas* canvas, uint8_t x, uint8_t y, CanvasDirection dir) {
|
||||
canvas_draw_triangle(canvas, x, y, 5, 3, dir);
|
||||
if(dir == CanvasDirectionBottomToTop) {
|
||||
canvas_draw_line(canvas, x, y + 6, x, y - 1);
|
||||
} else if(dir == CanvasDirectionTopToBottom) {
|
||||
canvas_draw_line(canvas, x, y - 6, x, y + 1);
|
||||
} else if(dir == CanvasDirectionRightToLeft) {
|
||||
canvas_draw_line(canvas, x + 6, y, x - 1, y);
|
||||
} else if(dir == CanvasDirectionLeftToRight) {
|
||||
canvas_draw_line(canvas, x - 6, y, x + 1, y);
|
||||
}
|
||||
}
|
||||
|
||||
static void bt_hid_keynote_draw_callback(Canvas* canvas, void* context) {
|
||||
furi_assert(context);
|
||||
BtHidKeynoteModel* model = context;
|
||||
|
||||
// Header
|
||||
if(model->connected) {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15);
|
||||
}
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Keynote");
|
||||
|
||||
canvas_draw_icon(canvas, 68, 2, &I_Pin_back_arrow_10x8);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
elements_multiline_text_aligned(canvas, 127, 3, AlignRight, AlignTop, "Hold to exit");
|
||||
|
||||
// Up
|
||||
canvas_draw_icon(canvas, 21, 24, &I_Button_18x18);
|
||||
if(model->up_pressed) {
|
||||
elements_slightly_rounded_box(canvas, 24, 26, 13, 13);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
bt_hid_keynote_draw_arrow(canvas, 30, 30, CanvasDirectionBottomToTop);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Down
|
||||
canvas_draw_icon(canvas, 21, 45, &I_Button_18x18);
|
||||
if(model->down_pressed) {
|
||||
elements_slightly_rounded_box(canvas, 24, 47, 13, 13);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
bt_hid_keynote_draw_arrow(canvas, 30, 55, CanvasDirectionTopToBottom);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Left
|
||||
canvas_draw_icon(canvas, 0, 45, &I_Button_18x18);
|
||||
if(model->left_pressed) {
|
||||
elements_slightly_rounded_box(canvas, 3, 47, 13, 13);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
bt_hid_keynote_draw_arrow(canvas, 7, 53, CanvasDirectionRightToLeft);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Right
|
||||
canvas_draw_icon(canvas, 42, 45, &I_Button_18x18);
|
||||
if(model->right_pressed) {
|
||||
elements_slightly_rounded_box(canvas, 45, 47, 13, 13);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
bt_hid_keynote_draw_arrow(canvas, 53, 53, CanvasDirectionLeftToRight);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Ok
|
||||
canvas_draw_icon(canvas, 63, 25, &I_Space_65x18);
|
||||
if(model->ok_pressed) {
|
||||
elements_slightly_rounded_box(canvas, 66, 27, 60, 13);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_icon(canvas, 74, 29, &I_Ok_btn_9x9);
|
||||
elements_multiline_text_aligned(canvas, 91, 36, AlignLeft, AlignBottom, "Space");
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Back
|
||||
canvas_draw_icon(canvas, 63, 45, &I_Space_65x18);
|
||||
if(model->back_pressed) {
|
||||
elements_slightly_rounded_box(canvas, 66, 47, 60, 13);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_icon(canvas, 74, 49, &I_Pin_back_arrow_10x8);
|
||||
elements_multiline_text_aligned(canvas, 91, 57, AlignLeft, AlignBottom, "Back");
|
||||
}
|
||||
|
||||
static void bt_hid_keynote_process(BtHidKeynote* bt_hid_keynote, InputEvent* event) {
|
||||
with_view_model(
|
||||
bt_hid_keynote->view, (BtHidKeynoteModel * model) {
|
||||
if(event->type == InputTypePress) {
|
||||
if(event->key == InputKeyUp) {
|
||||
model->up_pressed = true;
|
||||
furi_hal_bt_hid_kb_press(HID_KEYBOARD_UP_ARROW);
|
||||
} else if(event->key == InputKeyDown) {
|
||||
model->down_pressed = true;
|
||||
furi_hal_bt_hid_kb_press(HID_KEYBOARD_DOWN_ARROW);
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
model->left_pressed = true;
|
||||
furi_hal_bt_hid_kb_press(HID_KEYBOARD_LEFT_ARROW);
|
||||
} else if(event->key == InputKeyRight) {
|
||||
model->right_pressed = true;
|
||||
furi_hal_bt_hid_kb_press(HID_KEYBOARD_RIGHT_ARROW);
|
||||
} else if(event->key == InputKeyOk) {
|
||||
model->ok_pressed = true;
|
||||
furi_hal_bt_hid_kb_press(HID_KEYBOARD_SPACEBAR);
|
||||
} else if(event->key == InputKeyBack) {
|
||||
model->back_pressed = true;
|
||||
}
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
if(event->key == InputKeyUp) {
|
||||
model->up_pressed = false;
|
||||
furi_hal_bt_hid_kb_release(HID_KEYBOARD_UP_ARROW);
|
||||
} else if(event->key == InputKeyDown) {
|
||||
model->down_pressed = false;
|
||||
furi_hal_bt_hid_kb_release(HID_KEYBOARD_DOWN_ARROW);
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
model->left_pressed = false;
|
||||
furi_hal_bt_hid_kb_release(HID_KEYBOARD_LEFT_ARROW);
|
||||
} else if(event->key == InputKeyRight) {
|
||||
model->right_pressed = false;
|
||||
furi_hal_bt_hid_kb_release(HID_KEYBOARD_RIGHT_ARROW);
|
||||
} else if(event->key == InputKeyOk) {
|
||||
model->ok_pressed = false;
|
||||
furi_hal_bt_hid_kb_release(HID_KEYBOARD_SPACEBAR);
|
||||
} else if(event->key == InputKeyBack) {
|
||||
model->back_pressed = false;
|
||||
}
|
||||
} else if(event->type == InputTypeShort) {
|
||||
if(event->key == InputKeyBack) {
|
||||
furi_hal_bt_hid_kb_press(HID_KEYBOARD_DELETE);
|
||||
furi_hal_bt_hid_kb_release(HID_KEYBOARD_DELETE);
|
||||
furi_hal_bt_hid_consumer_key_press(HID_CONSUMER_AC_BACK);
|
||||
furi_hal_bt_hid_consumer_key_release(HID_CONSUMER_AC_BACK);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static bool bt_hid_keynote_input_callback(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
BtHidKeynote* bt_hid_keynote = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypeLong && event->key == InputKeyBack) {
|
||||
furi_hal_bt_hid_kb_release_all();
|
||||
} else {
|
||||
bt_hid_keynote_process(bt_hid_keynote, event);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
BtHidKeynote* bt_hid_keynote_alloc() {
|
||||
BtHidKeynote* bt_hid_keynote = malloc(sizeof(BtHidKeynote));
|
||||
bt_hid_keynote->view = view_alloc();
|
||||
view_set_context(bt_hid_keynote->view, bt_hid_keynote);
|
||||
view_allocate_model(bt_hid_keynote->view, ViewModelTypeLocking, sizeof(BtHidKeynoteModel));
|
||||
view_set_draw_callback(bt_hid_keynote->view, bt_hid_keynote_draw_callback);
|
||||
view_set_input_callback(bt_hid_keynote->view, bt_hid_keynote_input_callback);
|
||||
|
||||
return bt_hid_keynote;
|
||||
}
|
||||
|
||||
void bt_hid_keynote_free(BtHidKeynote* bt_hid_keynote) {
|
||||
furi_assert(bt_hid_keynote);
|
||||
view_free(bt_hid_keynote->view);
|
||||
free(bt_hid_keynote);
|
||||
}
|
||||
|
||||
View* bt_hid_keynote_get_view(BtHidKeynote* bt_hid_keynote) {
|
||||
furi_assert(bt_hid_keynote);
|
||||
return bt_hid_keynote->view;
|
||||
}
|
||||
|
||||
void bt_hid_keynote_set_connected_status(BtHidKeynote* bt_hid_keynote, bool connected) {
|
||||
furi_assert(bt_hid_keynote);
|
||||
with_view_model(
|
||||
bt_hid_keynote->view, (BtHidKeynoteModel * model) {
|
||||
model->connected = connected;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
typedef struct BtHidKeynote BtHidKeynote;
|
||||
|
||||
BtHidKeynote* bt_hid_keynote_alloc();
|
||||
|
||||
void bt_hid_keynote_free(BtHidKeynote* bt_hid_keynote);
|
||||
|
||||
View* bt_hid_keynote_get_view(BtHidKeynote* bt_hid_keynote);
|
||||
|
||||
void bt_hid_keynote_set_connected_status(BtHidKeynote* bt_hid_keynote, bool connected);
|
||||
@@ -0,0 +1,203 @@
|
||||
#include "bt_hid_media.h"
|
||||
#include <furi.h>
|
||||
#include <furi_hal_bt_hid.h>
|
||||
#include <furi_hal_usb_hid.h>
|
||||
#include <gui/elements.h>
|
||||
|
||||
struct BtHidMedia {
|
||||
View* view;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
bool left_pressed;
|
||||
bool up_pressed;
|
||||
bool right_pressed;
|
||||
bool down_pressed;
|
||||
bool ok_pressed;
|
||||
bool connected;
|
||||
} BtHidMediaModel;
|
||||
|
||||
static void bt_hid_media_draw_arrow(Canvas* canvas, uint8_t x, uint8_t y, CanvasDirection dir) {
|
||||
canvas_draw_triangle(canvas, x, y, 5, 3, dir);
|
||||
if(dir == CanvasDirectionBottomToTop) {
|
||||
canvas_draw_dot(canvas, x, y - 1);
|
||||
} else if(dir == CanvasDirectionTopToBottom) {
|
||||
canvas_draw_dot(canvas, x, y + 1);
|
||||
} else if(dir == CanvasDirectionRightToLeft) {
|
||||
canvas_draw_dot(canvas, x - 1, y);
|
||||
} else if(dir == CanvasDirectionLeftToRight) {
|
||||
canvas_draw_dot(canvas, x + 1, y);
|
||||
}
|
||||
}
|
||||
|
||||
static void bt_hid_media_draw_callback(Canvas* canvas, void* context) {
|
||||
furi_assert(context);
|
||||
BtHidMediaModel* model = context;
|
||||
|
||||
// Header
|
||||
if(model->connected) {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15);
|
||||
}
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Media");
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
// Keypad circles
|
||||
canvas_draw_icon(canvas, 76, 8, &I_Circles_47x47);
|
||||
|
||||
// Up
|
||||
if(model->up_pressed) {
|
||||
canvas_set_bitmap_mode(canvas, 1);
|
||||
canvas_draw_icon(canvas, 93, 9, &I_Pressed_Button_13x13);
|
||||
canvas_set_bitmap_mode(canvas, 0);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_icon(canvas, 96, 12, &I_Volup_8x6);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Down
|
||||
if(model->down_pressed) {
|
||||
canvas_set_bitmap_mode(canvas, 1);
|
||||
canvas_draw_icon(canvas, 93, 41, &I_Pressed_Button_13x13);
|
||||
canvas_set_bitmap_mode(canvas, 0);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_icon(canvas, 96, 45, &I_Voldwn_6x6);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Left
|
||||
if(model->left_pressed) {
|
||||
canvas_set_bitmap_mode(canvas, 1);
|
||||
canvas_draw_icon(canvas, 77, 25, &I_Pressed_Button_13x13);
|
||||
canvas_set_bitmap_mode(canvas, 0);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
bt_hid_media_draw_arrow(canvas, 82, 31, CanvasDirectionRightToLeft);
|
||||
bt_hid_media_draw_arrow(canvas, 86, 31, CanvasDirectionRightToLeft);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Right
|
||||
if(model->right_pressed) {
|
||||
canvas_set_bitmap_mode(canvas, 1);
|
||||
canvas_draw_icon(canvas, 109, 25, &I_Pressed_Button_13x13);
|
||||
canvas_set_bitmap_mode(canvas, 0);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
bt_hid_media_draw_arrow(canvas, 112, 31, CanvasDirectionLeftToRight);
|
||||
bt_hid_media_draw_arrow(canvas, 116, 31, CanvasDirectionLeftToRight);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Ok
|
||||
if(model->ok_pressed) {
|
||||
canvas_draw_icon(canvas, 93, 25, &I_Pressed_Button_13x13);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
bt_hid_media_draw_arrow(canvas, 96, 31, CanvasDirectionLeftToRight);
|
||||
canvas_draw_line(canvas, 100, 29, 100, 33);
|
||||
canvas_draw_line(canvas, 102, 29, 102, 33);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Exit
|
||||
canvas_draw_icon(canvas, 0, 54, &I_Pin_back_arrow_10x8);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
elements_multiline_text_aligned(canvas, 13, 62, AlignLeft, AlignBottom, "Hold to exit");
|
||||
}
|
||||
|
||||
static void bt_hid_media_process_press(BtHidMedia* bt_hid_media, InputEvent* event) {
|
||||
with_view_model(
|
||||
bt_hid_media->view, (BtHidMediaModel * model) {
|
||||
if(event->key == InputKeyUp) {
|
||||
model->up_pressed = true;
|
||||
furi_hal_bt_hid_consumer_key_press(HID_CONSUMER_VOLUME_INCREMENT);
|
||||
} else if(event->key == InputKeyDown) {
|
||||
model->down_pressed = true;
|
||||
furi_hal_bt_hid_consumer_key_press(HID_CONSUMER_VOLUME_DECREMENT);
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
model->left_pressed = true;
|
||||
furi_hal_bt_hid_consumer_key_press(HID_CONSUMER_SCAN_PREVIOUS_TRACK);
|
||||
} else if(event->key == InputKeyRight) {
|
||||
model->right_pressed = true;
|
||||
furi_hal_bt_hid_consumer_key_press(HID_CONSUMER_SCAN_NEXT_TRACK);
|
||||
} else if(event->key == InputKeyOk) {
|
||||
model->ok_pressed = true;
|
||||
furi_hal_bt_hid_consumer_key_press(HID_CONSUMER_PLAY_PAUSE);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static void bt_hid_media_process_release(BtHidMedia* bt_hid_media, InputEvent* event) {
|
||||
with_view_model(
|
||||
bt_hid_media->view, (BtHidMediaModel * model) {
|
||||
if(event->key == InputKeyUp) {
|
||||
model->up_pressed = false;
|
||||
furi_hal_bt_hid_consumer_key_release(HID_CONSUMER_VOLUME_INCREMENT);
|
||||
} else if(event->key == InputKeyDown) {
|
||||
model->down_pressed = false;
|
||||
furi_hal_bt_hid_consumer_key_release(HID_CONSUMER_VOLUME_DECREMENT);
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
model->left_pressed = false;
|
||||
furi_hal_bt_hid_consumer_key_release(HID_CONSUMER_SCAN_PREVIOUS_TRACK);
|
||||
} else if(event->key == InputKeyRight) {
|
||||
model->right_pressed = false;
|
||||
furi_hal_bt_hid_consumer_key_release(HID_CONSUMER_SCAN_NEXT_TRACK);
|
||||
} else if(event->key == InputKeyOk) {
|
||||
model->ok_pressed = false;
|
||||
furi_hal_bt_hid_consumer_key_release(HID_CONSUMER_PLAY_PAUSE);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static bool bt_hid_media_input_callback(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
BtHidMedia* bt_hid_media = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypePress) {
|
||||
bt_hid_media_process_press(bt_hid_media, event);
|
||||
consumed = true;
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
bt_hid_media_process_release(bt_hid_media, event);
|
||||
consumed = true;
|
||||
} else if(event->type == InputTypeShort) {
|
||||
if(event->key == InputKeyBack) {
|
||||
furi_hal_bt_hid_consumer_key_release_all();
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
BtHidMedia* bt_hid_media_alloc() {
|
||||
BtHidMedia* bt_hid_media = malloc(sizeof(BtHidMedia));
|
||||
bt_hid_media->view = view_alloc();
|
||||
view_set_context(bt_hid_media->view, bt_hid_media);
|
||||
view_allocate_model(bt_hid_media->view, ViewModelTypeLocking, sizeof(BtHidMediaModel));
|
||||
view_set_draw_callback(bt_hid_media->view, bt_hid_media_draw_callback);
|
||||
view_set_input_callback(bt_hid_media->view, bt_hid_media_input_callback);
|
||||
|
||||
return bt_hid_media;
|
||||
}
|
||||
|
||||
void bt_hid_media_free(BtHidMedia* bt_hid_media) {
|
||||
furi_assert(bt_hid_media);
|
||||
view_free(bt_hid_media->view);
|
||||
free(bt_hid_media);
|
||||
}
|
||||
|
||||
View* bt_hid_media_get_view(BtHidMedia* bt_hid_media) {
|
||||
furi_assert(bt_hid_media);
|
||||
return bt_hid_media->view;
|
||||
}
|
||||
|
||||
void bt_hid_media_set_connected_status(BtHidMedia* bt_hid_media, bool connected) {
|
||||
furi_assert(bt_hid_media);
|
||||
with_view_model(
|
||||
bt_hid_media->view, (BtHidMediaModel * model) {
|
||||
model->connected = connected;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
typedef struct BtHidMedia BtHidMedia;
|
||||
|
||||
BtHidMedia* bt_hid_media_alloc();
|
||||
|
||||
void bt_hid_media_free(BtHidMedia* bt_hid_media);
|
||||
|
||||
View* bt_hid_media_get_view(BtHidMedia* bt_hid_media);
|
||||
|
||||
void bt_hid_media_set_connected_status(BtHidMedia* bt_hid_media, bool connected);
|
||||
@@ -0,0 +1,218 @@
|
||||
#include "bt_hid_mouse.h"
|
||||
#include <furi.h>
|
||||
#include <furi_hal_bt_hid.h>
|
||||
#include <furi_hal_usb_hid.h>
|
||||
#include <gui/elements.h>
|
||||
|
||||
struct BtHidMouse {
|
||||
View* view;
|
||||
};
|
||||
#define MOUSE_MOVE_SHORT 5
|
||||
#define MOUSE_MOVE_LONG 20
|
||||
|
||||
typedef struct {
|
||||
bool left_pressed;
|
||||
bool up_pressed;
|
||||
bool right_pressed;
|
||||
bool down_pressed;
|
||||
bool left_mouse_pressed;
|
||||
bool left_mouse_held;
|
||||
bool right_mouse_pressed;
|
||||
bool connected;
|
||||
} BtHidMouseModel;
|
||||
|
||||
static void bt_hid_mouse_draw_callback(Canvas* canvas, void* context) {
|
||||
furi_assert(context);
|
||||
BtHidMouseModel* model = context;
|
||||
|
||||
// Header
|
||||
if(model->connected) {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15);
|
||||
}
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
elements_multiline_text_aligned(canvas, 17, 3, AlignLeft, AlignTop, "Mouse");
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
if(model->left_mouse_held == true) {
|
||||
elements_multiline_text_aligned(canvas, 0, 62, AlignLeft, AlignBottom, "Selecting...");
|
||||
} else {
|
||||
canvas_draw_icon(canvas, 0, 54, &I_Pin_back_arrow_10x8);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
elements_multiline_text_aligned(canvas, 13, 62, AlignLeft, AlignBottom, "Hold to exit");
|
||||
}
|
||||
|
||||
// Keypad circles
|
||||
canvas_draw_icon(canvas, 64, 8, &I_Circles_47x47);
|
||||
|
||||
// Up
|
||||
if(model->up_pressed) {
|
||||
canvas_set_bitmap_mode(canvas, 1);
|
||||
canvas_draw_icon(canvas, 81, 9, &I_Pressed_Button_13x13);
|
||||
canvas_set_bitmap_mode(canvas, 0);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_icon(canvas, 84, 10, &I_Pin_arrow_up7x9);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Down
|
||||
if(model->down_pressed) {
|
||||
canvas_set_bitmap_mode(canvas, 1);
|
||||
canvas_draw_icon(canvas, 81, 41, &I_Pressed_Button_13x13);
|
||||
canvas_set_bitmap_mode(canvas, 0);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_icon(canvas, 84, 43, &I_Pin_arrow_down_7x9);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Left
|
||||
if(model->left_pressed) {
|
||||
canvas_set_bitmap_mode(canvas, 1);
|
||||
canvas_draw_icon(canvas, 65, 25, &I_Pressed_Button_13x13);
|
||||
canvas_set_bitmap_mode(canvas, 0);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_icon(canvas, 67, 28, &I_Pin_arrow_left_9x7);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Right
|
||||
if(model->right_pressed) {
|
||||
canvas_set_bitmap_mode(canvas, 1);
|
||||
canvas_draw_icon(canvas, 97, 25, &I_Pressed_Button_13x13);
|
||||
canvas_set_bitmap_mode(canvas, 0);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_icon(canvas, 99, 28, &I_Pin_arrow_right_9x7);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
// Ok
|
||||
if(model->left_mouse_pressed) {
|
||||
canvas_draw_icon(canvas, 81, 25, &I_Ok_btn_pressed_13x13);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, 83, 27, &I_Left_mouse_icon_9x9);
|
||||
}
|
||||
|
||||
// Back
|
||||
if(model->right_mouse_pressed) {
|
||||
canvas_draw_icon(canvas, 108, 48, &I_Ok_btn_pressed_13x13);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, 110, 50, &I_Right_mouse_icon_9x9);
|
||||
}
|
||||
}
|
||||
|
||||
static void bt_hid_mouse_process(BtHidMouse* bt_hid_mouse, InputEvent* event) {
|
||||
with_view_model(
|
||||
bt_hid_mouse->view, (BtHidMouseModel * model) {
|
||||
if(event->key == InputKeyBack) {
|
||||
if(event->type == InputTypeShort) {
|
||||
furi_hal_bt_hid_mouse_press(HID_MOUSE_BTN_RIGHT);
|
||||
furi_hal_bt_hid_mouse_release(HID_MOUSE_BTN_RIGHT);
|
||||
} else if(event->type == InputTypePress) {
|
||||
model->right_mouse_pressed = true;
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
model->right_mouse_pressed = false;
|
||||
}
|
||||
} else if(event->key == InputKeyOk) {
|
||||
if(event->type == InputTypeShort) {
|
||||
// Just release if it was being held before
|
||||
if(!model->left_mouse_held) furi_hal_bt_hid_mouse_press(HID_MOUSE_BTN_LEFT);
|
||||
furi_hal_bt_hid_mouse_release(HID_MOUSE_BTN_LEFT);
|
||||
model->left_mouse_held = false;
|
||||
} else if(event->type == InputTypeLong) {
|
||||
furi_hal_bt_hid_mouse_press(HID_MOUSE_BTN_LEFT);
|
||||
model->left_mouse_held = true;
|
||||
model->left_mouse_pressed = true;
|
||||
} else if(event->type == InputTypePress) {
|
||||
model->left_mouse_pressed = true;
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
// Only release if it wasn't a long press
|
||||
if(!model->left_mouse_held) model->left_mouse_pressed = false;
|
||||
}
|
||||
|
||||
} else if(event->key == InputKeyRight) {
|
||||
if(event->type == InputTypePress) {
|
||||
model->right_pressed = true;
|
||||
furi_hal_bt_hid_mouse_move(MOUSE_MOVE_SHORT, 0);
|
||||
} else if(event->type == InputTypeRepeat) {
|
||||
furi_hal_bt_hid_mouse_move(MOUSE_MOVE_LONG, 0);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
model->right_pressed = false;
|
||||
}
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
if(event->type == InputTypePress) {
|
||||
model->left_pressed = true;
|
||||
furi_hal_bt_hid_mouse_move(-MOUSE_MOVE_SHORT, 0);
|
||||
} else if(event->type == InputTypeRepeat) {
|
||||
furi_hal_bt_hid_mouse_move(-MOUSE_MOVE_LONG, 0);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
model->left_pressed = false;
|
||||
}
|
||||
} else if(event->key == InputKeyDown) {
|
||||
if(event->type == InputTypePress) {
|
||||
model->down_pressed = true;
|
||||
furi_hal_bt_hid_mouse_move(0, MOUSE_MOVE_SHORT);
|
||||
} else if(event->type == InputTypeRepeat) {
|
||||
furi_hal_bt_hid_mouse_move(0, MOUSE_MOVE_LONG);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
model->down_pressed = false;
|
||||
}
|
||||
} else if(event->key == InputKeyUp) {
|
||||
if(event->type == InputTypePress) {
|
||||
model->up_pressed = true;
|
||||
furi_hal_bt_hid_mouse_move(0, -MOUSE_MOVE_SHORT);
|
||||
} else if(event->type == InputTypeRepeat) {
|
||||
furi_hal_bt_hid_mouse_move(0, -MOUSE_MOVE_LONG);
|
||||
} else if(event->type == InputTypeRelease) {
|
||||
model->up_pressed = false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
static bool bt_hid_mouse_input_callback(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
BtHidMouse* bt_hid_mouse = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event->type == InputTypeLong && event->key == InputKeyBack) {
|
||||
furi_hal_bt_hid_mouse_release_all();
|
||||
} else {
|
||||
bt_hid_mouse_process(bt_hid_mouse, event);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
BtHidMouse* bt_hid_mouse_alloc() {
|
||||
BtHidMouse* bt_hid_mouse = malloc(sizeof(BtHidMouse));
|
||||
bt_hid_mouse->view = view_alloc();
|
||||
view_set_context(bt_hid_mouse->view, bt_hid_mouse);
|
||||
view_allocate_model(bt_hid_mouse->view, ViewModelTypeLocking, sizeof(BtHidMouseModel));
|
||||
view_set_draw_callback(bt_hid_mouse->view, bt_hid_mouse_draw_callback);
|
||||
view_set_input_callback(bt_hid_mouse->view, bt_hid_mouse_input_callback);
|
||||
|
||||
return bt_hid_mouse;
|
||||
}
|
||||
|
||||
void bt_hid_mouse_free(BtHidMouse* bt_hid_mouse) {
|
||||
furi_assert(bt_hid_mouse);
|
||||
view_free(bt_hid_mouse->view);
|
||||
free(bt_hid_mouse);
|
||||
}
|
||||
|
||||
View* bt_hid_mouse_get_view(BtHidMouse* bt_hid_mouse) {
|
||||
furi_assert(bt_hid_mouse);
|
||||
return bt_hid_mouse->view;
|
||||
}
|
||||
|
||||
void bt_hid_mouse_set_connected_status(BtHidMouse* bt_hid_mouse, bool connected) {
|
||||
furi_assert(bt_hid_mouse);
|
||||
with_view_model(
|
||||
bt_hid_mouse->view, (BtHidMouseModel * model) {
|
||||
model->connected = connected;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
typedef struct BtHidMouse BtHidMouse;
|
||||
|
||||
BtHidMouse* bt_hid_mouse_alloc();
|
||||
|
||||
void bt_hid_mouse_free(BtHidMouse* bt_hid_mouse);
|
||||
|
||||
View* bt_hid_mouse_get_view(BtHidMouse* bt_hid_mouse);
|
||||
|
||||
void bt_hid_mouse_set_connected_status(BtHidMouse* bt_hid_mouse, bool connected);
|
||||
@@ -0,0 +1,24 @@
|
||||
App(
|
||||
appid="music_player",
|
||||
name="Music Player",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="music_player_app",
|
||||
cdefines=["APP_MUSIC_PLAYER"],
|
||||
requires=[
|
||||
"gui",
|
||||
"dialogs",
|
||||
],
|
||||
provides=["music_player_start"],
|
||||
stack_size=2 * 1024,
|
||||
order=20,
|
||||
fap_icon="../../../assets/icons/Archive/music_10px.png",
|
||||
fap_category="Misc",
|
||||
)
|
||||
|
||||
App(
|
||||
appid="music_player_start",
|
||||
apptype=FlipperAppType.STARTUP,
|
||||
entry_point="music_player_on_system_start",
|
||||
requires=["music_player"],
|
||||
order=30,
|
||||
)
|
||||
@@ -0,0 +1,361 @@
|
||||
#include "music_player_worker.h"
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
|
||||
#include <assets_icons.h>
|
||||
#include <gui/gui.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
#include <m-string.h>
|
||||
|
||||
#define TAG "MusicPlayer"
|
||||
|
||||
#define MUSIC_PLAYER_APP_PATH_FOLDER ANY_PATH("music_player")
|
||||
#define MUSIC_PLAYER_APP_EXTENSION "*"
|
||||
|
||||
#define MUSIC_PLAYER_SEMITONE_HISTORY_SIZE 4
|
||||
|
||||
typedef struct {
|
||||
uint8_t semitone_history[MUSIC_PLAYER_SEMITONE_HISTORY_SIZE];
|
||||
uint8_t duration_history[MUSIC_PLAYER_SEMITONE_HISTORY_SIZE];
|
||||
|
||||
uint8_t volume;
|
||||
uint8_t semitone;
|
||||
uint8_t dots;
|
||||
uint8_t duration;
|
||||
float position;
|
||||
} MusicPlayerModel;
|
||||
|
||||
typedef struct {
|
||||
MusicPlayerModel* model;
|
||||
FuriMutex** model_mutex;
|
||||
|
||||
FuriMessageQueue* input_queue;
|
||||
|
||||
ViewPort* view_port;
|
||||
Gui* gui;
|
||||
|
||||
MusicPlayerWorker* worker;
|
||||
} MusicPlayer;
|
||||
|
||||
static const float MUSIC_PLAYER_VOLUMES[] = {0, .25, .5, .75, 1};
|
||||
|
||||
static const char* semitone_to_note(int8_t semitone) {
|
||||
switch(semitone) {
|
||||
case 0:
|
||||
return "C";
|
||||
case 1:
|
||||
return "C#";
|
||||
case 2:
|
||||
return "D";
|
||||
case 3:
|
||||
return "D#";
|
||||
case 4:
|
||||
return "E";
|
||||
case 5:
|
||||
return "F";
|
||||
case 6:
|
||||
return "F#";
|
||||
case 7:
|
||||
return "G";
|
||||
case 8:
|
||||
return "G#";
|
||||
case 9:
|
||||
return "A";
|
||||
case 10:
|
||||
return "A#";
|
||||
case 11:
|
||||
return "B";
|
||||
default:
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_white_note(uint8_t semitone, uint8_t id) {
|
||||
switch(semitone) {
|
||||
case 0:
|
||||
if(id == 0) return true;
|
||||
break;
|
||||
case 2:
|
||||
if(id == 1) return true;
|
||||
break;
|
||||
case 4:
|
||||
if(id == 2) return true;
|
||||
break;
|
||||
case 5:
|
||||
if(id == 3) return true;
|
||||
break;
|
||||
case 7:
|
||||
if(id == 4) return true;
|
||||
break;
|
||||
case 9:
|
||||
if(id == 5) return true;
|
||||
break;
|
||||
case 11:
|
||||
if(id == 6) return true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_black_note(uint8_t semitone, uint8_t id) {
|
||||
switch(semitone) {
|
||||
case 1:
|
||||
if(id == 0) return true;
|
||||
break;
|
||||
case 3:
|
||||
if(id == 1) return true;
|
||||
break;
|
||||
case 6:
|
||||
if(id == 3) return true;
|
||||
break;
|
||||
case 8:
|
||||
if(id == 4) return true;
|
||||
break;
|
||||
case 10:
|
||||
if(id == 5) return true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void render_callback(Canvas* canvas, void* ctx) {
|
||||
MusicPlayer* music_player = ctx;
|
||||
furi_check(furi_mutex_acquire(music_player->model_mutex, FuriWaitForever) == FuriStatusOk);
|
||||
|
||||
canvas_clear(canvas);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str(canvas, 0, 12, "MusicPlayer");
|
||||
|
||||
uint8_t x_pos = 0;
|
||||
uint8_t y_pos = 24;
|
||||
const uint8_t white_w = 10;
|
||||
const uint8_t white_h = 40;
|
||||
|
||||
const int8_t black_x = 6;
|
||||
const int8_t black_y = -5;
|
||||
const uint8_t black_w = 8;
|
||||
const uint8_t black_h = 32;
|
||||
|
||||
// white keys
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
if(is_white_note(music_player->model->semitone, i)) {
|
||||
canvas_draw_box(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h);
|
||||
} else {
|
||||
canvas_draw_frame(canvas, x_pos + white_w * i, y_pos, white_w + 1, white_h);
|
||||
}
|
||||
}
|
||||
|
||||
// black keys
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
if(i != 2 && i != 6) {
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_box(
|
||||
canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
if(is_black_note(music_player->model->semitone, i)) {
|
||||
canvas_draw_box(
|
||||
canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h);
|
||||
} else {
|
||||
canvas_draw_frame(
|
||||
canvas, x_pos + white_w * i + black_x, y_pos + black_y, black_w + 1, black_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// volume view_port
|
||||
x_pos = 124;
|
||||
y_pos = 0;
|
||||
const uint8_t volume_h =
|
||||
(64 / (COUNT_OF(MUSIC_PLAYER_VOLUMES) - 1)) * music_player->model->volume;
|
||||
canvas_draw_frame(canvas, x_pos, y_pos, 4, 64);
|
||||
canvas_draw_box(canvas, x_pos, y_pos + (64 - volume_h), 4, volume_h);
|
||||
|
||||
// note stack view_port
|
||||
x_pos = 73;
|
||||
y_pos = 0;
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_frame(canvas, x_pos, y_pos, 49, 64);
|
||||
canvas_draw_line(canvas, x_pos + 28, 0, x_pos + 28, 64);
|
||||
|
||||
char duration_text[16];
|
||||
for(uint8_t i = 0; i < MUSIC_PLAYER_SEMITONE_HISTORY_SIZE; i++) {
|
||||
if(music_player->model->duration_history[i] == 0xFF) {
|
||||
snprintf(duration_text, 15, "--");
|
||||
} else {
|
||||
snprintf(duration_text, 15, "%d", music_player->model->duration_history[i]);
|
||||
}
|
||||
|
||||
if(i == 0) {
|
||||
canvas_draw_box(canvas, x_pos, y_pos + 48, 49, 16);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
} else {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
canvas_draw_str(
|
||||
canvas,
|
||||
x_pos + 4,
|
||||
64 - 16 * i - 3,
|
||||
semitone_to_note(music_player->model->semitone_history[i]));
|
||||
canvas_draw_str(canvas, x_pos + 31, 64 - 16 * i - 3, duration_text);
|
||||
canvas_draw_line(canvas, x_pos, 64 - 16 * i, x_pos + 48, 64 - 16 * i);
|
||||
}
|
||||
|
||||
furi_mutex_release(music_player->model_mutex);
|
||||
}
|
||||
|
||||
static void input_callback(InputEvent* input_event, void* ctx) {
|
||||
MusicPlayer* music_player = ctx;
|
||||
if(input_event->type == InputTypeShort) {
|
||||
furi_message_queue_put(music_player->input_queue, input_event, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void music_player_worker_callback(
|
||||
uint8_t semitone,
|
||||
uint8_t dots,
|
||||
uint8_t duration,
|
||||
float position,
|
||||
void* context) {
|
||||
MusicPlayer* music_player = context;
|
||||
furi_check(furi_mutex_acquire(music_player->model_mutex, FuriWaitForever) == FuriStatusOk);
|
||||
|
||||
for(size_t i = 0; i < MUSIC_PLAYER_SEMITONE_HISTORY_SIZE - 1; i++) {
|
||||
size_t r = MUSIC_PLAYER_SEMITONE_HISTORY_SIZE - 1 - i;
|
||||
music_player->model->duration_history[r] = music_player->model->duration_history[r - 1];
|
||||
music_player->model->semitone_history[r] = music_player->model->semitone_history[r - 1];
|
||||
}
|
||||
|
||||
semitone = (semitone == 0xFF) ? 0xFF : semitone % 12;
|
||||
|
||||
music_player->model->semitone = semitone;
|
||||
music_player->model->dots = dots;
|
||||
music_player->model->duration = duration;
|
||||
music_player->model->position = position;
|
||||
|
||||
music_player->model->semitone_history[0] = semitone;
|
||||
music_player->model->duration_history[0] = duration;
|
||||
|
||||
furi_mutex_release(music_player->model_mutex);
|
||||
view_port_update(music_player->view_port);
|
||||
}
|
||||
|
||||
MusicPlayer* music_player_alloc() {
|
||||
MusicPlayer* instance = malloc(sizeof(MusicPlayer));
|
||||
|
||||
instance->model = malloc(sizeof(MusicPlayerModel));
|
||||
memset(instance->model->duration_history, 0xff, MUSIC_PLAYER_SEMITONE_HISTORY_SIZE);
|
||||
memset(instance->model->semitone_history, 0xff, MUSIC_PLAYER_SEMITONE_HISTORY_SIZE);
|
||||
instance->model->volume = 3;
|
||||
|
||||
instance->model_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
|
||||
|
||||
instance->input_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
|
||||
|
||||
instance->worker = music_player_worker_alloc();
|
||||
music_player_worker_set_volume(
|
||||
instance->worker, MUSIC_PLAYER_VOLUMES[instance->model->volume]);
|
||||
music_player_worker_set_callback(instance->worker, music_player_worker_callback, instance);
|
||||
|
||||
instance->view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(instance->view_port, render_callback, instance);
|
||||
view_port_input_callback_set(instance->view_port, input_callback, instance);
|
||||
|
||||
// Open GUI and register view_port
|
||||
instance->gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(instance->gui, instance->view_port, GuiLayerFullscreen);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
void music_player_free(MusicPlayer* instance) {
|
||||
gui_remove_view_port(instance->gui, instance->view_port);
|
||||
furi_record_close(RECORD_GUI);
|
||||
view_port_free(instance->view_port);
|
||||
|
||||
music_player_worker_free(instance->worker);
|
||||
|
||||
furi_message_queue_free(instance->input_queue);
|
||||
|
||||
furi_mutex_free(instance->model_mutex);
|
||||
|
||||
free(instance->model);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
int32_t music_player_app(void* p) {
|
||||
MusicPlayer* music_player = music_player_alloc();
|
||||
|
||||
string_t file_path;
|
||||
string_init(file_path);
|
||||
|
||||
do {
|
||||
if(p && strlen(p)) {
|
||||
string_cat_str(file_path, p);
|
||||
} else {
|
||||
string_set_str(file_path, MUSIC_PLAYER_APP_PATH_FOLDER);
|
||||
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
dialog_file_browser_set_basic_options(
|
||||
&browser_options, MUSIC_PLAYER_APP_EXTENSION, &I_music_10px);
|
||||
browser_options.hide_ext = false;
|
||||
|
||||
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options);
|
||||
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
if(!res) {
|
||||
FURI_LOG_E(TAG, "No file selected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!music_player_worker_load(music_player->worker, string_get_cstr(file_path))) {
|
||||
FURI_LOG_E(TAG, "Unable to load file");
|
||||
break;
|
||||
}
|
||||
|
||||
music_player_worker_start(music_player->worker);
|
||||
|
||||
InputEvent input;
|
||||
while(furi_message_queue_get(music_player->input_queue, &input, FuriWaitForever) ==
|
||||
FuriStatusOk) {
|
||||
furi_check(
|
||||
furi_mutex_acquire(music_player->model_mutex, FuriWaitForever) == FuriStatusOk);
|
||||
|
||||
if(input.key == InputKeyBack) {
|
||||
furi_mutex_release(music_player->model_mutex);
|
||||
break;
|
||||
} else if(input.key == InputKeyUp) {
|
||||
if(music_player->model->volume < COUNT_OF(MUSIC_PLAYER_VOLUMES) - 1)
|
||||
music_player->model->volume++;
|
||||
music_player_worker_set_volume(
|
||||
music_player->worker, MUSIC_PLAYER_VOLUMES[music_player->model->volume]);
|
||||
} else if(input.key == InputKeyDown) {
|
||||
if(music_player->model->volume > 0) music_player->model->volume--;
|
||||
music_player_worker_set_volume(
|
||||
music_player->worker, MUSIC_PLAYER_VOLUMES[music_player->model->volume]);
|
||||
}
|
||||
|
||||
furi_mutex_release(music_player->model_mutex);
|
||||
view_port_update(music_player->view_port);
|
||||
}
|
||||
|
||||
music_player_worker_stop(music_player->worker);
|
||||
} while(0);
|
||||
|
||||
string_clear(file_path);
|
||||
music_player_free(music_player);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#include <furi.h>
|
||||
#include <cli/cli.h>
|
||||
#include <storage/storage.h>
|
||||
#include "music_player_worker.h"
|
||||
|
||||
static void music_player_cli(Cli* cli, string_t args, void* context) {
|
||||
UNUSED(context);
|
||||
MusicPlayerWorker* music_player_worker = music_player_worker_alloc();
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
do {
|
||||
if(storage_common_stat(storage, string_get_cstr(args), NULL) == FSE_OK) {
|
||||
if(!music_player_worker_load(music_player_worker, string_get_cstr(args))) {
|
||||
printf("Failed to open file %s\r\n", string_get_cstr(args));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if(!music_player_worker_load_rtttl_from_string(
|
||||
music_player_worker, string_get_cstr(args))) {
|
||||
printf("Argument is not a file or RTTTL\r\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
printf("Press CTRL+C to stop\r\n");
|
||||
music_player_worker_set_volume(music_player_worker, 1.0f);
|
||||
music_player_worker_start(music_player_worker);
|
||||
while(!cli_cmd_interrupt_received(cli)) {
|
||||
furi_delay_ms(50);
|
||||
}
|
||||
music_player_worker_stop(music_player_worker);
|
||||
} while(0);
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
music_player_worker_free(music_player_worker);
|
||||
}
|
||||
|
||||
void music_player_on_system_start() {
|
||||
#ifdef SRV_CLI
|
||||
Cli* cli = furi_record_open(RECORD_CLI);
|
||||
|
||||
cli_add_command(cli, "music_player", CliCommandFlagDefault, music_player_cli, NULL);
|
||||
|
||||
furi_record_close(RECORD_CLI);
|
||||
#else
|
||||
UNUSED(music_player_cli);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
#include "music_player_worker.h"
|
||||
|
||||
#include <furi_hal.h>
|
||||
#include <furi.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <lib/flipper_format/flipper_format.h>
|
||||
|
||||
#include <m-array.h>
|
||||
|
||||
#define TAG "MusicPlayerWorker"
|
||||
|
||||
#define MUSIC_PLAYER_FILETYPE "Flipper Music Format"
|
||||
#define MUSIC_PLAYER_VERSION 0
|
||||
|
||||
#define SEMITONE_PAUSE 0xFF
|
||||
|
||||
#define NOTE_C4 261.63f
|
||||
#define NOTE_C4_SEMITONE (4.0f * 12.0f)
|
||||
#define TWO_POW_TWELTH_ROOT 1.059463094359f
|
||||
|
||||
typedef struct {
|
||||
uint8_t semitone;
|
||||
uint8_t duration;
|
||||
uint8_t dots;
|
||||
} NoteBlock;
|
||||
|
||||
ARRAY_DEF(NoteBlockArray, NoteBlock, M_POD_OPLIST);
|
||||
|
||||
struct MusicPlayerWorker {
|
||||
FuriThread* thread;
|
||||
bool should_work;
|
||||
|
||||
MusicPlayerWorkerCallback callback;
|
||||
void* callback_context;
|
||||
|
||||
float volume;
|
||||
uint32_t bpm;
|
||||
uint32_t duration;
|
||||
uint32_t octave;
|
||||
NoteBlockArray_t notes;
|
||||
};
|
||||
|
||||
static int32_t music_player_worker_thread_callback(void* context) {
|
||||
furi_assert(context);
|
||||
MusicPlayerWorker* instance = context;
|
||||
|
||||
NoteBlockArray_it_t it;
|
||||
NoteBlockArray_it(it, instance->notes);
|
||||
|
||||
while(instance->should_work) {
|
||||
if(NoteBlockArray_end_p(it)) {
|
||||
NoteBlockArray_it(it, instance->notes);
|
||||
furi_delay_ms(10);
|
||||
} else {
|
||||
NoteBlock* note_block = NoteBlockArray_ref(it);
|
||||
|
||||
float note_from_a4 = (float)note_block->semitone - NOTE_C4_SEMITONE;
|
||||
float frequency = NOTE_C4 * powf(TWO_POW_TWELTH_ROOT, note_from_a4);
|
||||
float duration =
|
||||
60.0 * furi_kernel_get_tick_frequency() * 4 / instance->bpm / note_block->duration;
|
||||
uint32_t dots = note_block->dots;
|
||||
while(dots > 0) {
|
||||
duration += duration / 2;
|
||||
dots--;
|
||||
}
|
||||
uint32_t next_tick = furi_get_tick() + duration;
|
||||
float volume = instance->volume;
|
||||
|
||||
if(instance->callback) {
|
||||
instance->callback(
|
||||
note_block->semitone,
|
||||
note_block->dots,
|
||||
note_block->duration,
|
||||
0.0,
|
||||
instance->callback_context);
|
||||
}
|
||||
|
||||
furi_hal_speaker_stop();
|
||||
furi_hal_speaker_start(frequency, volume);
|
||||
while(instance->should_work && furi_get_tick() < next_tick) {
|
||||
volume *= 0.9945679;
|
||||
furi_hal_speaker_set_volume(volume);
|
||||
furi_delay_ms(2);
|
||||
}
|
||||
NoteBlockArray_next(it);
|
||||
}
|
||||
}
|
||||
|
||||
furi_hal_speaker_stop();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
MusicPlayerWorker* music_player_worker_alloc() {
|
||||
MusicPlayerWorker* instance = malloc(sizeof(MusicPlayerWorker));
|
||||
|
||||
NoteBlockArray_init(instance->notes);
|
||||
|
||||
instance->thread = furi_thread_alloc();
|
||||
furi_thread_set_name(instance->thread, "MusicPlayerWorker");
|
||||
furi_thread_set_stack_size(instance->thread, 1024);
|
||||
furi_thread_set_context(instance->thread, instance);
|
||||
furi_thread_set_callback(instance->thread, music_player_worker_thread_callback);
|
||||
|
||||
instance->volume = 1.0f;
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
void music_player_worker_free(MusicPlayerWorker* instance) {
|
||||
furi_assert(instance);
|
||||
furi_thread_free(instance->thread);
|
||||
NoteBlockArray_clear(instance->notes);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
static bool is_digit(const char c) {
|
||||
return isdigit(c) != 0;
|
||||
}
|
||||
|
||||
static bool is_letter(const char c) {
|
||||
return islower(c) != 0 || isupper(c) != 0;
|
||||
}
|
||||
|
||||
static bool is_space(const char c) {
|
||||
return c == ' ' || c == '\t';
|
||||
}
|
||||
|
||||
static size_t extract_number(const char* string, uint32_t* number) {
|
||||
size_t ret = 0;
|
||||
while(is_digit(*string)) {
|
||||
*number *= 10;
|
||||
*number += (*string - '0');
|
||||
string++;
|
||||
ret++;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static size_t extract_dots(const char* string, uint32_t* number) {
|
||||
size_t ret = 0;
|
||||
while(*string == '.') {
|
||||
*number += 1;
|
||||
string++;
|
||||
ret++;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static size_t extract_char(const char* string, char* symbol) {
|
||||
if(is_letter(*string)) {
|
||||
*symbol = *string;
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static size_t extract_sharp(const char* string, char* symbol) {
|
||||
if(*string == '#' || *string == '_') {
|
||||
*symbol = '#';
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static size_t skip_till(const char* string, const char symbol) {
|
||||
size_t ret = 0;
|
||||
while(*string != '\0' && *string != symbol) {
|
||||
string++;
|
||||
ret++;
|
||||
}
|
||||
if(*string != symbol) {
|
||||
ret = 0;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool music_player_worker_add_note(
|
||||
MusicPlayerWorker* instance,
|
||||
uint8_t semitone,
|
||||
uint8_t duration,
|
||||
uint8_t dots) {
|
||||
NoteBlock note_block;
|
||||
|
||||
note_block.semitone = semitone;
|
||||
note_block.duration = duration;
|
||||
note_block.dots = dots;
|
||||
|
||||
NoteBlockArray_push_back(instance->notes, note_block);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int8_t note_to_semitone(const char note) {
|
||||
switch(note) {
|
||||
case 'C':
|
||||
return 0;
|
||||
// C#
|
||||
case 'D':
|
||||
return 2;
|
||||
// D#
|
||||
case 'E':
|
||||
return 4;
|
||||
case 'F':
|
||||
return 5;
|
||||
// F#
|
||||
case 'G':
|
||||
return 7;
|
||||
// G#
|
||||
case 'A':
|
||||
return 9;
|
||||
// A#
|
||||
case 'B':
|
||||
return 11;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool music_player_worker_parse_notes(MusicPlayerWorker* instance, const char* string) {
|
||||
const char* cursor = string;
|
||||
bool result = true;
|
||||
|
||||
while(*cursor != '\0') {
|
||||
if(!is_space(*cursor)) {
|
||||
uint32_t duration = 0;
|
||||
char note_char = '\0';
|
||||
char sharp_char = '\0';
|
||||
uint32_t octave = 0;
|
||||
uint32_t dots = 0;
|
||||
|
||||
// Parsing
|
||||
cursor += extract_number(cursor, &duration);
|
||||
cursor += extract_char(cursor, ¬e_char);
|
||||
cursor += extract_sharp(cursor, &sharp_char);
|
||||
cursor += extract_number(cursor, &octave);
|
||||
cursor += extract_dots(cursor, &dots);
|
||||
|
||||
// Post processing
|
||||
note_char = toupper(note_char);
|
||||
if(!duration) {
|
||||
duration = instance->duration;
|
||||
}
|
||||
if(!octave) {
|
||||
octave = instance->octave;
|
||||
}
|
||||
|
||||
// Validation
|
||||
bool is_valid = true;
|
||||
is_valid &= (duration >= 1 && duration <= 128);
|
||||
is_valid &= ((note_char >= 'A' && note_char <= 'G') || note_char == 'P');
|
||||
is_valid &= (sharp_char == '#' || sharp_char == '\0');
|
||||
is_valid &= (octave <= 16);
|
||||
is_valid &= (dots <= 16);
|
||||
if(!is_valid) {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"Invalid note: %u%c%c%u.%u",
|
||||
duration,
|
||||
note_char == '\0' ? '_' : note_char,
|
||||
sharp_char == '\0' ? '_' : sharp_char,
|
||||
octave,
|
||||
dots);
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Note to semitones
|
||||
uint8_t semitone = 0;
|
||||
if(note_char == 'P') {
|
||||
semitone = SEMITONE_PAUSE;
|
||||
} else {
|
||||
semitone += octave * 12;
|
||||
semitone += note_to_semitone(note_char);
|
||||
semitone += sharp_char == '#' ? 1 : 0;
|
||||
}
|
||||
|
||||
if(music_player_worker_add_note(instance, semitone, duration, dots)) {
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"Added note: %c%c%u.%u = %u %u",
|
||||
note_char == '\0' ? '_' : note_char,
|
||||
sharp_char == '\0' ? '_' : sharp_char,
|
||||
octave,
|
||||
dots,
|
||||
semitone,
|
||||
duration);
|
||||
} else {
|
||||
FURI_LOG_E(
|
||||
TAG,
|
||||
"Invalid note: %c%c%u.%u = %u %u",
|
||||
note_char == '\0' ? '_' : note_char,
|
||||
sharp_char == '\0' ? '_' : sharp_char,
|
||||
octave,
|
||||
dots,
|
||||
semitone,
|
||||
duration);
|
||||
}
|
||||
cursor += skip_till(cursor, ',');
|
||||
}
|
||||
|
||||
if(*cursor != '\0') cursor++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool music_player_worker_load(MusicPlayerWorker* instance, const char* file_path) {
|
||||
furi_assert(instance);
|
||||
furi_assert(file_path);
|
||||
|
||||
bool ret = false;
|
||||
if(strcasestr(file_path, ".fmf")) {
|
||||
ret = music_player_worker_load_fmf_from_file(instance, file_path);
|
||||
} else {
|
||||
ret = music_player_worker_load_rtttl_from_file(instance, file_path);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool music_player_worker_load_fmf_from_file(MusicPlayerWorker* instance, const char* file_path) {
|
||||
furi_assert(instance);
|
||||
furi_assert(file_path);
|
||||
|
||||
bool result = false;
|
||||
string_t temp_str;
|
||||
string_init(temp_str);
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
|
||||
do {
|
||||
if(!flipper_format_file_open_existing(file, file_path)) break;
|
||||
|
||||
uint32_t version = 0;
|
||||
if(!flipper_format_read_header(file, temp_str, &version)) break;
|
||||
if(string_cmp_str(temp_str, MUSIC_PLAYER_FILETYPE) || (version != MUSIC_PLAYER_VERSION)) {
|
||||
FURI_LOG_E(TAG, "Incorrect file format or version");
|
||||
break;
|
||||
}
|
||||
|
||||
if(!flipper_format_read_uint32(file, "BPM", &instance->bpm, 1)) {
|
||||
FURI_LOG_E(TAG, "BPM is missing");
|
||||
break;
|
||||
}
|
||||
if(!flipper_format_read_uint32(file, "Duration", &instance->duration, 1)) {
|
||||
FURI_LOG_E(TAG, "Duration is missing");
|
||||
break;
|
||||
}
|
||||
if(!flipper_format_read_uint32(file, "Octave", &instance->octave, 1)) {
|
||||
FURI_LOG_E(TAG, "Octave is missing");
|
||||
break;
|
||||
}
|
||||
|
||||
if(!flipper_format_read_string(file, "Notes", temp_str)) {
|
||||
FURI_LOG_E(TAG, "Notes is missing");
|
||||
break;
|
||||
}
|
||||
|
||||
if(!music_player_worker_parse_notes(instance, string_get_cstr(temp_str))) {
|
||||
break;
|
||||
}
|
||||
|
||||
result = true;
|
||||
} while(false);
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
flipper_format_free(file);
|
||||
string_clear(temp_str);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool music_player_worker_load_rtttl_from_file(MusicPlayerWorker* instance, const char* file_path) {
|
||||
furi_assert(instance);
|
||||
furi_assert(file_path);
|
||||
|
||||
bool result = false;
|
||||
string_t content;
|
||||
string_init(content);
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* file = storage_file_alloc(storage);
|
||||
|
||||
do {
|
||||
if(!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
FURI_LOG_E(TAG, "Unable to open file");
|
||||
break;
|
||||
};
|
||||
|
||||
uint16_t ret = 0;
|
||||
do {
|
||||
uint8_t buffer[65] = {0};
|
||||
ret = storage_file_read(file, buffer, sizeof(buffer) - 1);
|
||||
for(size_t i = 0; i < ret; i++) {
|
||||
string_push_back(content, buffer[i]);
|
||||
}
|
||||
} while(ret > 0);
|
||||
|
||||
string_strim(content);
|
||||
if(!string_size(content)) {
|
||||
FURI_LOG_E(TAG, "Empty file");
|
||||
break;
|
||||
}
|
||||
|
||||
if(!music_player_worker_load_rtttl_from_string(instance, string_get_cstr(content))) {
|
||||
FURI_LOG_E(TAG, "Invalid file content");
|
||||
break;
|
||||
}
|
||||
|
||||
result = true;
|
||||
} while(0);
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
string_clear(content);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool music_player_worker_load_rtttl_from_string(MusicPlayerWorker* instance, const char* string) {
|
||||
furi_assert(instance);
|
||||
|
||||
const char* cursor = string;
|
||||
|
||||
// Skip name
|
||||
cursor += skip_till(cursor, ':');
|
||||
if(*cursor != ':') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Duration
|
||||
cursor += skip_till(cursor, '=');
|
||||
if(*cursor != '=') {
|
||||
return false;
|
||||
}
|
||||
cursor++;
|
||||
cursor += extract_number(cursor, &instance->duration);
|
||||
|
||||
// Octave
|
||||
cursor += skip_till(cursor, '=');
|
||||
if(*cursor != '=') {
|
||||
return false;
|
||||
}
|
||||
cursor++;
|
||||
cursor += extract_number(cursor, &instance->octave);
|
||||
|
||||
// BPM
|
||||
cursor += skip_till(cursor, '=');
|
||||
if(*cursor != '=') {
|
||||
return false;
|
||||
}
|
||||
cursor++;
|
||||
cursor += extract_number(cursor, &instance->bpm);
|
||||
|
||||
// Notes
|
||||
cursor += skip_till(cursor, ':');
|
||||
if(*cursor != ':') {
|
||||
return false;
|
||||
}
|
||||
cursor++;
|
||||
if(!music_player_worker_parse_notes(instance, cursor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void music_player_worker_set_callback(
|
||||
MusicPlayerWorker* instance,
|
||||
MusicPlayerWorkerCallback callback,
|
||||
void* context) {
|
||||
furi_assert(instance);
|
||||
instance->callback = callback;
|
||||
instance->callback_context = context;
|
||||
}
|
||||
|
||||
void music_player_worker_set_volume(MusicPlayerWorker* instance, float volume) {
|
||||
furi_assert(instance);
|
||||
instance->volume = volume;
|
||||
}
|
||||
|
||||
void music_player_worker_start(MusicPlayerWorker* instance) {
|
||||
furi_assert(instance);
|
||||
furi_assert(instance->should_work == false);
|
||||
|
||||
instance->should_work = true;
|
||||
furi_thread_start(instance->thread);
|
||||
}
|
||||
|
||||
void music_player_worker_stop(MusicPlayerWorker* instance) {
|
||||
furi_assert(instance);
|
||||
furi_assert(instance->should_work == true);
|
||||
|
||||
instance->should_work = false;
|
||||
furi_thread_join(instance->thread);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef void (*MusicPlayerWorkerCallback)(
|
||||
uint8_t semitone,
|
||||
uint8_t dots,
|
||||
uint8_t duration,
|
||||
float position,
|
||||
void* context);
|
||||
|
||||
typedef struct MusicPlayerWorker MusicPlayerWorker;
|
||||
|
||||
MusicPlayerWorker* music_player_worker_alloc();
|
||||
|
||||
void music_player_worker_free(MusicPlayerWorker* instance);
|
||||
|
||||
bool music_player_worker_load(MusicPlayerWorker* instance, const char* file_path);
|
||||
|
||||
bool music_player_worker_load_fmf_from_file(MusicPlayerWorker* instance, const char* file_path);
|
||||
|
||||
bool music_player_worker_load_rtttl_from_file(MusicPlayerWorker* instance, const char* file_path);
|
||||
|
||||
bool music_player_worker_load_rtttl_from_string(MusicPlayerWorker* instance, const char* string);
|
||||
|
||||
void music_player_worker_set_callback(
|
||||
MusicPlayerWorker* instance,
|
||||
MusicPlayerWorkerCallback callback,
|
||||
void* context);
|
||||
|
||||
void music_player_worker_set_volume(MusicPlayerWorker* instance, float volume);
|
||||
|
||||
void music_player_worker_start(MusicPlayerWorker* instance);
|
||||
|
||||
void music_player_worker_stop(MusicPlayerWorker* instance);
|
||||
@@ -0,0 +1,17 @@
|
||||
App(
|
||||
appid="picopass",
|
||||
name="PicoPass Reader",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="picopass_app",
|
||||
requires=[
|
||||
"storage",
|
||||
"gui",
|
||||
],
|
||||
stack_size=4 * 1024,
|
||||
order=30,
|
||||
fap_icon="../../../assets/icons/Archive/125_10px.png",
|
||||
fap_libs=[
|
||||
"mbedtls",
|
||||
],
|
||||
fap_category="Tools",
|
||||
)
|
||||
@@ -0,0 +1,318 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Borrowed initially from https://github.com/holiman/loclass
|
||||
// Copyright (C) 2014 Martin Holst Swende
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// WARNING
|
||||
//
|
||||
// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
|
||||
//
|
||||
// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
|
||||
// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
|
||||
// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
|
||||
//
|
||||
// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
|
||||
//-----------------------------------------------------------------------------
|
||||
// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
|
||||
//
|
||||
// The implementation is based on the work performed by
|
||||
// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
|
||||
// Milosch Meriac in the paper "Dismantling IClass".
|
||||
//-----------------------------------------------------------------------------
|
||||
/*
|
||||
This file contains an optimized version of the MAC-calculation algorithm. Some measurements on
|
||||
a std laptop showed it runs in about 1/3 of the time:
|
||||
|
||||
Std: 0.428962
|
||||
Opt: 0.151609
|
||||
|
||||
Additionally, it is self-reliant, not requiring e.g. bitstreams from the cipherutils, thus can
|
||||
be easily dropped into a code base.
|
||||
|
||||
The optimizations have been performed in the following steps:
|
||||
* Parameters passed by reference instead of by value.
|
||||
* Iteration instead of recursion, un-nesting recursive loops into for-loops.
|
||||
* Handling of bytes instead of individual bits, for less shuffling and masking
|
||||
* Less creation of "objects", structs, and instead reuse of alloc:ed memory
|
||||
* Inlining some functions via #define:s
|
||||
|
||||
As a consequence, this implementation is less generic. Also, I haven't bothered documenting this.
|
||||
For a thorough documentation, check out the MAC-calculation within cipher.c instead.
|
||||
|
||||
-- MHS 2015
|
||||
**/
|
||||
|
||||
/**
|
||||
|
||||
The runtime of opt_doTagMAC_2() with the MHS optimized version was 403 microseconds on Proxmark3.
|
||||
This was still to slow for some newer readers which didn't want to wait that long.
|
||||
|
||||
Further optimizations to speedup the MAC calculations:
|
||||
* Optimized opt_Tt logic
|
||||
* Look up table for opt_select
|
||||
* Removing many unnecessary bit maskings (& 0x1)
|
||||
* updating state in place instead of alternating use of a second state structure
|
||||
* remove the necessity to reverse bits of input and output bytes
|
||||
|
||||
opt_doTagMAC_2() now completes in 270 microseconds.
|
||||
|
||||
-- piwi 2019
|
||||
**/
|
||||
|
||||
/**
|
||||
add the possibility to do iCLASS on device only
|
||||
-- iceman 2020
|
||||
**/
|
||||
|
||||
#include "optimized_cipher.h"
|
||||
#include "optimized_elite.h"
|
||||
#include "optimized_ikeys.h"
|
||||
#include "optimized_cipherutils.h"
|
||||
|
||||
static const uint8_t loclass_opt_select_LUT[256] = {
|
||||
00, 03, 02, 01, 02, 03, 00, 01, 04, 07, 07, 04, 06, 07, 05, 04, 01, 02, 03, 00, 02, 03, 00, 01,
|
||||
05, 06, 06, 05, 06, 07, 05, 04, 06, 05, 04, 07, 04, 05, 06, 07, 06, 05, 05, 06, 04, 05, 07, 06,
|
||||
07, 04, 05, 06, 04, 05, 06, 07, 07, 04, 04, 07, 04, 05, 07, 06, 06, 05, 04, 07, 04, 05, 06, 07,
|
||||
02, 01, 01, 02, 00, 01, 03, 02, 03, 00, 01, 02, 00, 01, 02, 03, 07, 04, 04, 07, 04, 05, 07, 06,
|
||||
00, 03, 02, 01, 02, 03, 00, 01, 00, 03, 03, 00, 02, 03, 01, 00, 05, 06, 07, 04, 06, 07, 04, 05,
|
||||
05, 06, 06, 05, 06, 07, 05, 04, 02, 01, 00, 03, 00, 01, 02, 03, 06, 05, 05, 06, 04, 05, 07, 06,
|
||||
03, 00, 01, 02, 00, 01, 02, 03, 07, 04, 04, 07, 04, 05, 07, 06, 02, 01, 00, 03, 00, 01, 02, 03,
|
||||
02, 01, 01, 02, 00, 01, 03, 02, 03, 00, 01, 02, 00, 01, 02, 03, 03, 00, 00, 03, 00, 01, 03, 02,
|
||||
04, 07, 06, 05, 06, 07, 04, 05, 00, 03, 03, 00, 02, 03, 01, 00, 01, 02, 03, 00, 02, 03, 00, 01,
|
||||
05, 06, 06, 05, 06, 07, 05, 04, 04, 07, 06, 05, 06, 07, 04, 05, 04, 07, 07, 04, 06, 07, 05, 04,
|
||||
01, 02, 03, 00, 02, 03, 00, 01, 01, 02, 02, 01, 02, 03, 01, 00};
|
||||
|
||||
/********************** the table above has been generated with this code: ********
|
||||
#include "util.h"
|
||||
static void init_opt_select_LUT(void) {
|
||||
for (int r = 0; r < 256; r++) {
|
||||
uint8_t r_ls2 = r << 2;
|
||||
uint8_t r_and_ls2 = r & r_ls2;
|
||||
uint8_t r_or_ls2 = r | r_ls2;
|
||||
uint8_t z0 = (r_and_ls2 >> 5) ^ ((r & ~r_ls2) >> 4) ^ ( r_or_ls2 >> 3);
|
||||
uint8_t z1 = (r_or_ls2 >> 6) ^ ( r_or_ls2 >> 1) ^ (r >> 5) ^ r;
|
||||
uint8_t z2 = ((r & ~r_ls2) >> 4) ^ (r_and_ls2 >> 3) ^ r;
|
||||
loclass_opt_select_LUT[r] = (z0 & 4) | (z1 & 2) | (z2 & 1);
|
||||
}
|
||||
print_result("", loclass_opt_select_LUT, 256);
|
||||
}
|
||||
***********************************************************************************/
|
||||
|
||||
#define loclass_opt__select(x, y, r) \
|
||||
(4 & (((r & (r << 2)) >> 5) ^ ((r & ~(r << 2)) >> 4) ^ ((r | r << 2) >> 3))) | \
|
||||
(2 & (((r | r << 2) >> 6) ^ ((r | r << 2) >> 1) ^ (r >> 5) ^ r ^ ((x ^ y) << 1))) | \
|
||||
(1 & (((r & ~(r << 2)) >> 4) ^ ((r & (r << 2)) >> 3) ^ r ^ x))
|
||||
|
||||
static void loclass_opt_successor(const uint8_t* k, LoclassState_t* s, uint8_t y) {
|
||||
uint16_t Tt = s->t & 0xc533;
|
||||
Tt = Tt ^ (Tt >> 1);
|
||||
Tt = Tt ^ (Tt >> 4);
|
||||
Tt = Tt ^ (Tt >> 10);
|
||||
Tt = Tt ^ (Tt >> 8);
|
||||
|
||||
s->t = (s->t >> 1);
|
||||
s->t |= (Tt ^ (s->r >> 7) ^ (s->r >> 3)) << 15;
|
||||
|
||||
uint8_t opt_B = s->b;
|
||||
opt_B ^= s->b >> 6;
|
||||
opt_B ^= s->b >> 5;
|
||||
opt_B ^= s->b >> 4;
|
||||
|
||||
s->b = s->b >> 1;
|
||||
s->b |= (opt_B ^ s->r) << 7;
|
||||
|
||||
uint8_t opt_select = loclass_opt_select_LUT[s->r] & 0x04;
|
||||
opt_select |= (loclass_opt_select_LUT[s->r] ^ ((Tt ^ y) << 1)) & 0x02;
|
||||
opt_select |= (loclass_opt_select_LUT[s->r] ^ Tt) & 0x01;
|
||||
|
||||
uint8_t r = s->r;
|
||||
s->r = (k[opt_select] ^ s->b) + s->l;
|
||||
s->l = s->r + r;
|
||||
}
|
||||
|
||||
static void loclass_opt_suc(
|
||||
const uint8_t* k,
|
||||
LoclassState_t* s,
|
||||
const uint8_t* in,
|
||||
uint8_t length,
|
||||
bool add32Zeroes) {
|
||||
for(int i = 0; i < length; i++) {
|
||||
uint8_t head;
|
||||
head = in[i];
|
||||
loclass_opt_successor(k, s, head);
|
||||
|
||||
head >>= 1;
|
||||
loclass_opt_successor(k, s, head);
|
||||
|
||||
head >>= 1;
|
||||
loclass_opt_successor(k, s, head);
|
||||
|
||||
head >>= 1;
|
||||
loclass_opt_successor(k, s, head);
|
||||
|
||||
head >>= 1;
|
||||
loclass_opt_successor(k, s, head);
|
||||
|
||||
head >>= 1;
|
||||
loclass_opt_successor(k, s, head);
|
||||
|
||||
head >>= 1;
|
||||
loclass_opt_successor(k, s, head);
|
||||
|
||||
head >>= 1;
|
||||
loclass_opt_successor(k, s, head);
|
||||
}
|
||||
//For tag MAC, an additional 32 zeroes
|
||||
if(add32Zeroes) {
|
||||
for(int i = 0; i < 16; i++) {
|
||||
loclass_opt_successor(k, s, 0);
|
||||
loclass_opt_successor(k, s, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void loclass_opt_output(const uint8_t* k, LoclassState_t* s, uint8_t* buffer) {
|
||||
for(uint8_t times = 0; times < 4; times++) {
|
||||
uint8_t bout = 0;
|
||||
bout |= (s->r & 0x4) >> 2;
|
||||
loclass_opt_successor(k, s, 0);
|
||||
bout |= (s->r & 0x4) >> 1;
|
||||
loclass_opt_successor(k, s, 0);
|
||||
bout |= (s->r & 0x4);
|
||||
loclass_opt_successor(k, s, 0);
|
||||
bout |= (s->r & 0x4) << 1;
|
||||
loclass_opt_successor(k, s, 0);
|
||||
bout |= (s->r & 0x4) << 2;
|
||||
loclass_opt_successor(k, s, 0);
|
||||
bout |= (s->r & 0x4) << 3;
|
||||
loclass_opt_successor(k, s, 0);
|
||||
bout |= (s->r & 0x4) << 4;
|
||||
loclass_opt_successor(k, s, 0);
|
||||
bout |= (s->r & 0x4) << 5;
|
||||
loclass_opt_successor(k, s, 0);
|
||||
buffer[times] = bout;
|
||||
}
|
||||
}
|
||||
|
||||
static void loclass_opt_MAC(uint8_t* k, uint8_t* input, uint8_t* out) {
|
||||
LoclassState_t _init = {
|
||||
((k[0] ^ 0x4c) + 0xEC) & 0xFF, // l
|
||||
((k[0] ^ 0x4c) + 0x21) & 0xFF, // r
|
||||
0x4c, // b
|
||||
0xE012 // t
|
||||
};
|
||||
|
||||
loclass_opt_suc(k, &_init, input, 12, false);
|
||||
loclass_opt_output(k, &_init, out);
|
||||
}
|
||||
|
||||
static void loclass_opt_MAC_N(uint8_t* k, uint8_t* input, uint8_t in_size, uint8_t* out) {
|
||||
LoclassState_t _init = {
|
||||
((k[0] ^ 0x4c) + 0xEC) & 0xFF, // l
|
||||
((k[0] ^ 0x4c) + 0x21) & 0xFF, // r
|
||||
0x4c, // b
|
||||
0xE012 // t
|
||||
};
|
||||
|
||||
loclass_opt_suc(k, &_init, input, in_size, false);
|
||||
loclass_opt_output(k, &_init, out);
|
||||
}
|
||||
|
||||
void loclass_opt_doReaderMAC(uint8_t* cc_nr_p, uint8_t* div_key_p, uint8_t mac[4]) {
|
||||
uint8_t dest[] = {0, 0, 0, 0, 0, 0, 0, 0};
|
||||
loclass_opt_MAC(div_key_p, cc_nr_p, dest);
|
||||
memcpy(mac, dest, 4);
|
||||
}
|
||||
|
||||
void loclass_opt_doReaderMAC_2(
|
||||
LoclassState_t _init,
|
||||
uint8_t* nr,
|
||||
uint8_t mac[4],
|
||||
const uint8_t* div_key_p) {
|
||||
loclass_opt_suc(div_key_p, &_init, nr, 4, false);
|
||||
loclass_opt_output(div_key_p, &_init, mac);
|
||||
}
|
||||
|
||||
void loclass_doMAC_N(uint8_t* in_p, uint8_t in_size, uint8_t* div_key_p, uint8_t mac[4]) {
|
||||
uint8_t dest[] = {0, 0, 0, 0, 0, 0, 0, 0};
|
||||
loclass_opt_MAC_N(div_key_p, in_p, in_size, dest);
|
||||
memcpy(mac, dest, 4);
|
||||
}
|
||||
|
||||
void loclass_opt_doTagMAC(uint8_t* cc_p, const uint8_t* div_key_p, uint8_t mac[4]) {
|
||||
LoclassState_t _init = {
|
||||
((div_key_p[0] ^ 0x4c) + 0xEC) & 0xFF, // l
|
||||
((div_key_p[0] ^ 0x4c) + 0x21) & 0xFF, // r
|
||||
0x4c, // b
|
||||
0xE012 // t
|
||||
};
|
||||
loclass_opt_suc(div_key_p, &_init, cc_p, 12, true);
|
||||
loclass_opt_output(div_key_p, &_init, mac);
|
||||
}
|
||||
|
||||
/**
|
||||
* The tag MAC can be divided (both can, but no point in dividing the reader mac) into
|
||||
* two functions, since the first 8 bytes are known, we can pre-calculate the state
|
||||
* reached after feeding CC to the cipher.
|
||||
* @param cc_p
|
||||
* @param div_key_p
|
||||
* @return the cipher state
|
||||
*/
|
||||
LoclassState_t loclass_opt_doTagMAC_1(uint8_t* cc_p, const uint8_t* div_key_p) {
|
||||
LoclassState_t _init = {
|
||||
((div_key_p[0] ^ 0x4c) + 0xEC) & 0xFF, // l
|
||||
((div_key_p[0] ^ 0x4c) + 0x21) & 0xFF, // r
|
||||
0x4c, // b
|
||||
0xE012 // t
|
||||
};
|
||||
loclass_opt_suc(div_key_p, &_init, cc_p, 8, false);
|
||||
return _init;
|
||||
}
|
||||
|
||||
/**
|
||||
* The second part of the tag MAC calculation, since the CC is already calculated into the state,
|
||||
* this function is fed only the NR, and internally feeds the remaining 32 0-bits to generate the tag
|
||||
* MAC response.
|
||||
* @param _init - precalculated cipher state
|
||||
* @param nr - the reader challenge
|
||||
* @param mac - where to store the MAC
|
||||
* @param div_key_p - the key to use
|
||||
*/
|
||||
void loclass_opt_doTagMAC_2(
|
||||
LoclassState_t _init,
|
||||
uint8_t* nr,
|
||||
uint8_t mac[4],
|
||||
const uint8_t* div_key_p) {
|
||||
loclass_opt_suc(div_key_p, &_init, nr, 4, true);
|
||||
loclass_opt_output(div_key_p, &_init, mac);
|
||||
}
|
||||
|
||||
void loclass_iclass_calc_div_key(uint8_t* csn, uint8_t* key, uint8_t* div_key, bool elite) {
|
||||
if(elite) {
|
||||
uint8_t keytable[128] = {0};
|
||||
uint8_t key_index[8] = {0};
|
||||
uint8_t key_sel[8] = {0};
|
||||
uint8_t key_sel_p[8] = {0};
|
||||
loclass_hash2(key, keytable);
|
||||
loclass_hash1(csn, key_index);
|
||||
for(uint8_t i = 0; i < 8; i++) key_sel[i] = keytable[key_index[i]];
|
||||
|
||||
//Permute from iclass format to standard format
|
||||
loclass_permutekey_rev(key_sel, key_sel_p);
|
||||
loclass_diversifyKey(csn, key_sel_p, div_key);
|
||||
} else {
|
||||
loclass_diversifyKey(csn, key, div_key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Borrowed initially from https://github.com/holiman/loclass
|
||||
// More recently from https://github.com/RfidResearchGroup/proxmark3
|
||||
// Copyright (C) 2014 Martin Holst Swende
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// WARNING
|
||||
//
|
||||
// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
|
||||
//
|
||||
// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
|
||||
// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
|
||||
// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
|
||||
//
|
||||
// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
|
||||
//-----------------------------------------------------------------------------
|
||||
// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
|
||||
//
|
||||
// The implementation is based on the work performed by
|
||||
// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
|
||||
// Milosch Meriac in the paper "Dismantling IClass".
|
||||
//-----------------------------------------------------------------------------
|
||||
#ifndef OPTIMIZED_CIPHER_H
|
||||
#define OPTIMIZED_CIPHER_H
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* Definition 1 (Cipher state). A cipher state of iClass s is an element of F 40/2
|
||||
* consisting of the following four components:
|
||||
* 1. the left register l = (l 0 . . . l 7 ) ∈ F 8/2 ;
|
||||
* 2. the right register r = (r 0 . . . r 7 ) ∈ F 8/2 ;
|
||||
* 3. the top register t = (t 0 . . . t 15 ) ∈ F 16/2 .
|
||||
* 4. the bottom register b = (b 0 . . . b 7 ) ∈ F 8/2 .
|
||||
**/
|
||||
typedef struct {
|
||||
uint8_t l;
|
||||
uint8_t r;
|
||||
uint8_t b;
|
||||
uint16_t t;
|
||||
} LoclassState_t;
|
||||
|
||||
/** The reader MAC is MAC(key, CC * NR )
|
||||
**/
|
||||
void loclass_opt_doReaderMAC(uint8_t* cc_nr_p, uint8_t* div_key_p, uint8_t mac[4]);
|
||||
|
||||
void loclass_opt_doReaderMAC_2(
|
||||
LoclassState_t _init,
|
||||
uint8_t* nr,
|
||||
uint8_t mac[4],
|
||||
const uint8_t* div_key_p);
|
||||
|
||||
/**
|
||||
* The tag MAC is MAC(key, CC * NR * 32x0))
|
||||
*/
|
||||
void loclass_opt_doTagMAC(uint8_t* cc_p, const uint8_t* div_key_p, uint8_t mac[4]);
|
||||
|
||||
/**
|
||||
* The tag MAC can be divided (both can, but no point in dividing the reader mac) into
|
||||
* two functions, since the first 8 bytes are known, we can pre-calculate the state
|
||||
* reached after feeding CC to the cipher.
|
||||
* @param cc_p
|
||||
* @param div_key_p
|
||||
* @return the cipher state
|
||||
*/
|
||||
LoclassState_t loclass_opt_doTagMAC_1(uint8_t* cc_p, const uint8_t* div_key_p);
|
||||
/**
|
||||
* The second part of the tag MAC calculation, since the CC is already calculated into the state,
|
||||
* this function is fed only the NR, and internally feeds the remaining 32 0-bits to generate the tag
|
||||
* MAC response.
|
||||
* @param _init - precalculated cipher state
|
||||
* @param nr - the reader challenge
|
||||
* @param mac - where to store the MAC
|
||||
* @param div_key_p - the key to use
|
||||
*/
|
||||
void loclass_opt_doTagMAC_2(
|
||||
LoclassState_t _init,
|
||||
uint8_t* nr,
|
||||
uint8_t mac[4],
|
||||
const uint8_t* div_key_p);
|
||||
|
||||
void loclass_doMAC_N(uint8_t* in_p, uint8_t in_size, uint8_t* div_key_p, uint8_t mac[4]);
|
||||
void loclass_iclass_calc_div_key(uint8_t* csn, uint8_t* key, uint8_t* div_key, bool elite);
|
||||
#endif // OPTIMIZED_CIPHER_H
|
||||
@@ -0,0 +1,136 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Borrowed initially from https://github.com/holiman/loclass
|
||||
// Copyright (C) 2014 Martin Holst Swende
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// WARNING
|
||||
//
|
||||
// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
|
||||
//
|
||||
// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
|
||||
// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
|
||||
// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
|
||||
//
|
||||
// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
|
||||
//-----------------------------------------------------------------------------
|
||||
// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
|
||||
//
|
||||
// The implementation is based on the work performed by
|
||||
// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
|
||||
// Milosch Meriac in the paper "Dismantling IClass".
|
||||
//-----------------------------------------------------------------------------
|
||||
#include "optimized_cipherutils.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
*
|
||||
* @brief Return and remove the first bit (x0) in the stream : <x0 x1 x2 x3 ... xn >
|
||||
* @param stream
|
||||
* @return
|
||||
*/
|
||||
bool loclass_headBit(LoclassBitstreamIn_t* stream) {
|
||||
int bytepos = stream->position >> 3; // divide by 8
|
||||
int bitpos = (stream->position++) & 7; // mask out 00000111
|
||||
return (*(stream->buffer + bytepos) >> (7 - bitpos)) & 1;
|
||||
}
|
||||
/**
|
||||
* @brief Return and remove the last bit (xn) in the stream: <x0 x1 x2 ... xn>
|
||||
* @param stream
|
||||
* @return
|
||||
*/
|
||||
bool loclass_tailBit(LoclassBitstreamIn_t* stream) {
|
||||
int bitpos = stream->numbits - 1 - (stream->position++);
|
||||
|
||||
int bytepos = bitpos >> 3;
|
||||
bitpos &= 7;
|
||||
return (*(stream->buffer + bytepos) >> (7 - bitpos)) & 1;
|
||||
}
|
||||
/**
|
||||
* @brief Pushes bit onto the stream
|
||||
* @param stream
|
||||
* @param bit
|
||||
*/
|
||||
void loclass_pushBit(LoclassBitstreamOut_t* stream, bool bit) {
|
||||
int bytepos = stream->position >> 3; // divide by 8
|
||||
int bitpos = stream->position & 7;
|
||||
*(stream->buffer + bytepos) |= (bit) << (7 - bitpos);
|
||||
stream->position++;
|
||||
stream->numbits++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pushes the lower six bits onto the stream
|
||||
* as b0 b1 b2 b3 b4 b5 b6
|
||||
* @param stream
|
||||
* @param bits
|
||||
*/
|
||||
void loclass_push6bits(LoclassBitstreamOut_t* stream, uint8_t bits) {
|
||||
loclass_pushBit(stream, bits & 0x20);
|
||||
loclass_pushBit(stream, bits & 0x10);
|
||||
loclass_pushBit(stream, bits & 0x08);
|
||||
loclass_pushBit(stream, bits & 0x04);
|
||||
loclass_pushBit(stream, bits & 0x02);
|
||||
loclass_pushBit(stream, bits & 0x01);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief loclass_bitsLeft
|
||||
* @param stream
|
||||
* @return number of bits left in stream
|
||||
*/
|
||||
int loclass_bitsLeft(LoclassBitstreamIn_t* stream) {
|
||||
return stream->numbits - stream->position;
|
||||
}
|
||||
/**
|
||||
* @brief numBits
|
||||
* @param stream
|
||||
* @return Number of bits stored in stream
|
||||
*/
|
||||
void loclass_x_num_to_bytes(uint64_t n, size_t len, uint8_t* dest) {
|
||||
while(len--) {
|
||||
dest[len] = (uint8_t)n;
|
||||
n >>= 8;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t loclass_x_bytes_to_num(uint8_t* src, size_t len) {
|
||||
uint64_t num = 0;
|
||||
while(len--) {
|
||||
num = (num << 8) | (*src);
|
||||
src++;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
uint8_t loclass_reversebytes(uint8_t b) {
|
||||
b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
|
||||
b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
|
||||
b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
|
||||
return b;
|
||||
}
|
||||
|
||||
void loclass_reverse_arraybytes(uint8_t* arr, size_t len) {
|
||||
uint8_t i;
|
||||
for(i = 0; i < len; i++) {
|
||||
arr[i] = loclass_reversebytes(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void loclass_reverse_arraycopy(uint8_t* arr, uint8_t* dest, size_t len) {
|
||||
uint8_t i;
|
||||
for(i = 0; i < len; i++) {
|
||||
dest[i] = loclass_reversebytes(arr[i]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Borrowed initially from https://github.com/holiman/loclass
|
||||
// More recently from https://github.com/RfidResearchGroup/proxmark3
|
||||
// Copyright (C) 2014 Martin Holst Swende
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// WARNING
|
||||
//
|
||||
// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
|
||||
//
|
||||
// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
|
||||
// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
|
||||
// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
|
||||
//
|
||||
// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
|
||||
//-----------------------------------------------------------------------------
|
||||
// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
|
||||
//
|
||||
// The implementation is based on the work performed by
|
||||
// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
|
||||
// Milosch Meriac in the paper "Dismantling IClass".
|
||||
//-----------------------------------------------------------------------------
|
||||
#ifndef CIPHERUTILS_H
|
||||
#define CIPHERUTILS_H
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t* buffer;
|
||||
uint8_t numbits;
|
||||
uint8_t position;
|
||||
} LoclassBitstreamIn_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t* buffer;
|
||||
uint8_t numbits;
|
||||
uint8_t position;
|
||||
} LoclassBitstreamOut_t;
|
||||
|
||||
bool loclass_headBit(LoclassBitstreamIn_t* stream);
|
||||
bool loclass_tailBit(LoclassBitstreamIn_t* stream);
|
||||
void loclass_pushBit(LoclassBitstreamOut_t* stream, bool bit);
|
||||
int loclass_bitsLeft(LoclassBitstreamIn_t* stream);
|
||||
|
||||
void loclass_push6bits(LoclassBitstreamOut_t* stream, uint8_t bits);
|
||||
void loclass_x_num_to_bytes(uint64_t n, size_t len, uint8_t* dest);
|
||||
uint64_t loclass_x_bytes_to_num(uint8_t* src, size_t len);
|
||||
uint8_t loclass_reversebytes(uint8_t b);
|
||||
void loclass_reverse_arraybytes(uint8_t* arr, size_t len);
|
||||
void loclass_reverse_arraycopy(uint8_t* arr, uint8_t* dest, size_t len);
|
||||
#endif // CIPHERUTILS_H
|
||||
@@ -0,0 +1,232 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Borrowed initially from https://github.com/holiman/loclass
|
||||
// Copyright (C) 2014 Martin Holst Swende
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// WARNING
|
||||
//
|
||||
// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
|
||||
//
|
||||
// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
|
||||
// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
|
||||
// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
|
||||
//
|
||||
// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
|
||||
//-----------------------------------------------------------------------------
|
||||
// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
|
||||
//
|
||||
// The implementation is based on the work performed by
|
||||
// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
|
||||
// Milosch Meriac in the paper "Dismantling IClass".
|
||||
//-----------------------------------------------------------------------------
|
||||
#include "optimized_elite.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <mbedtls/des.h>
|
||||
#include "optimized_ikeys.h"
|
||||
|
||||
/**
|
||||
* @brief Permutes a key from standard NIST format to Iclass specific format
|
||||
* from http://www.proxmark.org/forum/viewtopic.php?pid=11220#p11220
|
||||
*
|
||||
* If you loclass_permute [6c 8d 44 f9 2a 2d 01 bf] you get [8a 0d b9 88 bb a7 90 ea] as shown below.
|
||||
*
|
||||
* 1 0 1 1 1 1 1 1 bf
|
||||
* 0 0 0 0 0 0 0 1 01
|
||||
* 0 0 1 0 1 1 0 1 2d
|
||||
* 0 0 1 0 1 0 1 0 2a
|
||||
* 1 1 1 1 1 0 0 1 f9
|
||||
* 0 1 0 0 0 1 0 0 44
|
||||
* 1 0 0 0 1 1 0 1 8d
|
||||
* 0 1 1 0 1 1 0 0 6c
|
||||
*
|
||||
* 8 0 b 8 b a 9 e
|
||||
* a d 9 8 b 7 0 a
|
||||
*
|
||||
* @param key
|
||||
* @param dest
|
||||
*/
|
||||
void loclass_permutekey(const uint8_t key[8], uint8_t dest[8]) {
|
||||
int i;
|
||||
for(i = 0; i < 8; i++) {
|
||||
dest[i] = (((key[7] & (0x80 >> i)) >> (7 - i)) << 7) |
|
||||
(((key[6] & (0x80 >> i)) >> (7 - i)) << 6) |
|
||||
(((key[5] & (0x80 >> i)) >> (7 - i)) << 5) |
|
||||
(((key[4] & (0x80 >> i)) >> (7 - i)) << 4) |
|
||||
(((key[3] & (0x80 >> i)) >> (7 - i)) << 3) |
|
||||
(((key[2] & (0x80 >> i)) >> (7 - i)) << 2) |
|
||||
(((key[1] & (0x80 >> i)) >> (7 - i)) << 1) |
|
||||
(((key[0] & (0x80 >> i)) >> (7 - i)) << 0);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Permutes a key from iclass specific format to NIST format
|
||||
* @brief loclass_permutekey_rev
|
||||
* @param key
|
||||
* @param dest
|
||||
*/
|
||||
void loclass_permutekey_rev(const uint8_t key[8], uint8_t dest[8]) {
|
||||
int i;
|
||||
for(i = 0; i < 8; i++) {
|
||||
dest[7 - i] = (((key[0] & (0x80 >> i)) >> (7 - i)) << 7) |
|
||||
(((key[1] & (0x80 >> i)) >> (7 - i)) << 6) |
|
||||
(((key[2] & (0x80 >> i)) >> (7 - i)) << 5) |
|
||||
(((key[3] & (0x80 >> i)) >> (7 - i)) << 4) |
|
||||
(((key[4] & (0x80 >> i)) >> (7 - i)) << 3) |
|
||||
(((key[5] & (0x80 >> i)) >> (7 - i)) << 2) |
|
||||
(((key[6] & (0x80 >> i)) >> (7 - i)) << 1) |
|
||||
(((key[7] & (0x80 >> i)) >> (7 - i)) << 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for loclass_hash1
|
||||
* @brief loclass_rr
|
||||
* @param val
|
||||
* @return
|
||||
*/
|
||||
static uint8_t loclass_rr(uint8_t val) {
|
||||
return val >> 1 | ((val & 1) << 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for loclass_hash1
|
||||
* @brief rl
|
||||
* @param val
|
||||
* @return
|
||||
*/
|
||||
static uint8_t loclass_rl(uint8_t val) {
|
||||
return val << 1 | ((val & 0x80) >> 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for loclass_hash1
|
||||
* @brief loclass_swap
|
||||
* @param val
|
||||
* @return
|
||||
*/
|
||||
static uint8_t loclass_swap(uint8_t val) {
|
||||
return ((val >> 4) & 0xFF) | ((val & 0xFF) << 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash1 takes CSN as input, and determines what bytes in the keytable will be used
|
||||
* when constructing the K_sel.
|
||||
* @param csn the CSN used
|
||||
* @param k output
|
||||
*/
|
||||
void loclass_hash1(const uint8_t csn[], uint8_t k[]) {
|
||||
k[0] = csn[0] ^ csn[1] ^ csn[2] ^ csn[3] ^ csn[4] ^ csn[5] ^ csn[6] ^ csn[7];
|
||||
k[1] = csn[0] + csn[1] + csn[2] + csn[3] + csn[4] + csn[5] + csn[6] + csn[7];
|
||||
k[2] = loclass_rr(loclass_swap(csn[2] + k[1]));
|
||||
k[3] = loclass_rl(loclass_swap(csn[3] + k[0]));
|
||||
k[4] = ~loclass_rr(csn[4] + k[2]) + 1;
|
||||
k[5] = ~loclass_rl(csn[5] + k[3]) + 1;
|
||||
k[6] = loclass_rr(csn[6] + (k[4] ^ 0x3c));
|
||||
k[7] = loclass_rl(csn[7] + (k[5] ^ 0xc3));
|
||||
|
||||
k[7] &= 0x7F;
|
||||
k[6] &= 0x7F;
|
||||
k[5] &= 0x7F;
|
||||
k[4] &= 0x7F;
|
||||
k[3] &= 0x7F;
|
||||
k[2] &= 0x7F;
|
||||
k[1] &= 0x7F;
|
||||
k[0] &= 0x7F;
|
||||
}
|
||||
/**
|
||||
Definition 14. Define the rotate key function loclass_rk : (F 82 ) 8 × N → (F 82 ) 8 as
|
||||
loclass_rk(x [0] . . . x [7] , 0) = x [0] . . . x [7]
|
||||
loclass_rk(x [0] . . . x [7] , n + 1) = loclass_rk(loclass_rl(x [0] ) . . . loclass_rl(x [7] ), n)
|
||||
**/
|
||||
static void loclass_rk(uint8_t* key, uint8_t n, uint8_t* outp_key) {
|
||||
memcpy(outp_key, key, 8);
|
||||
uint8_t j;
|
||||
while(n-- > 0) {
|
||||
for(j = 0; j < 8; j++) outp_key[j] = loclass_rl(outp_key[j]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
static mbedtls_des_context loclass_ctx_enc;
|
||||
static mbedtls_des_context loclass_ctx_dec;
|
||||
|
||||
static void loclass_desdecrypt_iclass(uint8_t* iclass_key, uint8_t* input, uint8_t* output) {
|
||||
uint8_t key_std_format[8] = {0};
|
||||
loclass_permutekey_rev(iclass_key, key_std_format);
|
||||
mbedtls_des_setkey_dec(&loclass_ctx_dec, key_std_format);
|
||||
mbedtls_des_crypt_ecb(&loclass_ctx_dec, input, output);
|
||||
}
|
||||
|
||||
static void loclass_desencrypt_iclass(uint8_t* iclass_key, uint8_t* input, uint8_t* output) {
|
||||
uint8_t key_std_format[8] = {0};
|
||||
loclass_permutekey_rev(iclass_key, key_std_format);
|
||||
mbedtls_des_setkey_enc(&loclass_ctx_enc, key_std_format);
|
||||
mbedtls_des_crypt_ecb(&loclass_ctx_enc, input, output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Insert uint8_t[8] custom master key to calculate hash2 and return key_select.
|
||||
* @param key unpermuted custom key
|
||||
* @param loclass_hash1 loclass_hash1
|
||||
* @param key_sel output key_sel=h[loclass_hash1[i]]
|
||||
*/
|
||||
void hash2(uint8_t* key64, uint8_t* outp_keytable) {
|
||||
/**
|
||||
*Expected:
|
||||
* High Security Key Table
|
||||
|
||||
00 F1 35 59 A1 0D 5A 26 7F 18 60 0B 96 8A C0 25 C1
|
||||
10 BF A1 3B B0 FF 85 28 75 F2 1F C6 8F 0E 74 8F 21
|
||||
20 14 7A 55 16 C8 A9 7D B3 13 0C 5D C9 31 8D A9 B2
|
||||
30 A3 56 83 0F 55 7E DE 45 71 21 D2 6D C1 57 1C 9C
|
||||
40 78 2F 64 51 42 7B 64 30 FA 26 51 76 D3 E0 FB B6
|
||||
50 31 9F BF 2F 7E 4F 94 B4 BD 4F 75 91 E3 1B EB 42
|
||||
60 3F 88 6F B8 6C 2C 93 0D 69 2C D5 20 3C C1 61 95
|
||||
70 43 08 A0 2F FE B3 26 D7 98 0B 34 7B 47 70 A0 AB
|
||||
|
||||
**** The 64-bit HS Custom Key Value = 5B7C62C491C11B39 ******/
|
||||
uint8_t key64_negated[8] = {0};
|
||||
uint8_t z[8][8] = {{0}, {0}};
|
||||
uint8_t temp_output[8] = {0};
|
||||
|
||||
//calculate complement of key
|
||||
int i;
|
||||
for(i = 0; i < 8; i++) key64_negated[i] = ~key64[i];
|
||||
|
||||
// Once again, key is on iclass-format
|
||||
loclass_desencrypt_iclass(key64, key64_negated, z[0]);
|
||||
|
||||
uint8_t y[8][8] = {{0}, {0}};
|
||||
|
||||
// y[0]=DES_dec(z[0],~key)
|
||||
// Once again, key is on iclass-format
|
||||
loclass_desdecrypt_iclass(z[0], key64_negated, y[0]);
|
||||
|
||||
for(i = 1; i < 8; i++) {
|
||||
loclass_rk(key64, i, temp_output);
|
||||
loclass_desdecrypt_iclass(temp_output, z[i - 1], z[i]);
|
||||
loclass_desencrypt_iclass(temp_output, y[i - 1], y[i]);
|
||||
}
|
||||
|
||||
if(outp_keytable != NULL) {
|
||||
for(i = 0; i < 8; i++) {
|
||||
memcpy(outp_keytable + i * 16, y[i], 8);
|
||||
memcpy(outp_keytable + 8 + i * 16, z[i], 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Borrowed initially from https://github.com/holiman/loclass
|
||||
// More recently from https://github.com/RfidResearchGroup/proxmark3
|
||||
// Copyright (C) 2014 Martin Holst Swende
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// WARNING
|
||||
//
|
||||
// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
|
||||
//
|
||||
// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
|
||||
// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
|
||||
// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
|
||||
//
|
||||
// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
|
||||
//-----------------------------------------------------------------------------
|
||||
// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
|
||||
//
|
||||
// The implementation is based on the work performed by
|
||||
// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
|
||||
// Milosch Meriac in the paper "Dismantling IClass".
|
||||
//-----------------------------------------------------------------------------
|
||||
#ifndef ELITE_CRACK_H
|
||||
#define ELITE_CRACK_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
void loclass_permutekey(const uint8_t key[8], uint8_t dest[8]);
|
||||
/**
|
||||
* Permutes a key from iclass specific format to NIST format
|
||||
* @brief loclass_permutekey_rev
|
||||
* @param key
|
||||
* @param dest
|
||||
*/
|
||||
void loclass_permutekey_rev(const uint8_t key[8], uint8_t dest[8]);
|
||||
/**
|
||||
* Hash1 takes CSN as input, and determines what bytes in the keytable will be used
|
||||
* when constructing the K_sel.
|
||||
* @param csn the CSN used
|
||||
* @param k output
|
||||
*/
|
||||
void loclass_hash1(const uint8_t* csn, uint8_t* k);
|
||||
void loclass_hash2(uint8_t* key64, uint8_t* outp_keytable);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,320 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Borrowed initially from https://github.com/holiman/loclass
|
||||
// Copyright (C) 2014 Martin Holst Swende
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// WARNING
|
||||
//
|
||||
// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
|
||||
//
|
||||
// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
|
||||
// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
|
||||
// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
|
||||
//
|
||||
// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
|
||||
//-----------------------------------------------------------------------------
|
||||
// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
|
||||
//
|
||||
// The implementation is based on the work performed by
|
||||
// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
|
||||
// Milosch Meriac in the paper "Dismantling IClass".
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
From "Dismantling iclass":
|
||||
This section describes in detail the built-in key diversification algorithm of iClass.
|
||||
Besides the obvious purpose of deriving a card key from a master key, this
|
||||
algorithm intends to circumvent weaknesses in the cipher by preventing the
|
||||
usage of certain ‘weak’ keys. In order to compute a diversified key, the iClass
|
||||
reader first encrypts the card identity id with the master key K, using single
|
||||
DES. The resulting ciphertext is then input to a function called loclass_hash0 which
|
||||
outputs the diversified key k.
|
||||
|
||||
k = loclass_hash0(DES enc (id, K))
|
||||
|
||||
Here the DES encryption of id with master key K outputs a cryptogram c
|
||||
of 64 bits. These 64 bits are divided as c = x, y, z [0] , . . . , z [7] ∈ F 82 × F 82 × (F 62 ) 8
|
||||
which is used as input to the loclass_hash0 function. This function introduces some
|
||||
obfuscation by performing a number of permutations, complement and modulo
|
||||
operations, see Figure 2.5. Besides that, it checks for and removes patterns like
|
||||
similar key bytes, which could produce a strong bias in the cipher. Finally, the
|
||||
output of loclass_hash0 is the diversified card key k = k [0] , . . . , k [7] ∈ (F 82 ) 8 .
|
||||
|
||||
**/
|
||||
#include "optimized_ikeys.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <inttypes.h>
|
||||
#include <mbedtls/des.h>
|
||||
#include "optimized_cipherutils.h"
|
||||
|
||||
static const uint8_t loclass_pi[35] = {0x0F, 0x17, 0x1B, 0x1D, 0x1E, 0x27, 0x2B, 0x2D, 0x2E,
|
||||
0x33, 0x35, 0x39, 0x36, 0x3A, 0x3C, 0x47, 0x4B, 0x4D,
|
||||
0x4E, 0x53, 0x55, 0x56, 0x59, 0x5A, 0x5C, 0x63, 0x65,
|
||||
0x66, 0x69, 0x6A, 0x6C, 0x71, 0x72, 0x74, 0x78};
|
||||
|
||||
/**
|
||||
* @brief The key diversification algorithm uses 6-bit bytes.
|
||||
* This implementation uses 64 bit uint to pack seven of them into one
|
||||
* variable. When they are there, they are placed as follows:
|
||||
* XXXX XXXX N0 .... N7, occupying the last 48 bits.
|
||||
*
|
||||
* This function picks out one from such a collection
|
||||
* @param all
|
||||
* @param n bitnumber
|
||||
* @return
|
||||
*/
|
||||
static uint8_t loclass_getSixBitByte(uint64_t c, int n) {
|
||||
return (c >> (42 - 6 * n)) & 0x3F;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Puts back a six-bit 'byte' into a uint64_t.
|
||||
* @param c buffer
|
||||
* @param z the value to place there
|
||||
* @param n bitnumber.
|
||||
*/
|
||||
static void loclass_pushbackSixBitByte(uint64_t* c, uint8_t z, int n) {
|
||||
//0x XXXX YYYY ZZZZ ZZZZ ZZZZ
|
||||
// ^z0 ^z7
|
||||
//z0: 1111 1100 0000 0000
|
||||
|
||||
uint64_t masked = z & 0x3F;
|
||||
uint64_t eraser = 0x3F;
|
||||
masked <<= 42 - 6 * n;
|
||||
eraser <<= 42 - 6 * n;
|
||||
|
||||
//masked <<= 6*n;
|
||||
//eraser <<= 6*n;
|
||||
|
||||
eraser = ~eraser;
|
||||
(*c) &= eraser;
|
||||
(*c) |= masked;
|
||||
}
|
||||
/**
|
||||
* @brief Swaps the z-values.
|
||||
* If the input value has format XYZ0Z1...Z7, the output will have the format
|
||||
* XYZ7Z6...Z0 instead
|
||||
* @param c
|
||||
* @return
|
||||
*/
|
||||
static uint64_t loclass_swapZvalues(uint64_t c) {
|
||||
uint64_t newz = 0;
|
||||
loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 0), 7);
|
||||
loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 1), 6);
|
||||
loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 2), 5);
|
||||
loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 3), 4);
|
||||
loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 4), 3);
|
||||
loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 5), 2);
|
||||
loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 6), 1);
|
||||
loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 7), 0);
|
||||
newz |= (c & 0xFFFF000000000000);
|
||||
return newz;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 4 six-bit bytes chunked into a uint64_t,as 00..00a0a1a2a3
|
||||
*/
|
||||
static uint64_t loclass_ck(int i, int j, uint64_t z) {
|
||||
if(i == 1 && j == -1) {
|
||||
// loclass_ck(1, −1, z [0] . . . z [3] ) = z [0] . . . z [3]
|
||||
return z;
|
||||
} else if(j == -1) {
|
||||
// loclass_ck(i, −1, z [0] . . . z [3] ) = loclass_ck(i − 1, i − 2, z [0] . . . z [3] )
|
||||
return loclass_ck(i - 1, i - 2, z);
|
||||
}
|
||||
|
||||
if(loclass_getSixBitByte(z, i) == loclass_getSixBitByte(z, j)) {
|
||||
//loclass_ck(i, j − 1, z [0] . . . z [i] ← j . . . z [3] )
|
||||
uint64_t newz = 0;
|
||||
int c;
|
||||
for(c = 0; c < 4; c++) {
|
||||
uint8_t val = loclass_getSixBitByte(z, c);
|
||||
if(c == i)
|
||||
loclass_pushbackSixBitByte(&newz, j, c);
|
||||
else
|
||||
loclass_pushbackSixBitByte(&newz, val, c);
|
||||
}
|
||||
return loclass_ck(i, j - 1, newz);
|
||||
} else {
|
||||
return loclass_ck(i, j - 1, z);
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
||||
Definition 8.
|
||||
Let the function check : (F 62 ) 8 → (F 62 ) 8 be defined as
|
||||
check(z [0] . . . z [7] ) = loclass_ck(3, 2, z [0] . . . z [3] ) · loclass_ck(3, 2, z [4] . . . z [7] )
|
||||
|
||||
where loclass_ck : N × N × (F 62 ) 4 → (F 62 ) 4 is defined as
|
||||
|
||||
loclass_ck(1, −1, z [0] . . . z [3] ) = z [0] . . . z [3]
|
||||
loclass_ck(i, −1, z [0] . . . z [3] ) = loclass_ck(i − 1, i − 2, z [0] . . . z [3] )
|
||||
loclass_ck(i, j, z [0] . . . z [3] ) =
|
||||
loclass_ck(i, j − 1, z [0] . . . z [i] ← j . . . z [3] ), if z [i] = z [j] ;
|
||||
loclass_ck(i, j − 1, z [0] . . . z [3] ), otherwise
|
||||
|
||||
otherwise.
|
||||
**/
|
||||
|
||||
static uint64_t loclass_check(uint64_t z) {
|
||||
//These 64 bits are divided as c = x, y, z [0] , . . . , z [7]
|
||||
|
||||
// loclass_ck(3, 2, z [0] . . . z [3] )
|
||||
uint64_t ck1 = loclass_ck(3, 2, z);
|
||||
|
||||
// loclass_ck(3, 2, z [4] . . . z [7] )
|
||||
uint64_t ck2 = loclass_ck(3, 2, z << 24);
|
||||
|
||||
//The loclass_ck function will place the values
|
||||
// in the middle of z.
|
||||
ck1 &= 0x00000000FFFFFF000000;
|
||||
ck2 &= 0x00000000FFFFFF000000;
|
||||
|
||||
return ck1 | ck2 >> 24;
|
||||
}
|
||||
|
||||
static void loclass_permute(
|
||||
LoclassBitstreamIn_t* p_in,
|
||||
uint64_t z,
|
||||
int l,
|
||||
int r,
|
||||
LoclassBitstreamOut_t* out) {
|
||||
if(loclass_bitsLeft(p_in) == 0) return;
|
||||
|
||||
bool pn = loclass_tailBit(p_in);
|
||||
if(pn) { // pn = 1
|
||||
uint8_t zl = loclass_getSixBitByte(z, l);
|
||||
|
||||
loclass_push6bits(out, zl + 1);
|
||||
loclass_permute(p_in, z, l + 1, r, out);
|
||||
} else { // otherwise
|
||||
uint8_t zr = loclass_getSixBitByte(z, r);
|
||||
|
||||
loclass_push6bits(out, zr);
|
||||
loclass_permute(p_in, z, l, r + 1, out);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*Definition 11. Let the function loclass_hash0 : F 82 × F 82 × (F 62 ) 8 → (F 82 ) 8 be defined as
|
||||
* loclass_hash0(x, y, z [0] . . . z [7] ) = k [0] . . . k [7] where
|
||||
* z'[i] = (z[i] mod (63-i)) + i i = 0...3
|
||||
* z'[i+4] = (z[i+4] mod (64-i)) + i i = 0...3
|
||||
* ẑ = check(z');
|
||||
* @param c
|
||||
* @param k this is where the diversified key is put (should be 8 bytes)
|
||||
* @return
|
||||
*/
|
||||
void loclass_hash0(uint64_t c, uint8_t k[8]) {
|
||||
c = loclass_swapZvalues(c);
|
||||
|
||||
//These 64 bits are divided as c = x, y, z [0] , . . . , z [7]
|
||||
// x = 8 bits
|
||||
// y = 8 bits
|
||||
// z0-z7 6 bits each : 48 bits
|
||||
uint8_t x = (c & 0xFF00000000000000) >> 56;
|
||||
uint8_t y = (c & 0x00FF000000000000) >> 48;
|
||||
uint64_t zP = 0;
|
||||
|
||||
for(int n = 0; n < 4; n++) {
|
||||
uint8_t zn = loclass_getSixBitByte(c, n);
|
||||
uint8_t zn4 = loclass_getSixBitByte(c, n + 4);
|
||||
uint8_t _zn = (zn % (63 - n)) + n;
|
||||
uint8_t _zn4 = (zn4 % (64 - n)) + n;
|
||||
loclass_pushbackSixBitByte(&zP, _zn, n);
|
||||
loclass_pushbackSixBitByte(&zP, _zn4, n + 4);
|
||||
}
|
||||
|
||||
uint64_t zCaret = loclass_check(zP);
|
||||
uint8_t p = loclass_pi[x % 35];
|
||||
|
||||
if(x & 1) //Check if x7 is 1
|
||||
p = ~p;
|
||||
|
||||
LoclassBitstreamIn_t p_in = {&p, 8, 0};
|
||||
uint8_t outbuffer[] = {0, 0, 0, 0, 0, 0, 0, 0};
|
||||
LoclassBitstreamOut_t out = {outbuffer, 0, 0};
|
||||
loclass_permute(&p_in, zCaret, 0, 4, &out); //returns 48 bits? or 6 8-bytes
|
||||
|
||||
//Out is now a buffer containing six-bit bytes, should be 48 bits
|
||||
// if all went well
|
||||
//Shift z-values down onto the lower segment
|
||||
|
||||
uint64_t zTilde = loclass_x_bytes_to_num(outbuffer, sizeof(outbuffer));
|
||||
|
||||
zTilde >>= 16;
|
||||
|
||||
for(int i = 0; i < 8; i++) {
|
||||
// the key on index i is first a bit from y
|
||||
// then six bits from z,
|
||||
// then a bit from p
|
||||
|
||||
// Init with zeroes
|
||||
k[i] = 0;
|
||||
// First, place yi leftmost in k
|
||||
//k[i] |= (y << i) & 0x80 ;
|
||||
|
||||
// First, place y(7-i) leftmost in k
|
||||
k[i] |= (y << (7 - i)) & 0x80;
|
||||
|
||||
uint8_t zTilde_i = loclass_getSixBitByte(zTilde, i);
|
||||
// zTildeI is now on the form 00XXXXXX
|
||||
// with one leftshift, it'll be
|
||||
// 0XXXXXX0
|
||||
// So after leftshift, we can OR it into k
|
||||
// However, when doing complement, we need to
|
||||
// again MASK 0XXXXXX0 (0x7E)
|
||||
zTilde_i <<= 1;
|
||||
|
||||
//Finally, add bit from p or p-mod
|
||||
//Shift bit i into rightmost location (mask only after complement)
|
||||
uint8_t p_i = p >> i & 0x1;
|
||||
|
||||
if(k[i]) { // yi = 1
|
||||
k[i] |= ~zTilde_i & 0x7E;
|
||||
k[i] |= p_i & 1;
|
||||
k[i] += 1;
|
||||
|
||||
} else { // otherwise
|
||||
k[i] |= zTilde_i & 0x7E;
|
||||
k[i] |= (~p_i) & 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @brief Performs Elite-class key diversification
|
||||
* @param csn
|
||||
* @param key
|
||||
* @param div_key
|
||||
*/
|
||||
void loclass_diversifyKey(uint8_t* csn, const uint8_t* key, uint8_t* div_key) {
|
||||
mbedtls_des_context loclass_ctx_enc;
|
||||
|
||||
// Prepare the DES key
|
||||
mbedtls_des_setkey_enc(&loclass_ctx_enc, key);
|
||||
|
||||
uint8_t crypted_csn[8] = {0};
|
||||
|
||||
// Calculate DES(CSN, KEY)
|
||||
mbedtls_des_crypt_ecb(&loclass_ctx_enc, csn, crypted_csn);
|
||||
|
||||
//Calculate HASH0(DES))
|
||||
uint64_t c_csn = loclass_x_bytes_to_num(crypted_csn, sizeof(crypted_csn));
|
||||
|
||||
loclass_hash0(c_csn, div_key);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Borrowed initially from https://github.com/holiman/loclass
|
||||
// More recently from https://github.com/RfidResearchGroup/proxmark3
|
||||
// Copyright (C) 2014 Martin Holst Swende
|
||||
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// See LICENSE.txt for the text of the license.
|
||||
//-----------------------------------------------------------------------------
|
||||
// WARNING
|
||||
//
|
||||
// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
|
||||
//
|
||||
// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
|
||||
// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
|
||||
// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
|
||||
//
|
||||
// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
|
||||
//-----------------------------------------------------------------------------
|
||||
// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
|
||||
//
|
||||
// The implementation is based on the work performed by
|
||||
// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
|
||||
// Milosch Meriac in the paper "Dismantling IClass".
|
||||
//-----------------------------------------------------------------------------
|
||||
#ifndef IKEYS_H
|
||||
#define IKEYS_H
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*Definition 11. Let the function loclass_hash0 : F 82 × F 82 × (F 62 ) 8 → (F 82 ) 8 be defined as
|
||||
* loclass_hash0(x, y, z [0] . . . z [7] ) = k [0] . . . k [7] where
|
||||
* z'[i] = (z[i] mod (63-i)) + i i = 0...3
|
||||
* z'[i+4] = (z[i+4] mod (64-i)) + i i = 0...3
|
||||
* ẑ = check(z');
|
||||
* @param c
|
||||
* @param k this is where the diversified key is put (should be 8 bytes)
|
||||
* @return
|
||||
*/
|
||||
void loclass_hash0(uint64_t c, uint8_t k[8]);
|
||||
/**
|
||||
* @brief Performs Elite-class key diversification
|
||||
* @param csn
|
||||
* @param key
|
||||
* @param div_key
|
||||
*/
|
||||
|
||||
void loclass_diversifyKey(uint8_t* csn, const uint8_t* key, uint8_t* div_key);
|
||||
/**
|
||||
* @brief Permutes a key from standard NIST format to Iclass specific format
|
||||
* @param key
|
||||
* @param dest
|
||||
*/
|
||||
|
||||
#endif // IKEYS_H
|
||||
@@ -0,0 +1,185 @@
|
||||
#include "picopass_i.h"
|
||||
|
||||
#define TAG "PicoPass"
|
||||
|
||||
bool picopass_custom_event_callback(void* context, uint32_t event) {
|
||||
furi_assert(context);
|
||||
Picopass* picopass = context;
|
||||
return scene_manager_handle_custom_event(picopass->scene_manager, event);
|
||||
}
|
||||
|
||||
bool picopass_back_event_callback(void* context) {
|
||||
furi_assert(context);
|
||||
Picopass* picopass = context;
|
||||
return scene_manager_handle_back_event(picopass->scene_manager);
|
||||
}
|
||||
|
||||
void picopass_tick_event_callback(void* context) {
|
||||
furi_assert(context);
|
||||
Picopass* picopass = context;
|
||||
scene_manager_handle_tick_event(picopass->scene_manager);
|
||||
}
|
||||
|
||||
Picopass* picopass_alloc() {
|
||||
Picopass* picopass = malloc(sizeof(Picopass));
|
||||
|
||||
picopass->worker = picopass_worker_alloc();
|
||||
picopass->view_dispatcher = view_dispatcher_alloc();
|
||||
picopass->scene_manager = scene_manager_alloc(&picopass_scene_handlers, picopass);
|
||||
view_dispatcher_enable_queue(picopass->view_dispatcher);
|
||||
view_dispatcher_set_event_callback_context(picopass->view_dispatcher, picopass);
|
||||
view_dispatcher_set_custom_event_callback(
|
||||
picopass->view_dispatcher, picopass_custom_event_callback);
|
||||
view_dispatcher_set_navigation_event_callback(
|
||||
picopass->view_dispatcher, picopass_back_event_callback);
|
||||
view_dispatcher_set_tick_event_callback(
|
||||
picopass->view_dispatcher, picopass_tick_event_callback, 100);
|
||||
|
||||
// Picopass device
|
||||
picopass->dev = picopass_device_alloc();
|
||||
|
||||
// Open GUI record
|
||||
picopass->gui = furi_record_open(RECORD_GUI);
|
||||
view_dispatcher_attach_to_gui(
|
||||
picopass->view_dispatcher, picopass->gui, ViewDispatcherTypeFullscreen);
|
||||
|
||||
// Open Notification record
|
||||
picopass->notifications = furi_record_open(RECORD_NOTIFICATION);
|
||||
|
||||
// Submenu
|
||||
picopass->submenu = submenu_alloc();
|
||||
view_dispatcher_add_view(
|
||||
picopass->view_dispatcher, PicopassViewMenu, submenu_get_view(picopass->submenu));
|
||||
|
||||
// Popup
|
||||
picopass->popup = popup_alloc();
|
||||
view_dispatcher_add_view(
|
||||
picopass->view_dispatcher, PicopassViewPopup, popup_get_view(picopass->popup));
|
||||
|
||||
// Loading
|
||||
picopass->loading = loading_alloc();
|
||||
view_dispatcher_add_view(
|
||||
picopass->view_dispatcher, PicopassViewLoading, loading_get_view(picopass->loading));
|
||||
|
||||
// Text Input
|
||||
picopass->text_input = text_input_alloc();
|
||||
view_dispatcher_add_view(
|
||||
picopass->view_dispatcher,
|
||||
PicopassViewTextInput,
|
||||
text_input_get_view(picopass->text_input));
|
||||
|
||||
// Custom Widget
|
||||
picopass->widget = widget_alloc();
|
||||
view_dispatcher_add_view(
|
||||
picopass->view_dispatcher, PicopassViewWidget, widget_get_view(picopass->widget));
|
||||
|
||||
return picopass;
|
||||
}
|
||||
|
||||
void picopass_free(Picopass* picopass) {
|
||||
furi_assert(picopass);
|
||||
|
||||
// Picopass device
|
||||
picopass_device_free(picopass->dev);
|
||||
picopass->dev = NULL;
|
||||
|
||||
// Submenu
|
||||
view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewMenu);
|
||||
submenu_free(picopass->submenu);
|
||||
|
||||
// Popup
|
||||
view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewPopup);
|
||||
popup_free(picopass->popup);
|
||||
|
||||
// Loading
|
||||
view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewLoading);
|
||||
loading_free(picopass->loading);
|
||||
|
||||
// TextInput
|
||||
view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewTextInput);
|
||||
text_input_free(picopass->text_input);
|
||||
|
||||
// Custom Widget
|
||||
view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewWidget);
|
||||
widget_free(picopass->widget);
|
||||
|
||||
// Worker
|
||||
picopass_worker_stop(picopass->worker);
|
||||
picopass_worker_free(picopass->worker);
|
||||
|
||||
// View Dispatcher
|
||||
view_dispatcher_free(picopass->view_dispatcher);
|
||||
|
||||
// Scene Manager
|
||||
scene_manager_free(picopass->scene_manager);
|
||||
|
||||
// GUI
|
||||
furi_record_close(RECORD_GUI);
|
||||
picopass->gui = NULL;
|
||||
|
||||
// Notifications
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
picopass->notifications = NULL;
|
||||
|
||||
free(picopass);
|
||||
}
|
||||
|
||||
void picopass_text_store_set(Picopass* picopass, const char* text, ...) {
|
||||
va_list args;
|
||||
va_start(args, text);
|
||||
|
||||
vsnprintf(picopass->text_store, sizeof(picopass->text_store), text, args);
|
||||
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void picopass_text_store_clear(Picopass* picopass) {
|
||||
memset(picopass->text_store, 0, sizeof(picopass->text_store));
|
||||
}
|
||||
|
||||
static const NotificationSequence picopass_sequence_blink_start_blue = {
|
||||
&message_blink_start_10,
|
||||
&message_blink_set_color_blue,
|
||||
&message_do_not_reset,
|
||||
NULL,
|
||||
};
|
||||
|
||||
static const NotificationSequence picopass_sequence_blink_stop = {
|
||||
&message_blink_stop,
|
||||
NULL,
|
||||
};
|
||||
|
||||
void picopass_blink_start(Picopass* picopass) {
|
||||
notification_message(picopass->notifications, &picopass_sequence_blink_start_blue);
|
||||
}
|
||||
|
||||
void picopass_blink_stop(Picopass* picopass) {
|
||||
notification_message(picopass->notifications, &picopass_sequence_blink_stop);
|
||||
}
|
||||
|
||||
void picopass_show_loading_popup(void* context, bool show) {
|
||||
Picopass* picopass = context;
|
||||
TaskHandle_t timer_task = xTaskGetHandle(configTIMER_SERVICE_TASK_NAME);
|
||||
|
||||
if(show) {
|
||||
// Raise timer priority so that animations can play
|
||||
vTaskPrioritySet(timer_task, configMAX_PRIORITIES - 1);
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewLoading);
|
||||
} else {
|
||||
// Restore default timer priority
|
||||
vTaskPrioritySet(timer_task, configTIMER_TASK_PRIORITY);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t picopass_app(void* p) {
|
||||
UNUSED(p);
|
||||
Picopass* picopass = picopass_alloc();
|
||||
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneStart);
|
||||
|
||||
view_dispatcher_run(picopass->view_dispatcher);
|
||||
|
||||
picopass_free(picopass);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
typedef struct Picopass Picopass;
|
||||
@@ -0,0 +1,378 @@
|
||||
#include "picopass_device.h"
|
||||
|
||||
#include <toolbox/path.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
|
||||
#define TAG "PicopassDevice"
|
||||
|
||||
static const char* picopass_file_header = "Flipper Picopass device";
|
||||
static const uint32_t picopass_file_version = 1;
|
||||
|
||||
const uint8_t picopass_iclass_decryptionkey[] =
|
||||
{0xb4, 0x21, 0x2c, 0xca, 0xb7, 0xed, 0x21, 0x0f, 0x7b, 0x93, 0xd4, 0x59, 0x39, 0xc7, 0xdd, 0x36};
|
||||
|
||||
PicopassDevice* picopass_device_alloc() {
|
||||
PicopassDevice* picopass_dev = malloc(sizeof(PicopassDevice));
|
||||
picopass_dev->dev_data.pacs.legacy = false;
|
||||
picopass_dev->dev_data.pacs.se_enabled = false;
|
||||
picopass_dev->dev_data.pacs.pin_length = 0;
|
||||
picopass_dev->storage = furi_record_open(RECORD_STORAGE);
|
||||
picopass_dev->dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
string_init(picopass_dev->load_path);
|
||||
return picopass_dev;
|
||||
}
|
||||
|
||||
void picopass_device_set_name(PicopassDevice* dev, const char* name) {
|
||||
furi_assert(dev);
|
||||
|
||||
strlcpy(dev->dev_name, name, PICOPASS_DEV_NAME_MAX_LEN);
|
||||
}
|
||||
|
||||
static bool picopass_device_save_file(
|
||||
PicopassDevice* dev,
|
||||
const char* dev_name,
|
||||
const char* folder,
|
||||
const char* extension,
|
||||
bool use_load_path) {
|
||||
furi_assert(dev);
|
||||
|
||||
bool saved = false;
|
||||
FlipperFormat* file = flipper_format_file_alloc(dev->storage);
|
||||
PicopassPacs* pacs = &dev->dev_data.pacs;
|
||||
PicopassBlock* AA1 = dev->dev_data.AA1;
|
||||
string_t temp_str;
|
||||
string_init(temp_str);
|
||||
|
||||
do {
|
||||
if(use_load_path && !string_empty_p(dev->load_path)) {
|
||||
// Get directory name
|
||||
path_extract_dirname(string_get_cstr(dev->load_path), temp_str);
|
||||
// Create picopass directory if necessary
|
||||
if(!storage_simply_mkdir(dev->storage, string_get_cstr(temp_str))) break;
|
||||
// Make path to file to save
|
||||
string_cat_printf(temp_str, "/%s%s", dev_name, extension);
|
||||
} else {
|
||||
// Create picopass directory if necessary
|
||||
if(!storage_simply_mkdir(dev->storage, PICOPASS_APP_FOLDER)) break;
|
||||
// First remove picopass device file if it was saved
|
||||
string_printf(temp_str, "%s/%s%s", folder, dev_name, extension);
|
||||
}
|
||||
// Open file
|
||||
if(!flipper_format_file_open_always(file, string_get_cstr(temp_str))) break;
|
||||
|
||||
if(dev->format == PicopassDeviceSaveFormatHF) {
|
||||
uint32_t fc = pacs->record.FacilityCode;
|
||||
uint32_t cn = pacs->record.CardNumber;
|
||||
// Write header
|
||||
if(!flipper_format_write_header_cstr(file, picopass_file_header, picopass_file_version))
|
||||
break;
|
||||
if(pacs->record.valid) {
|
||||
if(!flipper_format_write_uint32(file, "Facility Code", &fc, 1)) break;
|
||||
if(!flipper_format_write_uint32(file, "Card Number", &cn, 1)) break;
|
||||
if(!flipper_format_write_hex(
|
||||
file, "Credential", pacs->credential, PICOPASS_BLOCK_LEN))
|
||||
break;
|
||||
if(pacs->pin_length > 0) {
|
||||
if(!flipper_format_write_hex(file, "PIN\t\t", pacs->pin0, PICOPASS_BLOCK_LEN))
|
||||
break;
|
||||
if(!flipper_format_write_hex(
|
||||
file, "PIN(cont.)\t", pacs->pin1, PICOPASS_BLOCK_LEN))
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!flipper_format_write_comment_cstr(file, "Picopass blocks")) break;
|
||||
bool block_saved = true;
|
||||
|
||||
size_t app_limit = AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0] < PICOPASS_MAX_APP_LIMIT ?
|
||||
AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0] :
|
||||
PICOPASS_MAX_APP_LIMIT;
|
||||
for(size_t i = 0; i < app_limit; i++) {
|
||||
string_printf(temp_str, "Block %d", i);
|
||||
if(!flipper_format_write_hex(
|
||||
file, string_get_cstr(temp_str), AA1[i].data, PICOPASS_BLOCK_LEN)) {
|
||||
block_saved = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!block_saved) break;
|
||||
} else if(dev->format == PicopassDeviceSaveFormatLF) {
|
||||
const char* lf_header = "Flipper RFID key";
|
||||
// Write header
|
||||
if(!flipper_format_write_header_cstr(file, lf_header, 1)) break;
|
||||
if(!flipper_format_write_comment_cstr(
|
||||
file,
|
||||
"This was generated from the Picopass plugin and may not match current lfrfid"))
|
||||
break;
|
||||
// When lfrfid supports more formats, update this
|
||||
if(!flipper_format_write_string_cstr(file, "Key type", "H10301")) break;
|
||||
uint8_t H10301[3] = {0};
|
||||
H10301[0] = pacs->record.FacilityCode;
|
||||
H10301[1] = pacs->record.CardNumber >> 8;
|
||||
H10301[2] = pacs->record.CardNumber & 0x00FF;
|
||||
if(!flipper_format_write_hex(file, "Data", H10301, 3)) break;
|
||||
}
|
||||
saved = true;
|
||||
} while(0);
|
||||
|
||||
if(!saved) {
|
||||
dialog_message_show_storage_error(dev->dialogs, "Can not save\nfile");
|
||||
}
|
||||
string_clear(temp_str);
|
||||
flipper_format_free(file);
|
||||
return saved;
|
||||
}
|
||||
|
||||
bool picopass_device_save(PicopassDevice* dev, const char* dev_name) {
|
||||
if(dev->format == PicopassDeviceSaveFormatHF) {
|
||||
return picopass_device_save_file(
|
||||
dev, dev_name, PICOPASS_APP_FOLDER, PICOPASS_APP_EXTENSION, true);
|
||||
} else if(dev->format == PicopassDeviceSaveFormatLF) {
|
||||
return picopass_device_save_file(dev, dev_name, ANY_PATH("lfrfid"), ".rfid", true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool picopass_device_load_data(PicopassDevice* dev, string_t path, bool show_dialog) {
|
||||
bool parsed = false;
|
||||
FlipperFormat* file = flipper_format_file_alloc(dev->storage);
|
||||
PicopassBlock* AA1 = dev->dev_data.AA1;
|
||||
PicopassPacs* pacs = &dev->dev_data.pacs;
|
||||
string_t temp_str;
|
||||
string_init(temp_str);
|
||||
bool deprecated_version = false;
|
||||
|
||||
if(dev->loading_cb) {
|
||||
dev->loading_cb(dev->loading_cb_ctx, true);
|
||||
}
|
||||
|
||||
do {
|
||||
if(!flipper_format_file_open_existing(file, string_get_cstr(path))) break;
|
||||
|
||||
// Read and verify file header
|
||||
uint32_t version = 0;
|
||||
if(!flipper_format_read_header(file, temp_str, &version)) break;
|
||||
if(string_cmp_str(temp_str, picopass_file_header) || (version != picopass_file_version)) {
|
||||
deprecated_version = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse header blocks
|
||||
bool block_read = true;
|
||||
for(size_t i = 0; i < 6; i++) {
|
||||
string_printf(temp_str, "Block %d", i);
|
||||
if(!flipper_format_read_hex(
|
||||
file, string_get_cstr(temp_str), AA1[i].data, PICOPASS_BLOCK_LEN)) {
|
||||
block_read = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
size_t app_limit = AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0];
|
||||
for(size_t i = 6; i < app_limit; i++) {
|
||||
string_printf(temp_str, "Block %d", i);
|
||||
if(!flipper_format_read_hex(
|
||||
file, string_get_cstr(temp_str), AA1[i].data, PICOPASS_BLOCK_LEN)) {
|
||||
block_read = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!block_read) break;
|
||||
|
||||
if(picopass_device_parse_credential(AA1, pacs) != ERR_NONE) break;
|
||||
if(picopass_device_parse_wiegand(pacs->credential, &pacs->record) != ERR_NONE) break;
|
||||
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
if(dev->loading_cb) {
|
||||
dev->loading_cb(dev->loading_cb_ctx, false);
|
||||
}
|
||||
|
||||
if((!parsed) && (show_dialog)) {
|
||||
if(deprecated_version) {
|
||||
dialog_message_show_storage_error(dev->dialogs, "File format deprecated");
|
||||
} else {
|
||||
dialog_message_show_storage_error(dev->dialogs, "Can not parse\nfile");
|
||||
}
|
||||
}
|
||||
|
||||
string_clear(temp_str);
|
||||
flipper_format_free(file);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
void picopass_device_clear(PicopassDevice* dev) {
|
||||
furi_assert(dev);
|
||||
|
||||
picopass_device_data_clear(&dev->dev_data);
|
||||
memset(&dev->dev_data, 0, sizeof(dev->dev_data));
|
||||
dev->format = PicopassDeviceSaveFormatHF;
|
||||
string_reset(dev->load_path);
|
||||
}
|
||||
|
||||
void picopass_device_free(PicopassDevice* picopass_dev) {
|
||||
furi_assert(picopass_dev);
|
||||
picopass_device_clear(picopass_dev);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
string_clear(picopass_dev->load_path);
|
||||
free(picopass_dev);
|
||||
}
|
||||
|
||||
bool picopass_file_select(PicopassDevice* dev) {
|
||||
furi_assert(dev);
|
||||
|
||||
// Input events and views are managed by file_browser
|
||||
string_t picopass_app_folder;
|
||||
string_init_set_str(picopass_app_folder, PICOPASS_APP_FOLDER);
|
||||
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
dialog_file_browser_set_basic_options(&browser_options, PICOPASS_APP_EXTENSION, &I_Nfc_10px);
|
||||
|
||||
bool res = dialog_file_browser_show(
|
||||
dev->dialogs, dev->load_path, picopass_app_folder, &browser_options);
|
||||
|
||||
string_clear(picopass_app_folder);
|
||||
if(res) {
|
||||
string_t filename;
|
||||
string_init(filename);
|
||||
path_extract_filename(dev->load_path, filename, true);
|
||||
strncpy(dev->dev_name, string_get_cstr(filename), PICOPASS_DEV_NAME_MAX_LEN);
|
||||
res = picopass_device_load_data(dev, dev->load_path, true);
|
||||
if(res) {
|
||||
picopass_device_set_name(dev, dev->dev_name);
|
||||
}
|
||||
string_clear(filename);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void picopass_device_data_clear(PicopassDeviceData* dev_data) {
|
||||
for(size_t i = 0; i < PICOPASS_MAX_APP_LIMIT; i++) {
|
||||
memset(dev_data->AA1[i].data, 0, sizeof(dev_data->AA1[i].data));
|
||||
}
|
||||
dev_data->pacs.legacy = false;
|
||||
dev_data->pacs.se_enabled = false;
|
||||
dev_data->pacs.pin_length = 0;
|
||||
}
|
||||
|
||||
bool picopass_device_delete(PicopassDevice* dev, bool use_load_path) {
|
||||
furi_assert(dev);
|
||||
|
||||
bool deleted = false;
|
||||
string_t file_path;
|
||||
string_init(file_path);
|
||||
|
||||
do {
|
||||
// Delete original file
|
||||
if(use_load_path && !string_empty_p(dev->load_path)) {
|
||||
string_set(file_path, dev->load_path);
|
||||
} else {
|
||||
string_printf(
|
||||
file_path, "%s/%s%s", PICOPASS_APP_FOLDER, dev->dev_name, PICOPASS_APP_EXTENSION);
|
||||
}
|
||||
if(!storage_simply_remove(dev->storage, string_get_cstr(file_path))) break;
|
||||
deleted = true;
|
||||
} while(0);
|
||||
|
||||
if(!deleted) {
|
||||
dialog_message_show_storage_error(dev->dialogs, "Can not remove file");
|
||||
}
|
||||
|
||||
string_clear(file_path);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
void picopass_device_set_loading_callback(
|
||||
PicopassDevice* dev,
|
||||
PicopassLoadingCallback callback,
|
||||
void* context) {
|
||||
furi_assert(dev);
|
||||
|
||||
dev->loading_cb = callback;
|
||||
dev->loading_cb_ctx = context;
|
||||
}
|
||||
|
||||
ReturnCode picopass_device_decrypt(uint8_t* enc_data, uint8_t* dec_data) {
|
||||
uint8_t key[32] = {0};
|
||||
memcpy(key, picopass_iclass_decryptionkey, sizeof(picopass_iclass_decryptionkey));
|
||||
mbedtls_des3_context ctx;
|
||||
mbedtls_des3_init(&ctx);
|
||||
mbedtls_des3_set2key_dec(&ctx, key);
|
||||
mbedtls_des3_crypt_ecb(&ctx, enc_data, dec_data);
|
||||
mbedtls_des3_free(&ctx);
|
||||
return ERR_NONE;
|
||||
}
|
||||
|
||||
ReturnCode picopass_device_parse_credential(PicopassBlock* AA1, PicopassPacs* pacs) {
|
||||
ReturnCode err;
|
||||
|
||||
// Thank you proxmark!
|
||||
pacs->legacy = (memcmp(AA1[5].data, "\xff\xff\xff\xff\xff\xff\xff\xff", 8) == 0);
|
||||
pacs->se_enabled = (memcmp(AA1[5].data, "\xff\xff\xff\x00\x06\xff\xff\xff", 8) == 0);
|
||||
|
||||
pacs->biometrics = AA1[6].data[4];
|
||||
pacs->pin_length = AA1[6].data[6] & 0x0F;
|
||||
pacs->encryption = AA1[6].data[7];
|
||||
|
||||
if(pacs->encryption == PicopassDeviceEncryption3DES) {
|
||||
FURI_LOG_D(TAG, "3DES Encrypted");
|
||||
err = picopass_device_decrypt(AA1[7].data, pacs->credential);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "decrypt error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = picopass_device_decrypt(AA1[8].data, pacs->pin0);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "decrypt error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = picopass_device_decrypt(AA1[9].data, pacs->pin1);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "decrypt error %d", err);
|
||||
return err;
|
||||
}
|
||||
} else if(pacs->encryption == PicopassDeviceEncryptionNone) {
|
||||
FURI_LOG_D(TAG, "No Encryption");
|
||||
memcpy(pacs->credential, AA1[7].data, PICOPASS_BLOCK_LEN);
|
||||
memcpy(pacs->pin0, AA1[8].data, PICOPASS_BLOCK_LEN);
|
||||
memcpy(pacs->pin1, AA1[9].data, PICOPASS_BLOCK_LEN);
|
||||
} else if(pacs->encryption == PicopassDeviceEncryptionDES) {
|
||||
FURI_LOG_D(TAG, "DES Encrypted");
|
||||
} else {
|
||||
FURI_LOG_D(TAG, "Unknown encryption");
|
||||
}
|
||||
|
||||
return ERR_NONE;
|
||||
}
|
||||
|
||||
ReturnCode picopass_device_parse_wiegand(uint8_t* data, PicopassWiegandRecord* record) {
|
||||
uint32_t* halves = (uint32_t*)data;
|
||||
if(halves[0] == 0) {
|
||||
uint8_t leading0s = __builtin_clz(REVERSE_BYTES_U32(halves[1]));
|
||||
record->bitLength = 31 - leading0s;
|
||||
} else {
|
||||
uint8_t leading0s = __builtin_clz(REVERSE_BYTES_U32(halves[0]));
|
||||
record->bitLength = 63 - leading0s;
|
||||
}
|
||||
FURI_LOG_D(TAG, "bitLength: %d", record->bitLength);
|
||||
|
||||
if(record->bitLength == 26) {
|
||||
uint8_t* v4 = data + 4;
|
||||
uint32_t bot = v4[3] | (v4[2] << 8) | (v4[1] << 16) | (v4[0] << 24);
|
||||
|
||||
record->CardNumber = (bot >> 1) & 0xFFFF;
|
||||
record->FacilityCode = (bot >> 17) & 0xFF;
|
||||
FURI_LOG_D(TAG, "FC:%u CN: %u\n", record->FacilityCode, record->CardNumber);
|
||||
record->valid = true;
|
||||
} else {
|
||||
record->CardNumber = 0;
|
||||
record->FacilityCode = 0;
|
||||
record->valid = false;
|
||||
}
|
||||
return ERR_NONE;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <storage/storage.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <mbedtls/des.h>
|
||||
|
||||
#include "rfal_picopass.h"
|
||||
#include "loclass/optimized_ikeys.h"
|
||||
#include "loclass/optimized_cipher.h"
|
||||
|
||||
#define PICOPASS_DEV_NAME_MAX_LEN 22
|
||||
#define PICOPASS_READER_DATA_MAX_SIZE 64
|
||||
#define PICOPASS_BLOCK_LEN 8
|
||||
#define PICOPASS_MAX_APP_LIMIT 32
|
||||
|
||||
#define PICOPASS_CSN_BLOCK_INDEX 0
|
||||
#define PICOPASS_CONFIG_BLOCK_INDEX 1
|
||||
#define PICOPASS_AIA_BLOCK_INDEX 5
|
||||
|
||||
#define PICOPASS_APP_FOLDER ANY_PATH("picopass")
|
||||
#define PICOPASS_APP_EXTENSION ".picopass"
|
||||
#define PICOPASS_APP_SHADOW_EXTENSION ".pas"
|
||||
|
||||
typedef void (*PicopassLoadingCallback)(void* context, bool state);
|
||||
|
||||
typedef enum {
|
||||
PicopassDeviceEncryptionUnknown = 0,
|
||||
PicopassDeviceEncryptionNone = 0x14,
|
||||
PicopassDeviceEncryptionDES = 0x15,
|
||||
PicopassDeviceEncryption3DES = 0x17,
|
||||
} PicopassEncryption;
|
||||
|
||||
typedef enum {
|
||||
PicopassDeviceSaveFormatHF,
|
||||
PicopassDeviceSaveFormatLF,
|
||||
} PicopassDeviceSaveFormat;
|
||||
|
||||
typedef struct {
|
||||
bool valid;
|
||||
uint8_t bitLength;
|
||||
uint8_t FacilityCode;
|
||||
uint16_t CardNumber;
|
||||
} PicopassWiegandRecord;
|
||||
|
||||
typedef struct {
|
||||
bool legacy;
|
||||
bool se_enabled;
|
||||
bool biometrics;
|
||||
uint8_t pin_length;
|
||||
PicopassEncryption encryption;
|
||||
uint8_t credential[8];
|
||||
uint8_t pin0[8];
|
||||
uint8_t pin1[8];
|
||||
PicopassWiegandRecord record;
|
||||
} PicopassPacs;
|
||||
|
||||
typedef struct {
|
||||
uint8_t data[PICOPASS_BLOCK_LEN];
|
||||
} PicopassBlock;
|
||||
|
||||
typedef struct {
|
||||
PicopassBlock AA1[PICOPASS_MAX_APP_LIMIT];
|
||||
PicopassPacs pacs;
|
||||
} PicopassDeviceData;
|
||||
|
||||
typedef struct {
|
||||
Storage* storage;
|
||||
DialogsApp* dialogs;
|
||||
PicopassDeviceData dev_data;
|
||||
char dev_name[PICOPASS_DEV_NAME_MAX_LEN + 1];
|
||||
string_t load_path;
|
||||
PicopassDeviceSaveFormat format;
|
||||
PicopassLoadingCallback loading_cb;
|
||||
void* loading_cb_ctx;
|
||||
|
||||
} PicopassDevice;
|
||||
|
||||
PicopassDevice* picopass_device_alloc();
|
||||
|
||||
void picopass_device_free(PicopassDevice* picopass_dev);
|
||||
|
||||
void picopass_device_set_name(PicopassDevice* dev, const char* name);
|
||||
|
||||
bool picopass_device_save(PicopassDevice* dev, const char* dev_name);
|
||||
|
||||
bool picopass_file_select(PicopassDevice* dev);
|
||||
|
||||
void picopass_device_data_clear(PicopassDeviceData* dev_data);
|
||||
|
||||
void picopass_device_clear(PicopassDevice* dev);
|
||||
|
||||
bool picopass_device_delete(PicopassDevice* dev, bool use_load_path);
|
||||
|
||||
void picopass_device_set_loading_callback(
|
||||
PicopassDevice* dev,
|
||||
PicopassLoadingCallback callback,
|
||||
void* context);
|
||||
|
||||
ReturnCode picopass_device_parse_credential(PicopassBlock* AA1, PicopassPacs* pacs);
|
||||
ReturnCode picopass_device_parse_wiegand(uint8_t* data, PicopassWiegandRecord* record);
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "picopass.h"
|
||||
#include "picopass_worker.h"
|
||||
#include "picopass_device.h"
|
||||
|
||||
#include "rfal_picopass.h"
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/scene_manager.h>
|
||||
#include <notification/notification_messages.h>
|
||||
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/modules/popup.h>
|
||||
#include <gui/modules/loading.h>
|
||||
#include <gui/modules/text_input.h>
|
||||
#include <gui/modules/widget.h>
|
||||
|
||||
#include <input/input.h>
|
||||
|
||||
#include "scenes/picopass_scene.h"
|
||||
|
||||
#include <storage/storage.h>
|
||||
#include <lib/toolbox/path.h>
|
||||
|
||||
#define PICOPASS_TEXT_STORE_SIZE 128
|
||||
|
||||
enum PicopassCustomEvent {
|
||||
// Reserve first 100 events for button types and indexes, starting from 0
|
||||
PicopassCustomEventReserved = 100,
|
||||
|
||||
PicopassCustomEventViewExit,
|
||||
PicopassCustomEventWorkerExit,
|
||||
PicopassCustomEventByteInputDone,
|
||||
PicopassCustomEventTextInputDone,
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
EventTypeTick,
|
||||
EventTypeKey,
|
||||
} EventType;
|
||||
|
||||
struct Picopass {
|
||||
PicopassWorker* worker;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
Gui* gui;
|
||||
NotificationApp* notifications;
|
||||
SceneManager* scene_manager;
|
||||
PicopassDevice* dev;
|
||||
|
||||
char text_store[PICOPASS_TEXT_STORE_SIZE + 1];
|
||||
string_t text_box_store;
|
||||
|
||||
// Common Views
|
||||
Submenu* submenu;
|
||||
Popup* popup;
|
||||
Loading* loading;
|
||||
TextInput* text_input;
|
||||
Widget* widget;
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
PicopassViewMenu,
|
||||
PicopassViewPopup,
|
||||
PicopassViewLoading,
|
||||
PicopassViewTextInput,
|
||||
PicopassViewWidget,
|
||||
} PicopassView;
|
||||
|
||||
Picopass* picopass_alloc();
|
||||
|
||||
void picopass_text_store_set(Picopass* picopass, const char* text, ...);
|
||||
|
||||
void picopass_text_store_clear(Picopass* picopass);
|
||||
|
||||
void picopass_blink_start(Picopass* picopass);
|
||||
|
||||
void picopass_blink_stop(Picopass* picopass);
|
||||
|
||||
void picopass_show_loading_popup(void* context, bool show);
|
||||
@@ -0,0 +1,349 @@
|
||||
#include "picopass_worker_i.h"
|
||||
|
||||
#define TAG "PicopassWorker"
|
||||
|
||||
const uint8_t picopass_iclass_key[] = {0xaf, 0xa7, 0x85, 0xa7, 0xda, 0xb3, 0x33, 0x78};
|
||||
const uint8_t picopass_factory_key[] = {0x76, 0x65, 0x54, 0x43, 0x32, 0x21, 0x10, 0x00};
|
||||
|
||||
static void picopass_worker_enable_field() {
|
||||
furi_hal_nfc_ll_txrx_on();
|
||||
furi_hal_nfc_exit_sleep();
|
||||
furi_hal_nfc_ll_poll();
|
||||
}
|
||||
|
||||
static ReturnCode picopass_worker_disable_field(ReturnCode rc) {
|
||||
furi_hal_nfc_ll_txrx_off();
|
||||
furi_hal_nfc_start_sleep();
|
||||
return rc;
|
||||
}
|
||||
|
||||
/***************************** Picopass Worker API *******************************/
|
||||
|
||||
PicopassWorker* picopass_worker_alloc() {
|
||||
PicopassWorker* picopass_worker = malloc(sizeof(PicopassWorker));
|
||||
|
||||
// Worker thread attributes
|
||||
picopass_worker->thread = furi_thread_alloc();
|
||||
furi_thread_set_name(picopass_worker->thread, "PicopassWorker");
|
||||
furi_thread_set_stack_size(picopass_worker->thread, 8192);
|
||||
furi_thread_set_callback(picopass_worker->thread, picopass_worker_task);
|
||||
furi_thread_set_context(picopass_worker->thread, picopass_worker);
|
||||
|
||||
picopass_worker->callback = NULL;
|
||||
picopass_worker->context = NULL;
|
||||
picopass_worker->storage = furi_record_open(RECORD_STORAGE);
|
||||
|
||||
picopass_worker_change_state(picopass_worker, PicopassWorkerStateReady);
|
||||
|
||||
return picopass_worker;
|
||||
}
|
||||
|
||||
void picopass_worker_free(PicopassWorker* picopass_worker) {
|
||||
furi_assert(picopass_worker);
|
||||
|
||||
furi_thread_free(picopass_worker->thread);
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
free(picopass_worker);
|
||||
}
|
||||
|
||||
PicopassWorkerState picopass_worker_get_state(PicopassWorker* picopass_worker) {
|
||||
return picopass_worker->state;
|
||||
}
|
||||
|
||||
void picopass_worker_start(
|
||||
PicopassWorker* picopass_worker,
|
||||
PicopassWorkerState state,
|
||||
PicopassDeviceData* dev_data,
|
||||
PicopassWorkerCallback callback,
|
||||
void* context) {
|
||||
furi_assert(picopass_worker);
|
||||
furi_assert(dev_data);
|
||||
|
||||
picopass_worker->callback = callback;
|
||||
picopass_worker->context = context;
|
||||
picopass_worker->dev_data = dev_data;
|
||||
picopass_worker_change_state(picopass_worker, state);
|
||||
furi_thread_start(picopass_worker->thread);
|
||||
}
|
||||
|
||||
void picopass_worker_stop(PicopassWorker* picopass_worker) {
|
||||
furi_assert(picopass_worker);
|
||||
if(picopass_worker->state == PicopassWorkerStateBroken ||
|
||||
picopass_worker->state == PicopassWorkerStateReady) {
|
||||
return;
|
||||
}
|
||||
picopass_worker_disable_field(ERR_NONE);
|
||||
|
||||
picopass_worker_change_state(picopass_worker, PicopassWorkerStateStop);
|
||||
furi_thread_join(picopass_worker->thread);
|
||||
}
|
||||
|
||||
void picopass_worker_change_state(PicopassWorker* picopass_worker, PicopassWorkerState state) {
|
||||
picopass_worker->state = state;
|
||||
}
|
||||
|
||||
/***************************** Picopass Worker Thread *******************************/
|
||||
|
||||
ReturnCode picopass_detect_card(int timeout) {
|
||||
UNUSED(timeout);
|
||||
|
||||
ReturnCode err;
|
||||
|
||||
err = rfalPicoPassPollerInitialize();
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerInitialize error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = rfalFieldOnAndStartGT();
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalFieldOnAndStartGT error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = rfalPicoPassPollerCheckPresence();
|
||||
if(err != ERR_RF_COLLISION) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerCheckPresence error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
return ERR_NONE;
|
||||
}
|
||||
|
||||
ReturnCode picopass_read_card(PicopassBlock* AA1) {
|
||||
rfalPicoPassIdentifyRes idRes;
|
||||
rfalPicoPassSelectRes selRes;
|
||||
rfalPicoPassReadCheckRes rcRes;
|
||||
rfalPicoPassCheckRes chkRes;
|
||||
|
||||
ReturnCode err;
|
||||
|
||||
uint8_t div_key[8] = {0};
|
||||
uint8_t mac[4] = {0};
|
||||
uint8_t ccnr[12] = {0};
|
||||
|
||||
err = rfalPicoPassPollerIdentify(&idRes);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerIdentify error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = rfalPicoPassPollerSelect(idRes.CSN, &selRes);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerSelect error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = rfalPicoPassPollerReadCheck(&rcRes);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerReadCheck error %d", err);
|
||||
return err;
|
||||
}
|
||||
memcpy(ccnr, rcRes.CCNR, sizeof(rcRes.CCNR)); // last 4 bytes left 0
|
||||
|
||||
loclass_diversifyKey(selRes.CSN, picopass_iclass_key, div_key);
|
||||
loclass_opt_doReaderMAC(ccnr, div_key, mac);
|
||||
|
||||
err = rfalPicoPassPollerCheck(mac, &chkRes);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerCheck error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
rfalPicoPassReadBlockRes csn;
|
||||
err = rfalPicoPassPollerReadBlock(PICOPASS_CSN_BLOCK_INDEX, &csn);
|
||||
memcpy(AA1[PICOPASS_CSN_BLOCK_INDEX].data, csn.data, sizeof(csn.data));
|
||||
|
||||
rfalPicoPassReadBlockRes cfg;
|
||||
err = rfalPicoPassPollerReadBlock(PICOPASS_CONFIG_BLOCK_INDEX, &cfg);
|
||||
memcpy(AA1[PICOPASS_CONFIG_BLOCK_INDEX].data, cfg.data, sizeof(cfg.data));
|
||||
|
||||
size_t app_limit = cfg.data[0] < PICOPASS_MAX_APP_LIMIT ? cfg.data[0] : PICOPASS_MAX_APP_LIMIT;
|
||||
|
||||
for(size_t i = 2; i < app_limit; i++) {
|
||||
FURI_LOG_D(TAG, "rfalPicoPassPollerReadBlock block %d", i);
|
||||
rfalPicoPassReadBlockRes block;
|
||||
err = rfalPicoPassPollerReadBlock(i, &block);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerReadBlock error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"rfalPicoPassPollerReadBlock %d %02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
i,
|
||||
block.data[0],
|
||||
block.data[1],
|
||||
block.data[2],
|
||||
block.data[3],
|
||||
block.data[4],
|
||||
block.data[5],
|
||||
block.data[6],
|
||||
block.data[7]);
|
||||
|
||||
memcpy(AA1[i].data, block.data, sizeof(block.data));
|
||||
}
|
||||
|
||||
return ERR_NONE;
|
||||
}
|
||||
|
||||
ReturnCode picopass_write_card(PicopassBlock* AA1) {
|
||||
rfalPicoPassIdentifyRes idRes;
|
||||
rfalPicoPassSelectRes selRes;
|
||||
rfalPicoPassReadCheckRes rcRes;
|
||||
rfalPicoPassCheckRes chkRes;
|
||||
|
||||
ReturnCode err;
|
||||
|
||||
uint8_t div_key[8] = {0};
|
||||
uint8_t mac[4] = {0};
|
||||
uint8_t ccnr[12] = {0};
|
||||
|
||||
err = rfalPicoPassPollerIdentify(&idRes);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerIdentify error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = rfalPicoPassPollerSelect(idRes.CSN, &selRes);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerSelect error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = rfalPicoPassPollerReadCheck(&rcRes);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerReadCheck error %d", err);
|
||||
return err;
|
||||
}
|
||||
memcpy(ccnr, rcRes.CCNR, sizeof(rcRes.CCNR)); // last 4 bytes left 0
|
||||
|
||||
loclass_diversifyKey(selRes.CSN, picopass_iclass_key, div_key);
|
||||
loclass_opt_doReaderMAC(ccnr, div_key, mac);
|
||||
|
||||
err = rfalPicoPassPollerCheck(mac, &chkRes);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerCheck error %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
for(size_t i = 6; i < 10; i++) {
|
||||
FURI_LOG_D(TAG, "rfalPicoPassPollerWriteBlock %d", i);
|
||||
uint8_t data[9] = {0};
|
||||
data[0] = i;
|
||||
memcpy(data + 1, AA1[i].data, RFAL_PICOPASS_MAX_BLOCK_LEN);
|
||||
loclass_doMAC_N(data, sizeof(data), div_key, mac);
|
||||
FURI_LOG_D(
|
||||
TAG,
|
||||
"loclass_doMAC_N %d %02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x",
|
||||
i,
|
||||
data[1],
|
||||
data[2],
|
||||
data[3],
|
||||
data[4],
|
||||
data[5],
|
||||
data[6],
|
||||
data[7],
|
||||
data[8],
|
||||
mac[0],
|
||||
mac[1],
|
||||
mac[2],
|
||||
mac[3]);
|
||||
|
||||
err = rfalPicoPassPollerWriteBlock(i, AA1[i].data, mac);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "rfalPicoPassPollerWriteBlock error %d", err);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
return ERR_NONE;
|
||||
}
|
||||
|
||||
int32_t picopass_worker_task(void* context) {
|
||||
PicopassWorker* picopass_worker = context;
|
||||
|
||||
picopass_worker_enable_field();
|
||||
if(picopass_worker->state == PicopassWorkerStateDetect) {
|
||||
picopass_worker_detect(picopass_worker);
|
||||
} else if(picopass_worker->state == PicopassWorkerStateWrite) {
|
||||
picopass_worker_write(picopass_worker);
|
||||
}
|
||||
picopass_worker_disable_field(ERR_NONE);
|
||||
|
||||
picopass_worker_change_state(picopass_worker, PicopassWorkerStateReady);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void picopass_worker_detect(PicopassWorker* picopass_worker) {
|
||||
picopass_device_data_clear(picopass_worker->dev_data);
|
||||
PicopassDeviceData* dev_data = picopass_worker->dev_data;
|
||||
|
||||
PicopassBlock* AA1 = dev_data->AA1;
|
||||
PicopassPacs* pacs = &dev_data->pacs;
|
||||
ReturnCode err;
|
||||
|
||||
PicopassWorkerEvent nextState = PicopassWorkerEventSuccess;
|
||||
|
||||
while(picopass_worker->state == PicopassWorkerStateDetect) {
|
||||
if(picopass_detect_card(1000) == ERR_NONE) {
|
||||
// Process first found device
|
||||
err = picopass_read_card(AA1);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "picopass_read_card error %d", err);
|
||||
nextState = PicopassWorkerEventFail;
|
||||
}
|
||||
|
||||
if(nextState == PicopassWorkerEventSuccess) {
|
||||
err = picopass_device_parse_credential(AA1, pacs);
|
||||
}
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "picopass_device_parse_credential error %d", err);
|
||||
nextState = PicopassWorkerEventFail;
|
||||
}
|
||||
|
||||
if(nextState == PicopassWorkerEventSuccess) {
|
||||
err = picopass_device_parse_wiegand(pacs->credential, &pacs->record);
|
||||
}
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "picopass_device_parse_wiegand error %d", err);
|
||||
nextState = PicopassWorkerEventFail;
|
||||
}
|
||||
|
||||
// Notify caller and exit
|
||||
if(picopass_worker->callback) {
|
||||
picopass_worker->callback(nextState, picopass_worker->context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
furi_delay_ms(100);
|
||||
}
|
||||
}
|
||||
|
||||
void picopass_worker_write(PicopassWorker* picopass_worker) {
|
||||
PicopassDeviceData* dev_data = picopass_worker->dev_data;
|
||||
PicopassBlock* AA1 = dev_data->AA1;
|
||||
ReturnCode err;
|
||||
PicopassWorkerEvent nextState = PicopassWorkerEventSuccess;
|
||||
|
||||
while(picopass_worker->state == PicopassWorkerStateWrite) {
|
||||
if(picopass_detect_card(1000) == ERR_NONE) {
|
||||
err = picopass_write_card(AA1);
|
||||
if(err != ERR_NONE) {
|
||||
FURI_LOG_E(TAG, "picopass_write_card error %d", err);
|
||||
nextState = PicopassWorkerEventFail;
|
||||
}
|
||||
|
||||
// Notify caller and exit
|
||||
if(picopass_worker->callback) {
|
||||
picopass_worker->callback(nextState, picopass_worker->context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
furi_delay_ms(100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "picopass_device.h"
|
||||
|
||||
typedef struct PicopassWorker PicopassWorker;
|
||||
|
||||
typedef enum {
|
||||
// Init states
|
||||
PicopassWorkerStateNone,
|
||||
PicopassWorkerStateBroken,
|
||||
PicopassWorkerStateReady,
|
||||
// Main worker states
|
||||
PicopassWorkerStateDetect,
|
||||
PicopassWorkerStateWrite,
|
||||
// Transition
|
||||
PicopassWorkerStateStop,
|
||||
} PicopassWorkerState;
|
||||
|
||||
typedef enum {
|
||||
// Reserve first 50 events for application events
|
||||
PicopassWorkerEventReserved = 50,
|
||||
|
||||
// Picopass worker common events
|
||||
PicopassWorkerEventSuccess,
|
||||
PicopassWorkerEventFail,
|
||||
PicopassWorkerEventNoCardDetected,
|
||||
|
||||
PicopassWorkerEventStartReading,
|
||||
} PicopassWorkerEvent;
|
||||
|
||||
typedef void (*PicopassWorkerCallback)(PicopassWorkerEvent event, void* context);
|
||||
|
||||
PicopassWorker* picopass_worker_alloc();
|
||||
|
||||
PicopassWorkerState picopass_worker_get_state(PicopassWorker* picopass_worker);
|
||||
|
||||
void picopass_worker_free(PicopassWorker* picopass_worker);
|
||||
|
||||
void picopass_worker_start(
|
||||
PicopassWorker* picopass_worker,
|
||||
PicopassWorkerState state,
|
||||
PicopassDeviceData* dev_data,
|
||||
PicopassWorkerCallback callback,
|
||||
void* context);
|
||||
|
||||
void picopass_worker_stop(PicopassWorker* picopass_worker);
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "picopass_worker.h"
|
||||
#include "picopass_i.h"
|
||||
|
||||
#include <furi.h>
|
||||
#include <lib/toolbox/stream/file_stream.h>
|
||||
|
||||
#include <furi_hal.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <st25r3916.h>
|
||||
#include <rfal_analogConfig.h>
|
||||
#include <rfal_rf.h>
|
||||
|
||||
#include <platform.h>
|
||||
|
||||
struct PicopassWorker {
|
||||
FuriThread* thread;
|
||||
Storage* storage;
|
||||
|
||||
PicopassDeviceData* dev_data;
|
||||
PicopassWorkerCallback callback;
|
||||
void* context;
|
||||
|
||||
PicopassWorkerState state;
|
||||
};
|
||||
|
||||
void picopass_worker_change_state(PicopassWorker* picopass_worker, PicopassWorkerState state);
|
||||
|
||||
int32_t picopass_worker_task(void* context);
|
||||
|
||||
void picopass_worker_detect(PicopassWorker* picopass_worker);
|
||||
void picopass_worker_write(PicopassWorker* picopass_worker);
|
||||
@@ -0,0 +1,215 @@
|
||||
#include "rfal_picopass.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define RFAL_PICOPASS_TXRX_FLAGS \
|
||||
(FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_TX_MANUAL | FURI_HAL_NFC_LL_TXRX_FLAGS_AGC_ON | \
|
||||
FURI_HAL_NFC_LL_TXRX_FLAGS_PAR_RX_REMV | FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_RX_KEEP)
|
||||
|
||||
#define TAG "RFAL_PICOPASS"
|
||||
|
||||
typedef struct {
|
||||
uint8_t CMD;
|
||||
uint8_t CSN[RFAL_PICOPASS_UID_LEN];
|
||||
} rfalPicoPassSelectReq;
|
||||
|
||||
typedef struct {
|
||||
uint8_t CMD;
|
||||
uint8_t null[4];
|
||||
uint8_t mac[4];
|
||||
} rfalPicoPassCheckReq;
|
||||
|
||||
static uint16_t rfalPicoPassUpdateCcitt(uint16_t crcSeed, uint8_t dataByte) {
|
||||
uint16_t crc = crcSeed;
|
||||
uint8_t dat = dataByte;
|
||||
|
||||
dat ^= (uint8_t)(crc & 0xFFU);
|
||||
dat ^= (dat << 4);
|
||||
|
||||
crc = (crc >> 8) ^ (((uint16_t)dat) << 8) ^ (((uint16_t)dat) << 3) ^ (((uint16_t)dat) >> 4);
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
static uint16_t
|
||||
rfalPicoPassCalculateCcitt(uint16_t preloadValue, const uint8_t* buf, uint16_t length) {
|
||||
uint16_t crc = preloadValue;
|
||||
uint16_t index;
|
||||
|
||||
for(index = 0; index < length; index++) {
|
||||
crc = rfalPicoPassUpdateCcitt(crc, buf[index]);
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
FuriHalNfcReturn rfalPicoPassPollerInitialize(void) {
|
||||
FuriHalNfcReturn ret;
|
||||
|
||||
ret = furi_hal_nfc_ll_set_mode(
|
||||
FuriHalNfcModePollPicopass, FuriHalNfcBitrate26p48, FuriHalNfcBitrate26p48);
|
||||
if(ret != FuriHalNfcReturnOk) {
|
||||
return ret;
|
||||
};
|
||||
|
||||
furi_hal_nfc_ll_set_error_handling(FuriHalNfcErrorHandlingNfc);
|
||||
furi_hal_nfc_ll_set_guard_time(FURI_HAL_NFC_LL_GT_PICOPASS);
|
||||
furi_hal_nfc_ll_set_fdt_listen(FURI_HAL_NFC_LL_FDT_LISTEN_PICOPASS_POLLER);
|
||||
furi_hal_nfc_ll_set_fdt_poll(FURI_HAL_NFC_LL_FDT_POLL_PICOPASS_POLLER);
|
||||
|
||||
return FuriHalNfcReturnOk;
|
||||
}
|
||||
|
||||
FuriHalNfcReturn rfalPicoPassPollerCheckPresence(void) {
|
||||
FuriHalNfcReturn ret;
|
||||
uint8_t txBuf[1] = {RFAL_PICOPASS_CMD_ACTALL};
|
||||
uint8_t rxBuf[32] = {0};
|
||||
uint16_t recvLen = 0;
|
||||
uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
|
||||
uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
|
||||
|
||||
ret = furi_hal_nfc_ll_txrx(txBuf, 1, rxBuf, 32, &recvLen, flags, fwt);
|
||||
return ret;
|
||||
}
|
||||
|
||||
FuriHalNfcReturn rfalPicoPassPollerIdentify(rfalPicoPassIdentifyRes* idRes) {
|
||||
FuriHalNfcReturn ret;
|
||||
|
||||
uint8_t txBuf[1] = {RFAL_PICOPASS_CMD_IDENTIFY};
|
||||
uint16_t recvLen = 0;
|
||||
uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
|
||||
uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
|
||||
|
||||
ret = furi_hal_nfc_ll_txrx(
|
||||
txBuf,
|
||||
sizeof(txBuf),
|
||||
(uint8_t*)idRes,
|
||||
sizeof(rfalPicoPassIdentifyRes),
|
||||
&recvLen,
|
||||
flags,
|
||||
fwt);
|
||||
// printf("identify rx: %d %s\n", recvLen, hex2Str(idRes->CSN, RFAL_PICOPASS_UID_LEN));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FuriHalNfcReturn rfalPicoPassPollerSelect(uint8_t* csn, rfalPicoPassSelectRes* selRes) {
|
||||
FuriHalNfcReturn ret;
|
||||
|
||||
rfalPicoPassSelectReq selReq;
|
||||
selReq.CMD = RFAL_PICOPASS_CMD_SELECT;
|
||||
ST_MEMCPY(selReq.CSN, csn, RFAL_PICOPASS_UID_LEN);
|
||||
uint16_t recvLen = 0;
|
||||
uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
|
||||
uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
|
||||
|
||||
ret = furi_hal_nfc_ll_txrx(
|
||||
(uint8_t*)&selReq,
|
||||
sizeof(rfalPicoPassSelectReq),
|
||||
(uint8_t*)selRes,
|
||||
sizeof(rfalPicoPassSelectRes),
|
||||
&recvLen,
|
||||
flags,
|
||||
fwt);
|
||||
// printf("select rx: %d %s\n", recvLen, hex2Str(selRes->CSN, RFAL_PICOPASS_UID_LEN));
|
||||
if(ret == FuriHalNfcReturnTimeout) {
|
||||
return FuriHalNfcReturnOk;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FuriHalNfcReturn rfalPicoPassPollerReadCheck(rfalPicoPassReadCheckRes* rcRes) {
|
||||
FuriHalNfcReturn ret;
|
||||
uint8_t txBuf[2] = {RFAL_PICOPASS_CMD_READCHECK, 0x02};
|
||||
uint16_t recvLen = 0;
|
||||
uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
|
||||
uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
|
||||
|
||||
ret = furi_hal_nfc_ll_txrx(
|
||||
txBuf,
|
||||
sizeof(txBuf),
|
||||
(uint8_t*)rcRes,
|
||||
sizeof(rfalPicoPassReadCheckRes),
|
||||
&recvLen,
|
||||
flags,
|
||||
fwt);
|
||||
// printf("readcheck rx: %d %s\n", recvLen, hex2Str(rcRes->CCNR, 8));
|
||||
|
||||
if(ret == FuriHalNfcReturnCrc) {
|
||||
return FuriHalNfcReturnOk;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FuriHalNfcReturn rfalPicoPassPollerCheck(uint8_t* mac, rfalPicoPassCheckRes* chkRes) {
|
||||
FuriHalNfcReturn ret;
|
||||
rfalPicoPassCheckReq chkReq;
|
||||
chkReq.CMD = RFAL_PICOPASS_CMD_CHECK;
|
||||
ST_MEMCPY(chkReq.mac, mac, 4);
|
||||
ST_MEMSET(chkReq.null, 0, 4);
|
||||
uint16_t recvLen = 0;
|
||||
uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
|
||||
uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
|
||||
|
||||
// printf("check tx: %s\n", hex2Str((uint8_t *)&chkReq, sizeof(rfalPicoPassCheckReq)));
|
||||
ret = furi_hal_nfc_ll_txrx(
|
||||
(uint8_t*)&chkReq,
|
||||
sizeof(rfalPicoPassCheckReq),
|
||||
(uint8_t*)chkRes,
|
||||
sizeof(rfalPicoPassCheckRes),
|
||||
&recvLen,
|
||||
flags,
|
||||
fwt);
|
||||
// printf("check rx: %d %s\n", recvLen, hex2Str(chkRes->mac, 4));
|
||||
if(ret == FuriHalNfcReturnCrc) {
|
||||
return FuriHalNfcReturnOk;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FuriHalNfcReturn rfalPicoPassPollerReadBlock(uint8_t blockNum, rfalPicoPassReadBlockRes* readRes) {
|
||||
FuriHalNfcReturn ret;
|
||||
|
||||
uint8_t txBuf[4] = {RFAL_PICOPASS_CMD_READ, 0, 0, 0};
|
||||
txBuf[1] = blockNum;
|
||||
uint16_t crc = rfalPicoPassCalculateCcitt(0xE012, txBuf + 1, 1);
|
||||
memcpy(txBuf + 2, &crc, sizeof(uint16_t));
|
||||
|
||||
uint16_t recvLen = 0;
|
||||
uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
|
||||
uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
|
||||
|
||||
ret = furi_hal_nfc_ll_txrx(
|
||||
txBuf,
|
||||
sizeof(txBuf),
|
||||
(uint8_t*)readRes,
|
||||
sizeof(rfalPicoPassReadBlockRes),
|
||||
&recvLen,
|
||||
flags,
|
||||
fwt);
|
||||
return ret;
|
||||
}
|
||||
|
||||
FuriHalNfcReturn rfalPicoPassPollerWriteBlock(uint8_t blockNum, uint8_t data[8], uint8_t mac[4]) {
|
||||
FuriHalNfcReturn ret;
|
||||
|
||||
uint8_t txBuf[14] = {RFAL_PICOPASS_CMD_WRITE, blockNum, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
memcpy(txBuf + 2, data, RFAL_PICOPASS_MAX_BLOCK_LEN);
|
||||
memcpy(txBuf + 10, mac, 4);
|
||||
|
||||
uint16_t recvLen = 0;
|
||||
uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
|
||||
uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
|
||||
rfalPicoPassReadBlockRes block;
|
||||
|
||||
ret = furi_hal_nfc_ll_txrx(
|
||||
txBuf, sizeof(txBuf), (uint8_t*)&block, sizeof(block), &recvLen, flags, fwt);
|
||||
|
||||
if(ret == FuriHalNfcReturnOk) {
|
||||
// TODO: compare response
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi_hal_nfc.h>
|
||||
|
||||
#define RFAL_PICOPASS_UID_LEN 8
|
||||
#define RFAL_PICOPASS_MAX_BLOCK_LEN 8
|
||||
|
||||
enum {
|
||||
RFAL_PICOPASS_CMD_ACTALL = 0x0A,
|
||||
RFAL_PICOPASS_CMD_IDENTIFY = 0x0C,
|
||||
RFAL_PICOPASS_CMD_SELECT = 0x81,
|
||||
RFAL_PICOPASS_CMD_READCHECK = 0x88,
|
||||
RFAL_PICOPASS_CMD_CHECK = 0x05,
|
||||
RFAL_PICOPASS_CMD_READ = 0x0C,
|
||||
RFAL_PICOPASS_CMD_WRITE = 0x87,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint8_t CSN[RFAL_PICOPASS_UID_LEN]; // Anti-collision CSN
|
||||
uint8_t crc[2];
|
||||
} rfalPicoPassIdentifyRes;
|
||||
|
||||
typedef struct {
|
||||
uint8_t CSN[RFAL_PICOPASS_UID_LEN]; // Real CSN
|
||||
uint8_t crc[2];
|
||||
} rfalPicoPassSelectRes;
|
||||
|
||||
typedef struct {
|
||||
uint8_t CCNR[8];
|
||||
} rfalPicoPassReadCheckRes;
|
||||
|
||||
typedef struct {
|
||||
uint8_t mac[4];
|
||||
} rfalPicoPassCheckRes;
|
||||
|
||||
typedef struct {
|
||||
uint8_t data[RFAL_PICOPASS_MAX_BLOCK_LEN];
|
||||
uint8_t crc[2];
|
||||
} rfalPicoPassReadBlockRes;
|
||||
|
||||
FuriHalNfcReturn rfalPicoPassPollerInitialize(void);
|
||||
FuriHalNfcReturn rfalPicoPassPollerCheckPresence(void);
|
||||
FuriHalNfcReturn rfalPicoPassPollerIdentify(rfalPicoPassIdentifyRes* idRes);
|
||||
FuriHalNfcReturn rfalPicoPassPollerSelect(uint8_t* csn, rfalPicoPassSelectRes* selRes);
|
||||
FuriHalNfcReturn rfalPicoPassPollerReadCheck(rfalPicoPassReadCheckRes* rcRes);
|
||||
FuriHalNfcReturn rfalPicoPassPollerCheck(uint8_t* mac, rfalPicoPassCheckRes* chkRes);
|
||||
FuriHalNfcReturn rfalPicoPassPollerReadBlock(uint8_t blockNum, rfalPicoPassReadBlockRes* readRes);
|
||||
FuriHalNfcReturn rfalPicoPassPollerWriteBlock(uint8_t blockNum, uint8_t data[8], uint8_t mac[4]);
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "picopass_scene.h"
|
||||
|
||||
// Generate scene on_enter handlers array
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
|
||||
void (*const picopass_on_enter_handlers[])(void*) = {
|
||||
#include "picopass_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 picopass_on_event_handlers[])(void* context, SceneManagerEvent event) = {
|
||||
#include "picopass_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 picopass_on_exit_handlers[])(void* context) = {
|
||||
#include "picopass_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Initialize scene handlers configuration structure
|
||||
const SceneManagerHandlers picopass_scene_handlers = {
|
||||
.on_enter_handlers = picopass_on_enter_handlers,
|
||||
.on_event_handlers = picopass_on_event_handlers,
|
||||
.on_exit_handlers = picopass_on_exit_handlers,
|
||||
.scene_num = PicopassSceneNum,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/scene_manager.h>
|
||||
|
||||
// Generate scene id and total number
|
||||
#define ADD_SCENE(prefix, name, id) PicopassScene##id,
|
||||
typedef enum {
|
||||
#include "picopass_scene_config.h"
|
||||
PicopassSceneNum,
|
||||
} PicopassScene;
|
||||
#undef ADD_SCENE
|
||||
|
||||
extern const SceneManagerHandlers picopass_scene_handlers;
|
||||
|
||||
// Generate scene on_enter handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
|
||||
#include "picopass_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 "picopass_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 "picopass_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "../picopass_i.h"
|
||||
|
||||
enum SubmenuIndex {
|
||||
SubmenuIndexSave,
|
||||
SubmenuIndexSaveAsLF,
|
||||
};
|
||||
|
||||
void picopass_scene_card_menu_submenu_callback(void* context, uint32_t index) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void picopass_scene_card_menu_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
Submenu* submenu = picopass->submenu;
|
||||
|
||||
submenu_add_item(
|
||||
submenu, "Save", SubmenuIndexSave, picopass_scene_card_menu_submenu_callback, picopass);
|
||||
if(picopass->dev->dev_data.pacs.record.valid) {
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"Save as LF",
|
||||
SubmenuIndexSaveAsLF,
|
||||
picopass_scene_card_menu_submenu_callback,
|
||||
picopass);
|
||||
}
|
||||
submenu_set_selected_item(
|
||||
picopass->submenu,
|
||||
scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneCardMenu));
|
||||
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewMenu);
|
||||
}
|
||||
|
||||
bool picopass_scene_card_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == SubmenuIndexSave) {
|
||||
scene_manager_set_scene_state(
|
||||
picopass->scene_manager, PicopassSceneCardMenu, SubmenuIndexSave);
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
|
||||
picopass->dev->format = PicopassDeviceSaveFormatHF;
|
||||
consumed = true;
|
||||
} else if(event.event == SubmenuIndexSaveAsLF) {
|
||||
scene_manager_set_scene_state(
|
||||
picopass->scene_manager, PicopassSceneCardMenu, SubmenuIndexSaveAsLF);
|
||||
picopass->dev->format = PicopassDeviceSaveFormatLF;
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
|
||||
consumed = true;
|
||||
}
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
consumed = scene_manager_search_and_switch_to_previous_scene(
|
||||
picopass->scene_manager, PicopassSceneStart);
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_card_menu_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
submenu_reset(picopass->submenu);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
ADD_SCENE(picopass, start, Start)
|
||||
ADD_SCENE(picopass, read_card, ReadCard)
|
||||
ADD_SCENE(picopass, read_card_success, ReadCardSuccess)
|
||||
ADD_SCENE(picopass, card_menu, CardMenu)
|
||||
ADD_SCENE(picopass, save_name, SaveName)
|
||||
ADD_SCENE(picopass, save_success, SaveSuccess)
|
||||
ADD_SCENE(picopass, saved_menu, SavedMenu)
|
||||
ADD_SCENE(picopass, file_select, FileSelect)
|
||||
ADD_SCENE(picopass, device_info, DeviceInfo)
|
||||
ADD_SCENE(picopass, delete, Delete)
|
||||
ADD_SCENE(picopass, delete_success, DeleteSuccess)
|
||||
ADD_SCENE(picopass, write_card, WriteCard)
|
||||
ADD_SCENE(picopass, write_card_success, WriteCardSuccess)
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "../picopass_i.h"
|
||||
|
||||
void picopass_scene_delete_widget_callback(GuiButtonType result, InputType type, void* context) {
|
||||
Picopass* picopass = context;
|
||||
if(type == InputTypeShort) {
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
|
||||
}
|
||||
}
|
||||
|
||||
void picopass_scene_delete_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Setup Custom Widget view
|
||||
char temp_str[64];
|
||||
snprintf(temp_str, sizeof(temp_str), "\e#Delete %s?\e#", picopass->dev->dev_name);
|
||||
widget_add_text_box_element(
|
||||
picopass->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, temp_str, false);
|
||||
widget_add_button_element(
|
||||
picopass->widget,
|
||||
GuiButtonTypeLeft,
|
||||
"Back",
|
||||
picopass_scene_delete_widget_callback,
|
||||
picopass);
|
||||
widget_add_button_element(
|
||||
picopass->widget,
|
||||
GuiButtonTypeRight,
|
||||
"Delete",
|
||||
picopass_scene_delete_widget_callback,
|
||||
picopass);
|
||||
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
|
||||
}
|
||||
|
||||
bool picopass_scene_delete_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == GuiButtonTypeLeft) {
|
||||
return scene_manager_previous_scene(picopass->scene_manager);
|
||||
} else if(event.event == GuiButtonTypeRight) {
|
||||
if(picopass_device_delete(picopass->dev, true)) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneDeleteSuccess);
|
||||
} else {
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
picopass->scene_manager, PicopassSceneStart);
|
||||
}
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_delete_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
widget_reset(picopass->widget);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#include "../picopass_i.h"
|
||||
|
||||
void picopass_scene_delete_success_popup_callback(void* context) {
|
||||
Picopass* picopass = context;
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventViewExit);
|
||||
}
|
||||
|
||||
void picopass_scene_delete_success_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Setup view
|
||||
Popup* popup = picopass->popup;
|
||||
popup_set_icon(popup, 0, 2, &I_DolphinMafia_115x62);
|
||||
popup_set_header(popup, "Deleted", 83, 19, AlignLeft, AlignBottom);
|
||||
popup_set_timeout(popup, 1500);
|
||||
popup_set_context(popup, picopass);
|
||||
popup_set_callback(popup, picopass_scene_delete_success_popup_callback);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
|
||||
}
|
||||
|
||||
bool picopass_scene_delete_success_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == PicopassCustomEventViewExit) {
|
||||
consumed = scene_manager_search_and_switch_to_previous_scene(
|
||||
picopass->scene_manager, PicopassSceneStart);
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_delete_success_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Clear view
|
||||
popup_reset(picopass->popup);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "../picopass_i.h"
|
||||
#include <dolphin/dolphin.h>
|
||||
|
||||
void picopass_scene_device_info_widget_callback(
|
||||
GuiButtonType result,
|
||||
InputType type,
|
||||
void* context) {
|
||||
Picopass* picopass = context;
|
||||
if(type == InputTypeShort) {
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
|
||||
}
|
||||
}
|
||||
|
||||
void picopass_scene_device_info_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
string_t credential_str;
|
||||
string_t wiegand_str;
|
||||
string_init(credential_str);
|
||||
string_init(wiegand_str);
|
||||
|
||||
DOLPHIN_DEED(DolphinDeedNfcReadSuccess);
|
||||
|
||||
// Setup view
|
||||
PicopassPacs* pacs = &picopass->dev->dev_data.pacs;
|
||||
Widget* widget = picopass->widget;
|
||||
|
||||
size_t bytesLength = 1 + pacs->record.bitLength / 8;
|
||||
string_set_str(credential_str, "");
|
||||
for(uint8_t i = PICOPASS_BLOCK_LEN - bytesLength; i < PICOPASS_BLOCK_LEN; i++) {
|
||||
string_cat_printf(credential_str, " %02X", pacs->credential[i]);
|
||||
}
|
||||
|
||||
if(pacs->record.valid) {
|
||||
string_cat_printf(
|
||||
wiegand_str, "FC: %u CN: %u", pacs->record.FacilityCode, pacs->record.CardNumber);
|
||||
} else {
|
||||
string_cat_printf(wiegand_str, "%d bits", pacs->record.bitLength);
|
||||
}
|
||||
|
||||
widget_add_string_element(
|
||||
widget, 64, 12, AlignCenter, AlignCenter, FontPrimary, string_get_cstr(wiegand_str));
|
||||
widget_add_string_element(
|
||||
widget, 64, 32, AlignCenter, AlignCenter, FontSecondary, string_get_cstr(credential_str));
|
||||
|
||||
string_clear(credential_str);
|
||||
string_clear(wiegand_str);
|
||||
|
||||
widget_add_button_element(
|
||||
picopass->widget,
|
||||
GuiButtonTypeLeft,
|
||||
"Back",
|
||||
picopass_scene_device_info_widget_callback,
|
||||
picopass);
|
||||
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
|
||||
}
|
||||
|
||||
bool picopass_scene_device_info_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == GuiButtonTypeLeft) {
|
||||
consumed = scene_manager_previous_scene(picopass->scene_manager);
|
||||
} else if(event.event == PicopassCustomEventViewExit) {
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
|
||||
consumed = true;
|
||||
}
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
|
||||
consumed = true;
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_device_info_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Clear views
|
||||
widget_reset(picopass->widget);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "../picopass_i.h"
|
||||
#include "../picopass_device.h"
|
||||
|
||||
void picopass_scene_file_select_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
// Process file_select return
|
||||
picopass_device_set_loading_callback(picopass->dev, picopass_show_loading_popup, picopass);
|
||||
if(picopass_file_select(picopass->dev)) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneSavedMenu);
|
||||
} else {
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
picopass->scene_manager, PicopassSceneStart);
|
||||
}
|
||||
picopass_device_set_loading_callback(picopass->dev, NULL, picopass);
|
||||
}
|
||||
|
||||
bool picopass_scene_file_select_on_event(void* context, SceneManagerEvent event) {
|
||||
UNUSED(context);
|
||||
UNUSED(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void picopass_scene_file_select_on_exit(void* context) {
|
||||
UNUSED(context);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "../picopass_i.h"
|
||||
#include <dolphin/dolphin.h>
|
||||
|
||||
void picopass_read_card_worker_callback(PicopassWorkerEvent event, void* context) {
|
||||
UNUSED(event);
|
||||
Picopass* picopass = context;
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventWorkerExit);
|
||||
}
|
||||
|
||||
void picopass_scene_read_card_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
DOLPHIN_DEED(DolphinDeedNfcRead);
|
||||
|
||||
// Setup view
|
||||
Popup* popup = picopass->popup;
|
||||
popup_set_header(popup, "Detecting\npicopass\ncard", 68, 30, AlignLeft, AlignTop);
|
||||
popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
|
||||
|
||||
// Start worker
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
|
||||
picopass_worker_start(
|
||||
picopass->worker,
|
||||
PicopassWorkerStateDetect,
|
||||
&picopass->dev->dev_data,
|
||||
picopass_read_card_worker_callback,
|
||||
picopass);
|
||||
|
||||
picopass_blink_start(picopass);
|
||||
}
|
||||
|
||||
bool picopass_scene_read_card_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == PicopassCustomEventWorkerExit) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadCardSuccess);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_read_card_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Stop worker
|
||||
picopass_worker_stop(picopass->worker);
|
||||
// Clear view
|
||||
popup_reset(picopass->popup);
|
||||
|
||||
picopass_blink_stop(picopass);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "../picopass_i.h"
|
||||
#include <dolphin/dolphin.h>
|
||||
|
||||
void picopass_scene_read_card_success_widget_callback(
|
||||
GuiButtonType result,
|
||||
InputType type,
|
||||
void* context) {
|
||||
furi_assert(context);
|
||||
Picopass* picopass = context;
|
||||
|
||||
if(type == InputTypeShort) {
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
|
||||
}
|
||||
}
|
||||
|
||||
void picopass_scene_read_card_success_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
string_t credential_str;
|
||||
string_t wiegand_str;
|
||||
string_init(credential_str);
|
||||
string_init(wiegand_str);
|
||||
|
||||
DOLPHIN_DEED(DolphinDeedNfcReadSuccess);
|
||||
|
||||
// Send notification
|
||||
notification_message(picopass->notifications, &sequence_success);
|
||||
|
||||
// Setup view
|
||||
PicopassPacs* pacs = &picopass->dev->dev_data.pacs;
|
||||
Widget* widget = picopass->widget;
|
||||
|
||||
if(pacs->record.bitLength == 0) {
|
||||
string_cat_printf(wiegand_str, "Read Failed");
|
||||
|
||||
widget_add_button_element(
|
||||
widget,
|
||||
GuiButtonTypeLeft,
|
||||
"Retry",
|
||||
picopass_scene_read_card_success_widget_callback,
|
||||
picopass);
|
||||
|
||||
widget_add_string_element(
|
||||
widget, 64, 12, AlignCenter, AlignCenter, FontPrimary, string_get_cstr(wiegand_str));
|
||||
} else {
|
||||
size_t bytesLength = 1 + pacs->record.bitLength / 8;
|
||||
string_set_str(credential_str, "");
|
||||
for(uint8_t i = PICOPASS_BLOCK_LEN - bytesLength; i < PICOPASS_BLOCK_LEN; i++) {
|
||||
string_cat_printf(credential_str, " %02X", pacs->credential[i]);
|
||||
}
|
||||
|
||||
if(pacs->record.valid) {
|
||||
string_cat_printf(
|
||||
wiegand_str, "FC: %u CN: %u", pacs->record.FacilityCode, pacs->record.CardNumber);
|
||||
} else {
|
||||
string_cat_printf(wiegand_str, "%d bits", pacs->record.bitLength);
|
||||
}
|
||||
|
||||
widget_add_button_element(
|
||||
widget,
|
||||
GuiButtonTypeLeft,
|
||||
"Retry",
|
||||
picopass_scene_read_card_success_widget_callback,
|
||||
picopass);
|
||||
|
||||
widget_add_button_element(
|
||||
widget,
|
||||
GuiButtonTypeRight,
|
||||
"More",
|
||||
picopass_scene_read_card_success_widget_callback,
|
||||
picopass);
|
||||
|
||||
widget_add_string_element(
|
||||
widget, 64, 12, AlignCenter, AlignCenter, FontPrimary, string_get_cstr(wiegand_str));
|
||||
widget_add_string_element(
|
||||
widget,
|
||||
64,
|
||||
32,
|
||||
AlignCenter,
|
||||
AlignCenter,
|
||||
FontSecondary,
|
||||
string_get_cstr(credential_str));
|
||||
}
|
||||
string_clear(credential_str);
|
||||
string_clear(wiegand_str);
|
||||
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
|
||||
}
|
||||
|
||||
bool picopass_scene_read_card_success_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == GuiButtonTypeLeft) {
|
||||
consumed = scene_manager_previous_scene(picopass->scene_manager);
|
||||
} else if(event.event == GuiButtonTypeRight) {
|
||||
// Clear device name
|
||||
picopass_device_set_name(picopass->dev, "");
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneCardMenu);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_read_card_success_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Clear view
|
||||
widget_reset(picopass->widget);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "../picopass_i.h"
|
||||
#include "m-string.h"
|
||||
#include <lib/toolbox/random_name.h>
|
||||
#include <gui/modules/validators.h>
|
||||
#include <toolbox/path.h>
|
||||
|
||||
void picopass_scene_save_name_text_input_callback(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventTextInputDone);
|
||||
}
|
||||
|
||||
void picopass_scene_save_name_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Setup view
|
||||
TextInput* text_input = picopass->text_input;
|
||||
bool dev_name_empty = false;
|
||||
if(!strcmp(picopass->dev->dev_name, "")) {
|
||||
set_random_name(picopass->text_store, sizeof(picopass->text_store));
|
||||
dev_name_empty = true;
|
||||
} else {
|
||||
picopass_text_store_set(picopass, picopass->dev->dev_name);
|
||||
}
|
||||
text_input_set_header_text(text_input, "Name the card");
|
||||
text_input_set_result_callback(
|
||||
text_input,
|
||||
picopass_scene_save_name_text_input_callback,
|
||||
picopass,
|
||||
picopass->text_store,
|
||||
PICOPASS_DEV_NAME_MAX_LEN,
|
||||
dev_name_empty);
|
||||
|
||||
string_t folder_path;
|
||||
string_init(folder_path);
|
||||
|
||||
if(string_end_with_str_p(picopass->dev->load_path, PICOPASS_APP_EXTENSION)) {
|
||||
path_extract_dirname(string_get_cstr(picopass->dev->load_path), folder_path);
|
||||
} else {
|
||||
string_set_str(folder_path, PICOPASS_APP_FOLDER);
|
||||
}
|
||||
|
||||
ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
|
||||
string_get_cstr(folder_path), PICOPASS_APP_EXTENSION, picopass->dev->dev_name);
|
||||
text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
|
||||
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewTextInput);
|
||||
|
||||
string_clear(folder_path);
|
||||
}
|
||||
|
||||
bool picopass_scene_save_name_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == PicopassCustomEventTextInputDone) {
|
||||
if(strcmp(picopass->dev->dev_name, "")) {
|
||||
// picopass_device_delete(picopass->dev, true);
|
||||
}
|
||||
strlcpy(
|
||||
picopass->dev->dev_name, picopass->text_store, strlen(picopass->text_store) + 1);
|
||||
if(picopass_device_save(picopass->dev, picopass->text_store)) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveSuccess);
|
||||
consumed = true;
|
||||
} else {
|
||||
consumed = scene_manager_search_and_switch_to_previous_scene(
|
||||
picopass->scene_manager, PicopassSceneStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_save_name_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Clear view
|
||||
void* validator_context = text_input_get_validator_callback_context(picopass->text_input);
|
||||
text_input_set_validator(picopass->text_input, NULL, NULL);
|
||||
validator_is_file_free(validator_context);
|
||||
|
||||
text_input_reset(picopass->text_input);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "../picopass_i.h"
|
||||
#include <dolphin/dolphin.h>
|
||||
|
||||
void picopass_scene_save_success_popup_callback(void* context) {
|
||||
Picopass* picopass = context;
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventViewExit);
|
||||
}
|
||||
|
||||
void picopass_scene_save_success_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
DOLPHIN_DEED(DolphinDeedNfcSave);
|
||||
|
||||
// Setup view
|
||||
Popup* popup = picopass->popup;
|
||||
popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
|
||||
popup_set_header(popup, "Saved!", 13, 22, AlignLeft, AlignBottom);
|
||||
popup_set_timeout(popup, 1500);
|
||||
popup_set_context(popup, picopass);
|
||||
popup_set_callback(popup, picopass_scene_save_success_popup_callback);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
|
||||
}
|
||||
|
||||
bool picopass_scene_save_success_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == PicopassCustomEventViewExit) {
|
||||
if(scene_manager_has_previous_scene(picopass->scene_manager, PicopassSceneCardMenu)) {
|
||||
consumed = scene_manager_search_and_switch_to_previous_scene(
|
||||
picopass->scene_manager, PicopassSceneCardMenu);
|
||||
} else {
|
||||
consumed = scene_manager_search_and_switch_to_previous_scene(
|
||||
picopass->scene_manager, PicopassSceneStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_save_success_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Clear view
|
||||
popup_reset(picopass->popup);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "../picopass_i.h"
|
||||
|
||||
enum SubmenuIndex {
|
||||
SubmenuIndexDelete,
|
||||
SubmenuIndexInfo,
|
||||
SubmenuIndexWrite,
|
||||
};
|
||||
|
||||
void picopass_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void picopass_scene_saved_menu_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
Submenu* submenu = picopass->submenu;
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"Delete",
|
||||
SubmenuIndexDelete,
|
||||
picopass_scene_saved_menu_submenu_callback,
|
||||
picopass);
|
||||
submenu_add_item(
|
||||
submenu, "Info", SubmenuIndexInfo, picopass_scene_saved_menu_submenu_callback, picopass);
|
||||
submenu_add_item(
|
||||
submenu, "Write", SubmenuIndexWrite, picopass_scene_saved_menu_submenu_callback, picopass);
|
||||
|
||||
submenu_set_selected_item(
|
||||
picopass->submenu,
|
||||
scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneSavedMenu));
|
||||
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewMenu);
|
||||
}
|
||||
|
||||
bool picopass_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
scene_manager_set_scene_state(
|
||||
picopass->scene_manager, PicopassSceneSavedMenu, event.event);
|
||||
|
||||
if(event.event == SubmenuIndexDelete) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneDelete);
|
||||
consumed = true;
|
||||
} else if(event.event == SubmenuIndexInfo) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneDeviceInfo);
|
||||
consumed = true;
|
||||
} else if(event.event == SubmenuIndexWrite) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCard);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_saved_menu_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
submenu_reset(picopass->submenu);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "../picopass_i.h"
|
||||
enum SubmenuIndex {
|
||||
SubmenuIndexRead,
|
||||
SubmenuIndexRunScript,
|
||||
SubmenuIndexSaved,
|
||||
SubmenuIndexAddManualy,
|
||||
SubmenuIndexDebug,
|
||||
};
|
||||
|
||||
void picopass_scene_start_submenu_callback(void* context, uint32_t index) {
|
||||
Picopass* picopass = context;
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, index);
|
||||
}
|
||||
void picopass_scene_start_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
Submenu* submenu = picopass->submenu;
|
||||
submenu_add_item(
|
||||
submenu, "Read Card", SubmenuIndexRead, picopass_scene_start_submenu_callback, picopass);
|
||||
submenu_add_item(
|
||||
submenu, "Saved", SubmenuIndexSaved, picopass_scene_start_submenu_callback, picopass);
|
||||
|
||||
submenu_set_selected_item(
|
||||
submenu, scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneStart));
|
||||
picopass_device_clear(picopass->dev);
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewMenu);
|
||||
}
|
||||
|
||||
bool picopass_scene_start_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == SubmenuIndexRead) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadCard);
|
||||
consumed = true;
|
||||
} else if(event.event == SubmenuIndexSaved) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneFileSelect);
|
||||
consumed = true;
|
||||
}
|
||||
scene_manager_set_scene_state(picopass->scene_manager, PicopassSceneStart, event.event);
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_start_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
submenu_reset(picopass->submenu);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "../picopass_i.h"
|
||||
#include <dolphin/dolphin.h>
|
||||
|
||||
void picopass_write_card_worker_callback(PicopassWorkerEvent event, void* context) {
|
||||
UNUSED(event);
|
||||
Picopass* picopass = context;
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventWorkerExit);
|
||||
}
|
||||
|
||||
void picopass_scene_write_card_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
DOLPHIN_DEED(DolphinDeedNfcSave);
|
||||
|
||||
// Setup view
|
||||
Popup* popup = picopass->popup;
|
||||
popup_set_header(popup, "Writing\npicopass\ncard", 68, 30, AlignLeft, AlignTop);
|
||||
popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61);
|
||||
|
||||
// Start worker
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
|
||||
picopass_worker_start(
|
||||
picopass->worker,
|
||||
PicopassWorkerStateWrite,
|
||||
&picopass->dev->dev_data,
|
||||
picopass_write_card_worker_callback,
|
||||
picopass);
|
||||
|
||||
picopass_blink_start(picopass);
|
||||
}
|
||||
|
||||
bool picopass_scene_write_card_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == PicopassCustomEventWorkerExit) {
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCardSuccess);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_write_card_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Stop worker
|
||||
picopass_worker_stop(picopass->worker);
|
||||
// Clear view
|
||||
popup_reset(picopass->popup);
|
||||
|
||||
picopass_blink_stop(picopass);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "../picopass_i.h"
|
||||
#include <dolphin/dolphin.h>
|
||||
|
||||
void picopass_scene_write_card_success_widget_callback(
|
||||
GuiButtonType result,
|
||||
InputType type,
|
||||
void* context) {
|
||||
furi_assert(context);
|
||||
Picopass* picopass = context;
|
||||
|
||||
if(type == InputTypeShort) {
|
||||
view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
|
||||
}
|
||||
}
|
||||
|
||||
void picopass_scene_write_card_success_on_enter(void* context) {
|
||||
Picopass* picopass = context;
|
||||
Widget* widget = picopass->widget;
|
||||
|
||||
DOLPHIN_DEED(DolphinDeedNfcReadSuccess);
|
||||
|
||||
// Send notification
|
||||
notification_message(picopass->notifications, &sequence_success);
|
||||
|
||||
widget_add_button_element(
|
||||
widget,
|
||||
GuiButtonTypeLeft,
|
||||
"Retry",
|
||||
picopass_scene_write_card_success_widget_callback,
|
||||
picopass);
|
||||
|
||||
view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
|
||||
}
|
||||
|
||||
bool picopass_scene_write_card_success_on_event(void* context, SceneManagerEvent event) {
|
||||
Picopass* picopass = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == GuiButtonTypeLeft) {
|
||||
consumed = scene_manager_previous_scene(picopass->scene_manager);
|
||||
} else if(event.event == GuiButtonTypeRight) {
|
||||
// Clear device name
|
||||
picopass_device_set_name(picopass->dev, "");
|
||||
scene_manager_next_scene(picopass->scene_manager, PicopassSceneCardMenu);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void picopass_scene_write_card_success_on_exit(void* context) {
|
||||
Picopass* picopass = context;
|
||||
|
||||
// Clear view
|
||||
widget_reset(picopass->widget);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
App(
|
||||
appid="snake_game",
|
||||
name="Snake Game",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="snake_game_app",
|
||||
cdefines=["APP_SNAKE_GAME"],
|
||||
requires=["gui"],
|
||||
stack_size=1 * 1024,
|
||||
order=30,
|
||||
fap_icon="snake_10px.png",
|
||||
fap_category="Games",
|
||||
)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 158 B |
@@ -0,0 +1,387 @@
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef struct {
|
||||
// +-----x
|
||||
// |
|
||||
// |
|
||||
// y
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
} Point;
|
||||
|
||||
typedef enum {
|
||||
GameStateLife,
|
||||
|
||||
// https://melmagazine.com/en-us/story/snake-nokia-6110-oral-history-taneli-armanto
|
||||
// Armanto: While testing the early versions of the game, I noticed it was hard
|
||||
// to control the snake upon getting close to and edge but not crashing — especially
|
||||
// in the highest speed levels. I wanted the highest level to be as fast as I could
|
||||
// possibly make the device "run," but on the other hand, I wanted to be friendly
|
||||
// and help the player manage that level. Otherwise it might not be fun to play. So
|
||||
// I implemented a little delay. A few milliseconds of extra time right before
|
||||
// the player crashes, during which she can still change the directions. And if
|
||||
// she does, the game continues.
|
||||
GameStateLastChance,
|
||||
|
||||
GameStateGameOver,
|
||||
} GameState;
|
||||
|
||||
// Note: do not change without purpose. Current values are used in smart
|
||||
// orthogonality calculation in `snake_game_get_turn_snake`.
|
||||
typedef enum {
|
||||
DirectionUp,
|
||||
DirectionRight,
|
||||
DirectionDown,
|
||||
DirectionLeft,
|
||||
} Direction;
|
||||
|
||||
#define MAX_SNAKE_LEN 253
|
||||
|
||||
typedef struct {
|
||||
Point points[MAX_SNAKE_LEN];
|
||||
uint16_t len;
|
||||
Direction currentMovement;
|
||||
Direction nextMovement; // if backward of currentMovement, ignore
|
||||
Point fruit;
|
||||
GameState state;
|
||||
} SnakeState;
|
||||
|
||||
typedef enum {
|
||||
EventTypeTick,
|
||||
EventTypeKey,
|
||||
} EventType;
|
||||
|
||||
typedef struct {
|
||||
EventType type;
|
||||
InputEvent input;
|
||||
} SnakeEvent;
|
||||
|
||||
static void snake_game_render_callback(Canvas* const canvas, void* ctx) {
|
||||
const SnakeState* snake_state = acquire_mutex((ValueMutex*)ctx, 25);
|
||||
if(snake_state == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before the function is called, the state is set with the canvas_reset(canvas)
|
||||
|
||||
// Frame
|
||||
canvas_draw_frame(canvas, 0, 0, 128, 64);
|
||||
|
||||
// Fruit
|
||||
Point f = snake_state->fruit;
|
||||
f.x = f.x * 4 + 1;
|
||||
f.y = f.y * 4 + 1;
|
||||
canvas_draw_rframe(canvas, f.x, f.y, 6, 6, 2);
|
||||
|
||||
// Snake
|
||||
for(uint16_t i = 0; i < snake_state->len; i++) {
|
||||
Point p = snake_state->points[i];
|
||||
p.x = p.x * 4 + 2;
|
||||
p.y = p.y * 4 + 2;
|
||||
canvas_draw_box(canvas, p.x, p.y, 4, 4);
|
||||
}
|
||||
|
||||
// Game Over banner
|
||||
if(snake_state->state == GameStateGameOver) {
|
||||
// Screen is 128x64 px
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_box(canvas, 34, 20, 62, 24);
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_frame(canvas, 34, 20, 62, 24);
|
||||
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str(canvas, 37, 31, "Game Over");
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
char buffer[12];
|
||||
snprintf(buffer, sizeof(buffer), "Score: %u", snake_state->len - 7);
|
||||
canvas_draw_str_aligned(canvas, 64, 41, AlignCenter, AlignBottom, buffer);
|
||||
}
|
||||
|
||||
release_mutex((ValueMutex*)ctx, snake_state);
|
||||
}
|
||||
|
||||
static void snake_game_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
|
||||
furi_assert(event_queue);
|
||||
|
||||
SnakeEvent event = {.type = EventTypeKey, .input = *input_event};
|
||||
furi_message_queue_put(event_queue, &event, FuriWaitForever);
|
||||
}
|
||||
|
||||
static void snake_game_update_timer_callback(FuriMessageQueue* event_queue) {
|
||||
furi_assert(event_queue);
|
||||
|
||||
SnakeEvent event = {.type = EventTypeTick};
|
||||
furi_message_queue_put(event_queue, &event, 0);
|
||||
}
|
||||
|
||||
static void snake_game_init_game(SnakeState* const snake_state) {
|
||||
Point p[] = {{8, 6}, {7, 6}, {6, 6}, {5, 6}, {4, 6}, {3, 6}, {2, 6}};
|
||||
memcpy(snake_state->points, p, sizeof(p));
|
||||
|
||||
snake_state->len = 7;
|
||||
|
||||
snake_state->currentMovement = DirectionRight;
|
||||
|
||||
snake_state->nextMovement = DirectionRight;
|
||||
|
||||
Point f = {18, 6};
|
||||
snake_state->fruit = f;
|
||||
|
||||
snake_state->state = GameStateLife;
|
||||
}
|
||||
|
||||
static Point snake_game_get_new_fruit(SnakeState const* const snake_state) {
|
||||
// 1 bit for each point on the playing field where the snake can turn
|
||||
// and where the fruit can appear
|
||||
uint16_t buffer[8];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
uint8_t empty = 8 * 16;
|
||||
|
||||
for(uint16_t i = 0; i < snake_state->len; i++) {
|
||||
Point p = snake_state->points[i];
|
||||
|
||||
if(p.x % 2 != 0 || p.y % 2 != 0) {
|
||||
continue;
|
||||
}
|
||||
p.x /= 2;
|
||||
p.y /= 2;
|
||||
|
||||
buffer[p.y] |= 1 << p.x;
|
||||
empty--;
|
||||
}
|
||||
// Bit set if snake use that playing field
|
||||
|
||||
uint16_t newFruit = rand() % empty;
|
||||
|
||||
// Skip random number of _empty_ fields
|
||||
for(uint8_t y = 0; y < 8; y++) {
|
||||
for(uint16_t x = 0, mask = 1; x < 16; x += 1, mask <<= 1) {
|
||||
if((buffer[y] & mask) == 0) {
|
||||
if(newFruit == 0) {
|
||||
Point p = {
|
||||
.x = x * 2,
|
||||
.y = y * 2,
|
||||
};
|
||||
return p;
|
||||
}
|
||||
newFruit--;
|
||||
}
|
||||
}
|
||||
}
|
||||
// We will never be here
|
||||
Point p = {0, 0};
|
||||
return p;
|
||||
}
|
||||
|
||||
static bool snake_game_collision_with_frame(Point const next_step) {
|
||||
// if x == 0 && currentMovement == left then x - 1 == 255 ,
|
||||
// so check only x > right border
|
||||
return next_step.x > 30 || next_step.y > 14;
|
||||
}
|
||||
|
||||
static bool
|
||||
snake_game_collision_with_tail(SnakeState const* const snake_state, Point const next_step) {
|
||||
for(uint16_t i = 0; i < snake_state->len; i++) {
|
||||
Point p = snake_state->points[i];
|
||||
if(p.x == next_step.x && p.y == next_step.y) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static Direction snake_game_get_turn_snake(SnakeState const* const snake_state) {
|
||||
// Sum of two `Direction` lies between 0 and 6, odd values indicate orthogonality.
|
||||
bool is_orthogonal = (snake_state->currentMovement + snake_state->nextMovement) % 2 == 1;
|
||||
return is_orthogonal ? snake_state->nextMovement : snake_state->currentMovement;
|
||||
}
|
||||
|
||||
static Point snake_game_get_next_step(SnakeState const* const snake_state) {
|
||||
Point next_step = snake_state->points[0];
|
||||
switch(snake_state->currentMovement) {
|
||||
// +-----x
|
||||
// |
|
||||
// |
|
||||
// y
|
||||
case DirectionUp:
|
||||
next_step.y--;
|
||||
break;
|
||||
case DirectionRight:
|
||||
next_step.x++;
|
||||
break;
|
||||
case DirectionDown:
|
||||
next_step.y++;
|
||||
break;
|
||||
case DirectionLeft:
|
||||
next_step.x--;
|
||||
break;
|
||||
}
|
||||
return next_step;
|
||||
}
|
||||
|
||||
static void snake_game_move_snake(SnakeState* const snake_state, Point const next_step) {
|
||||
memmove(snake_state->points + 1, snake_state->points, snake_state->len * sizeof(Point));
|
||||
snake_state->points[0] = next_step;
|
||||
}
|
||||
|
||||
static void snake_game_process_game_step(SnakeState* const snake_state) {
|
||||
if(snake_state->state == GameStateGameOver) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool can_turn = (snake_state->points[0].x % 2 == 0) && (snake_state->points[0].y % 2 == 0);
|
||||
if(can_turn) {
|
||||
snake_state->currentMovement = snake_game_get_turn_snake(snake_state);
|
||||
}
|
||||
|
||||
Point next_step = snake_game_get_next_step(snake_state);
|
||||
|
||||
bool crush = snake_game_collision_with_frame(next_step);
|
||||
if(crush) {
|
||||
if(snake_state->state == GameStateLife) {
|
||||
snake_state->state = GameStateLastChance;
|
||||
return;
|
||||
} else if(snake_state->state == GameStateLastChance) {
|
||||
snake_state->state = GameStateGameOver;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if(snake_state->state == GameStateLastChance) {
|
||||
snake_state->state = GameStateLife;
|
||||
}
|
||||
}
|
||||
|
||||
crush = snake_game_collision_with_tail(snake_state, next_step);
|
||||
if(crush) {
|
||||
snake_state->state = GameStateGameOver;
|
||||
return;
|
||||
}
|
||||
|
||||
bool eatFruit = (next_step.x == snake_state->fruit.x) && (next_step.y == snake_state->fruit.y);
|
||||
if(eatFruit) {
|
||||
snake_state->len++;
|
||||
if(snake_state->len >= MAX_SNAKE_LEN) {
|
||||
snake_state->state = GameStateGameOver;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
snake_game_move_snake(snake_state, next_step);
|
||||
|
||||
if(eatFruit) {
|
||||
snake_state->fruit = snake_game_get_new_fruit(snake_state);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t snake_game_app(void* p) {
|
||||
UNUSED(p);
|
||||
srand(DWT->CYCCNT);
|
||||
|
||||
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(SnakeEvent));
|
||||
|
||||
SnakeState* snake_state = malloc(sizeof(SnakeState));
|
||||
snake_game_init_game(snake_state);
|
||||
|
||||
ValueMutex state_mutex;
|
||||
if(!init_mutex(&state_mutex, snake_state, sizeof(SnakeState))) {
|
||||
FURI_LOG_E("SnakeGame", "cannot create mutex\r\n");
|
||||
free(snake_state);
|
||||
return 255;
|
||||
}
|
||||
|
||||
ViewPort* view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(view_port, snake_game_render_callback, &state_mutex);
|
||||
view_port_input_callback_set(view_port, snake_game_input_callback, event_queue);
|
||||
|
||||
FuriTimer* timer =
|
||||
furi_timer_alloc(snake_game_update_timer_callback, FuriTimerTypePeriodic, event_queue);
|
||||
furi_timer_start(timer, furi_kernel_get_tick_frequency() / 4);
|
||||
|
||||
// Open GUI and register view_port
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||
|
||||
SnakeEvent event;
|
||||
for(bool processing = true; processing;) {
|
||||
FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
|
||||
|
||||
SnakeState* snake_state = (SnakeState*)acquire_mutex_block(&state_mutex);
|
||||
|
||||
if(event_status == FuriStatusOk) {
|
||||
// press events
|
||||
if(event.type == EventTypeKey) {
|
||||
if(event.input.type == InputTypePress) {
|
||||
switch(event.input.key) {
|
||||
case InputKeyUp:
|
||||
snake_state->nextMovement = DirectionUp;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
snake_state->nextMovement = DirectionDown;
|
||||
break;
|
||||
case InputKeyRight:
|
||||
snake_state->nextMovement = DirectionRight;
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
snake_state->nextMovement = DirectionLeft;
|
||||
break;
|
||||
case InputKeyOk:
|
||||
if(snake_state->state == GameStateGameOver) {
|
||||
snake_game_init_game(snake_state);
|
||||
}
|
||||
break;
|
||||
case InputKeyBack:
|
||||
processing = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(event.type == EventTypeTick) {
|
||||
snake_game_process_game_step(snake_state);
|
||||
}
|
||||
} else {
|
||||
// event timeout
|
||||
}
|
||||
|
||||
view_port_update(view_port);
|
||||
release_mutex(&state_mutex, snake_state);
|
||||
}
|
||||
|
||||
furi_timer_free(timer);
|
||||
view_port_enabled_set(view_port, false);
|
||||
gui_remove_view_port(gui, view_port);
|
||||
furi_record_close(RECORD_GUI);
|
||||
view_port_free(view_port);
|
||||
furi_message_queue_free(event_queue);
|
||||
delete_mutex(&state_mutex);
|
||||
free(snake_state);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Screen is 128x64 px
|
||||
// (4 + 4) * 16 - 4 + 2 + 2border == 128
|
||||
// (4 + 4) * 8 - 4 + 2 + 2border == 64
|
||||
// Game field from point{x: 0, y: 0} to point{x: 30, y: 14}.
|
||||
// The snake turns only in even cells - intersections.
|
||||
// ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
|
||||
// └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘
|
||||
Reference in New Issue
Block a user