public release 0.4
This commit is contained in:
commit
7ebef9616f
39
application.fam
Normal file
39
application.fam
Normal file
@ -0,0 +1,39 @@
|
||||
# COGS MyKai
|
||||
App(
|
||||
appid="cogs_mikai",
|
||||
name="COGS - MyKey",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="cogs_mikai_app",
|
||||
stack_size=4 * 1024,
|
||||
fap_category="NFC",
|
||||
fap_version="1.1",
|
||||
fap_icon="cogs_mikai.png",
|
||||
fap_description="Read and edit COGES MyKey NFC cards.",
|
||||
fap_author="luhf",
|
||||
fap_weburl="https://monocul.us/",
|
||||
fap_icon_assets="images",
|
||||
sources=[
|
||||
"cogs_mikai_app.c",
|
||||
"mykey_core.c",
|
||||
"nfc_srix.c",
|
||||
"scenes/cogs_mikai_scene.c",
|
||||
"scenes/cogs_mikai_scene_start.c",
|
||||
"scenes/cogs_mikai_scene_read.c",
|
||||
"scenes/cogs_mikai_scene_info.c",
|
||||
"scenes/cogs_mikai_scene_write_card.c",
|
||||
"scenes/cogs_mikai_scene_add_credit.c",
|
||||
"scenes/cogs_mikai_scene_set_credit.c",
|
||||
"scenes/cogs_mikai_scene_reset.c",
|
||||
"scenes/cogs_mikai_scene_save_file.c",
|
||||
"scenes/cogs_mikai_scene_load_file.c",
|
||||
"scenes/cogs_mikai_scene_debug.c",
|
||||
"scenes/cogs_mikai_scene_about.c",
|
||||
],
|
||||
requires=[
|
||||
"gui",
|
||||
"dialogs",
|
||||
],
|
||||
provides=[
|
||||
"cogs_mikai",
|
||||
],
|
||||
)
|
||||
137
cogs_mikai.h
Normal file
137
cogs_mikai.h
Normal file
@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/scene_manager.h>
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/modules/variable_item_list.h>
|
||||
#include <gui/modules/text_input.h>
|
||||
#include <gui/modules/popup.h>
|
||||
#include <gui/modules/widget.h>
|
||||
#include <gui/modules/text_box.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <notification/notification_messages.h>
|
||||
|
||||
#define TAG "COGSMyKai"
|
||||
|
||||
// SRIX4K Constants
|
||||
#define SRIX_BLOCK_LENGTH 4
|
||||
#define SRIX_UID_LENGTH 8
|
||||
#define SRIX4K_BLOCKS 128
|
||||
#define SRIX4K_BYTES 512
|
||||
|
||||
typedef enum {
|
||||
COGSMyKaiViewSubmenu,
|
||||
COGSMyKaiViewTextInput,
|
||||
COGSMyKaiViewPopup,
|
||||
COGSMyKaiViewWidget,
|
||||
COGSMyKaiViewTextBox,
|
||||
} COGSMyKaiView;
|
||||
|
||||
typedef enum {
|
||||
COGSMyKaiSceneStart,
|
||||
COGSMyKaiSceneRead,
|
||||
COGSMyKaiSceneInfo,
|
||||
COGSMyKaiSceneWriteCard,
|
||||
COGSMyKaiSceneAddCredit,
|
||||
COGSMyKaiSceneSetCredit,
|
||||
COGSMyKaiSceneReset,
|
||||
COGSMyKaiSceneSaveFile,
|
||||
COGSMyKaiSceneLoadFile,
|
||||
COGSMyKaiSceneDebug,
|
||||
COGSMyKaiSceneAbout,
|
||||
COGSMyKaiSceneCount,
|
||||
} COGSMyKaiScene;
|
||||
|
||||
typedef struct {
|
||||
uint32_t eeprom[SRIX4K_BLOCKS];
|
||||
uint64_t uid;
|
||||
uint32_t encryption_key;
|
||||
bool is_loaded;
|
||||
bool is_reset;
|
||||
bool is_modified;
|
||||
uint16_t current_credit;
|
||||
} MyKeyData;
|
||||
|
||||
typedef struct {
|
||||
Gui* gui;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
SceneManager* scene_manager;
|
||||
Submenu* submenu;
|
||||
TextInput* text_input;
|
||||
Popup* popup;
|
||||
Widget* widget;
|
||||
TextBox* text_box;
|
||||
FuriString* text_box_store;
|
||||
DialogsApp* dialogs;
|
||||
NotificationApp* notifications;
|
||||
|
||||
MyKeyData mykey;
|
||||
char text_buffer[32];
|
||||
uint32_t temp_credit_value;
|
||||
} COGSMyKaiApp;
|
||||
|
||||
// Scene handler function declarations
|
||||
void cogs_mikai_scene_start_on_enter(void* context);
|
||||
bool cogs_mikai_scene_start_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_start_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_read_on_enter(void* context);
|
||||
bool cogs_mikai_scene_read_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_read_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_info_on_enter(void* context);
|
||||
bool cogs_mikai_scene_info_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_info_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_add_credit_on_enter(void* context);
|
||||
bool cogs_mikai_scene_add_credit_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_add_credit_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_set_credit_on_enter(void* context);
|
||||
bool cogs_mikai_scene_set_credit_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_set_credit_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_reset_on_enter(void* context);
|
||||
bool cogs_mikai_scene_reset_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_reset_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_write_card_on_enter(void* context);
|
||||
bool cogs_mikai_scene_write_card_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_write_card_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_save_file_on_enter(void* context);
|
||||
bool cogs_mikai_scene_save_file_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_save_file_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_load_file_on_enter(void* context);
|
||||
bool cogs_mikai_scene_load_file_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_load_file_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_debug_on_enter(void* context);
|
||||
bool cogs_mikai_scene_debug_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_debug_on_exit(void* context);
|
||||
|
||||
void cogs_mikai_scene_about_on_enter(void* context);
|
||||
bool cogs_mikai_scene_about_on_event(void* context, SceneManagerEvent event);
|
||||
void cogs_mikai_scene_about_on_exit(void* context);
|
||||
|
||||
// Scene handlers declaration
|
||||
extern const SceneManagerHandlers cogs_mikai_scene_handlers;
|
||||
|
||||
// MyKey operations
|
||||
bool mykey_read_from_nfc(COGSMyKaiApp* app);
|
||||
bool mykey_write_to_nfc(COGSMyKaiApp* app);
|
||||
void mykey_calculate_encryption_key(MyKeyData* key);
|
||||
bool mykey_is_reset(MyKeyData* key);
|
||||
uint16_t mykey_get_current_credit(MyKeyData* key);
|
||||
uint16_t mykey_get_credit_from_history(MyKeyData* key);
|
||||
void mykey_encode_decode_block(uint32_t* block);
|
||||
bool mykey_add_cents(MyKeyData* key, uint16_t cents, uint8_t day, uint8_t month, uint8_t year);
|
||||
bool mykey_set_cents(MyKeyData* key, uint16_t cents, uint8_t day, uint8_t month, uint8_t year);
|
||||
void mykey_reset(MyKeyData* key);
|
||||
uint32_t mykey_get_block(MyKeyData* key, uint8_t block_num);
|
||||
void mykey_modify_block(MyKeyData* key, uint32_t block, uint8_t block_num);
|
||||
bool mykey_save_raw_data(COGSMyKaiApp* app, const char* path);
|
||||
BIN
cogs_mikai.png
Normal file
BIN
cogs_mikai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 B |
106
cogs_mikai_app.c
Normal file
106
cogs_mikai_app.c
Normal file
@ -0,0 +1,106 @@
|
||||
#include "cogs_mikai.h"
|
||||
|
||||
static bool cogs_mikai_custom_event_callback(void* context, uint32_t event) {
|
||||
furi_assert(context);
|
||||
COGSMyKaiApp* app = context;
|
||||
return scene_manager_handle_custom_event(app->scene_manager, event);
|
||||
}
|
||||
|
||||
static bool cogs_mikai_back_event_callback(void* context) {
|
||||
furi_assert(context);
|
||||
COGSMyKaiApp* app = context;
|
||||
return scene_manager_handle_back_event(app->scene_manager);
|
||||
}
|
||||
|
||||
static COGSMyKaiApp* cogs_mikai_app_alloc() {
|
||||
COGSMyKaiApp* app = malloc(sizeof(COGSMyKaiApp));
|
||||
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
app->dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
app->notifications = furi_record_open(RECORD_NOTIFICATION);
|
||||
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
app->scene_manager = scene_manager_alloc(&cogs_mikai_scene_handlers, app);
|
||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||
|
||||
view_dispatcher_set_custom_event_callback(
|
||||
app->view_dispatcher, cogs_mikai_custom_event_callback);
|
||||
view_dispatcher_set_navigation_event_callback(
|
||||
app->view_dispatcher, cogs_mikai_back_event_callback);
|
||||
|
||||
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||
|
||||
// Allocate views
|
||||
app->submenu = submenu_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, COGSMyKaiViewSubmenu, submenu_get_view(app->submenu));
|
||||
|
||||
app->text_input = text_input_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, COGSMyKaiViewTextInput, text_input_get_view(app->text_input));
|
||||
|
||||
app->popup = popup_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, COGSMyKaiViewPopup, popup_get_view(app->popup));
|
||||
|
||||
app->widget = widget_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, COGSMyKaiViewWidget, widget_get_view(app->widget));
|
||||
|
||||
app->text_box = text_box_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, COGSMyKaiViewTextBox, text_box_get_view(app->text_box));
|
||||
app->text_box_store = furi_string_alloc();
|
||||
|
||||
// Initialize MyKey data
|
||||
memset(&app->mykey, 0, sizeof(MyKeyData));
|
||||
app->mykey.is_loaded = false;
|
||||
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneStart);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
static void cogs_mikai_app_free(COGSMyKaiApp* app) {
|
||||
furi_assert(app);
|
||||
|
||||
// Remove views
|
||||
view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewSubmenu);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewTextInput);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewWidget);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, COGSMyKaiViewTextBox);
|
||||
|
||||
submenu_free(app->submenu);
|
||||
text_input_free(app->text_input);
|
||||
popup_free(app->popup);
|
||||
widget_free(app->widget);
|
||||
text_box_free(app->text_box);
|
||||
furi_string_free(app->text_box_store);
|
||||
|
||||
// Free view dispatcher and scene manager
|
||||
view_dispatcher_free(app->view_dispatcher);
|
||||
scene_manager_free(app->scene_manager);
|
||||
|
||||
// Close records
|
||||
furi_record_close(RECORD_GUI);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
|
||||
free(app);
|
||||
}
|
||||
|
||||
int32_t cogs_mikai_app(void* p) {
|
||||
UNUSED(p);
|
||||
COGSMyKaiApp* app = cogs_mikai_app_alloc();
|
||||
|
||||
FURI_LOG_I(TAG, "COGS MyKai app started");
|
||||
|
||||
view_dispatcher_run(app->view_dispatcher);
|
||||
|
||||
FURI_LOG_I(TAG, "COGS MyKai app stopped");
|
||||
|
||||
cogs_mikai_app_free(app);
|
||||
|
||||
return 0;
|
||||
}
|
||||
BIN
dist/cogs_mikai.fap
vendored
Normal file
BIN
dist/cogs_mikai.fap
vendored
Normal file
Binary file not shown.
BIN
dist/debug/cogs_mikai_d.elf
vendored
Normal file
BIN
dist/debug/cogs_mikai_d.elf
vendored
Normal file
Binary file not shown.
0
images/.gitkeep
Normal file
0
images/.gitkeep
Normal file
492
mykey_core.c
Normal file
492
mykey_core.c
Normal file
@ -0,0 +1,492 @@
|
||||
#include "cogs_mikai.h"
|
||||
#include <furi.h>
|
||||
#include <string.h>
|
||||
#include <machine/endian.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
// Encode or decode a MyKey block (XOR bit manipulation)
|
||||
static inline void encode_decode_block(uint32_t* block) {
|
||||
*block ^= (*block & 0x00C00000) << 6 | (*block & 0x0000C000) << 12 | (*block & 0x000000C0) << 18 |
|
||||
(*block & 0x000C0000) >> 6 | (*block & 0x00030000) >> 12 | (*block & 0x00000300) >> 6;
|
||||
*block ^= (*block & 0x30000000) >> 6 | (*block & 0x0C000000) >> 12 | (*block & 0x03000000) >> 18 |
|
||||
(*block & 0x00003000) << 6 | (*block & 0x00000030) << 12 | (*block & 0x0000000C) << 6;
|
||||
*block ^= (*block & 0x00C00000) << 6 | (*block & 0x0000C000) << 12 | (*block & 0x000000C0) << 18 |
|
||||
(*block & 0x000C0000) >> 6 | (*block & 0x00030000) >> 12 | (*block & 0x00000300) >> 6;
|
||||
}
|
||||
|
||||
// Public wrapper for debug purposes
|
||||
void mykey_encode_decode_block(uint32_t* block) {
|
||||
encode_decode_block(block);
|
||||
}
|
||||
|
||||
// Calculate checksum of a generic block
|
||||
static inline void calculate_block_checksum(uint32_t* block, const uint8_t block_num) {
|
||||
uint8_t checksum = 0xFF - block_num - (*block & 0x0F) - (*block >> 4 & 0x0F) -
|
||||
(*block >> 8 & 0x0F) - (*block >> 12 & 0x0F) - (*block >> 16 & 0x0F) -
|
||||
(*block >> 20 & 0x0F);
|
||||
|
||||
// Clear first byte and set to checksum value
|
||||
*block &= 0x00FFFFFF;
|
||||
*block |= checksum << 24;
|
||||
}
|
||||
|
||||
// Return the number of days between 1/1/1995 and a specified date
|
||||
static uint32_t days_difference(uint8_t day, uint8_t month, uint16_t year) {
|
||||
if(month < 3) {
|
||||
year--;
|
||||
month += 12;
|
||||
}
|
||||
return year * 365 + year / 4 - year / 100 + year / 400 + (month * 153 + 3) / 5 + day - 728692;
|
||||
}
|
||||
|
||||
// Get current transaction offset
|
||||
static uint8_t get_current_transaction_offset(MyKeyData* key) {
|
||||
uint32_t block3C = key->eeprom[0x3C];
|
||||
|
||||
// If first transaction, set the pointer to 7 to fill the first transaction block
|
||||
if(block3C == 0xFFFFFFFF) {
|
||||
return 0x07;
|
||||
}
|
||||
|
||||
// Decode transaction pointer
|
||||
uint32_t current = block3C ^ (key->eeprom[0x07] & 0x00FFFFFF);
|
||||
encode_decode_block(¤t);
|
||||
|
||||
if((current & 0x00FF0000 >> 16) > 0x07) {
|
||||
// Out of range
|
||||
return 0x07;
|
||||
} else {
|
||||
// Return result (a value between 0x00 and 0x07)
|
||||
return current >> 16;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the encryption key and save the result in key struct
|
||||
void mykey_calculate_encryption_key(MyKeyData* key) {
|
||||
FURI_LOG_I(TAG, "=== Encryption Key Calculation ===");
|
||||
FURI_LOG_I(TAG, "UID (as stored): 0x%016llX", key->uid);
|
||||
|
||||
// OTP calculation (reverse block 6 + 1, incremental. 1,2,3, etc.)
|
||||
uint32_t block6 = key->eeprom[0x06];
|
||||
FURI_LOG_I(TAG, "Block 0x06 raw: 0x%08lX", block6);
|
||||
|
||||
uint32_t block6_reversed = __bswap32(block6);
|
||||
FURI_LOG_I(TAG, "Block 0x06 reversed: 0x%08lX", block6_reversed);
|
||||
|
||||
uint32_t otp = ~block6_reversed + 1;
|
||||
FURI_LOG_I(TAG, "OTP (~reversed + 1): 0x%08lX", otp);
|
||||
|
||||
// Encryption key calculation
|
||||
// MK = UID * VENDOR
|
||||
// SK (Encryption key) = MK * OTP
|
||||
uint32_t block18_raw = key->eeprom[0x18];
|
||||
uint32_t block19_raw = key->eeprom[0x19];
|
||||
FURI_LOG_I(TAG, "Block 0x18 raw: 0x%08lX", block18_raw);
|
||||
FURI_LOG_I(TAG, "Block 0x19 raw: 0x%08lX", block19_raw);
|
||||
|
||||
uint32_t block18 = block18_raw;
|
||||
uint32_t block19 = block19_raw;
|
||||
encode_decode_block(&block18);
|
||||
encode_decode_block(&block19);
|
||||
FURI_LOG_I(TAG, "Block 0x18 decoded: 0x%08lX", block18);
|
||||
FURI_LOG_I(TAG, "Block 0x19 decoded: 0x%08lX", block19);
|
||||
|
||||
uint64_t vendor = (((uint64_t)block18 << 16) | (block19 & 0x0000FFFF)) + 1;
|
||||
FURI_LOG_I(TAG, "Vendor: 0x%llX", vendor);
|
||||
|
||||
// Calculate encryption key: UID * vendor * OTP
|
||||
// UID is now correctly stored in big-endian format, no swapping needed
|
||||
key->encryption_key = (key->uid * vendor * otp) & 0xFFFFFFFF;
|
||||
FURI_LOG_I(TAG, "Encryption Key: 0x%08lX", key->encryption_key);
|
||||
FURI_LOG_I(TAG, "===================================");
|
||||
}
|
||||
|
||||
// Check if MyKey is reset (no vendor bound)
|
||||
bool mykey_is_reset(MyKeyData* key) {
|
||||
static const uint32_t block18_reset = 0x8FCD0F48;
|
||||
static const uint32_t block19_reset = 0xC0820007;
|
||||
return key->eeprom[0x18] == block18_reset && key->eeprom[0x19] == block19_reset;
|
||||
}
|
||||
|
||||
// Get block value
|
||||
uint32_t mykey_get_block(MyKeyData* key, uint8_t block_num) {
|
||||
if(block_num >= SRIX4K_BLOCKS) return 0;
|
||||
return key->eeprom[block_num];
|
||||
}
|
||||
|
||||
// Modify block
|
||||
void mykey_modify_block(MyKeyData* key, uint32_t block, uint8_t block_num) {
|
||||
if(block_num < SRIX4K_BLOCKS) {
|
||||
key->eeprom[block_num] = block;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract current credit using libmikai method (block 0x21) - PRIMARY METHOD
|
||||
uint16_t mykey_get_current_credit(MyKeyData* key) {
|
||||
FURI_LOG_I(TAG, "=== Credit Decoding (libmikai method) ===");
|
||||
|
||||
// Use libmikai approach: read from block 0x21
|
||||
uint32_t block21_raw = key->eeprom[0x21];
|
||||
FURI_LOG_I(TAG, "Block 0x21 raw: 0x%08lX", block21_raw);
|
||||
FURI_LOG_I(TAG, " Bytes: [%02X %02X %02X %02X]",
|
||||
(uint8_t)(block21_raw & 0xFF),
|
||||
(uint8_t)((block21_raw >> 8) & 0xFF),
|
||||
(uint8_t)((block21_raw >> 16) & 0xFF),
|
||||
(uint8_t)((block21_raw >> 24) & 0xFF));
|
||||
|
||||
FURI_LOG_I(TAG, "Encryption key: 0x%08lX", key->encryption_key);
|
||||
|
||||
uint32_t after_xor = block21_raw ^ key->encryption_key;
|
||||
FURI_LOG_I(TAG, "After XOR: 0x%08lX", after_xor);
|
||||
|
||||
uint32_t current_credit = after_xor;
|
||||
encode_decode_block(¤t_credit);
|
||||
FURI_LOG_I(TAG, "After encode_decode: 0x%08lX", current_credit);
|
||||
|
||||
uint16_t credit_lower = current_credit & 0xFFFF;
|
||||
uint16_t credit_upper = (current_credit >> 16) & 0xFFFF;
|
||||
FURI_LOG_I(TAG, "Lower 16 bits: %u cents (%u.%02u EUR)",
|
||||
credit_lower, credit_lower / 100, credit_lower % 100);
|
||||
FURI_LOG_I(TAG, "Upper 16 bits: %u cents (%u.%02u EUR)",
|
||||
credit_upper, credit_upper / 100, credit_upper % 100);
|
||||
FURI_LOG_I(TAG, "=========================================");
|
||||
|
||||
return credit_lower;
|
||||
}
|
||||
|
||||
// Get credit from transaction history (for comparison/debugging)
|
||||
uint16_t mykey_get_credit_from_history(MyKeyData* key) {
|
||||
uint32_t block3C = key->eeprom[0x3C];
|
||||
if(block3C == 0xFFFFFFFF) {
|
||||
return 0xFFFF; // No history available
|
||||
}
|
||||
|
||||
// Decrypt block 0x3C to get starting offset
|
||||
uint32_t decrypted_3C = block3C ^ key->eeprom[0x07];
|
||||
uint32_t starting_offset = ((decrypted_3C & 0x30000000) >> 28) |
|
||||
((decrypted_3C & 0x00100000) >> 18);
|
||||
|
||||
if(starting_offset >= 8) {
|
||||
return 0xFFFF; // Invalid offset
|
||||
}
|
||||
|
||||
// Get most recent transaction (offset 8 in the circular buffer)
|
||||
// Blocks are already in big-endian format, credit is in lower 16 bits
|
||||
uint32_t txn_block = key->eeprom[0x34 + ((starting_offset + 8) % 8)];
|
||||
uint16_t credit = txn_block & 0xFFFF;
|
||||
|
||||
FURI_LOG_D(TAG, "Credit from transaction history: %d cents", credit);
|
||||
return credit;
|
||||
}
|
||||
|
||||
// Add N cents to MyKey actual credit
|
||||
bool mykey_add_cents(MyKeyData* key, uint16_t cents, uint8_t day, uint8_t month, uint8_t year) {
|
||||
FURI_LOG_I(TAG, "=== Adding %u cents (%u.%02u EUR) ===", cents, cents / 100, cents % 100);
|
||||
|
||||
// Check reset key
|
||||
if(mykey_is_reset(key)) {
|
||||
FURI_LOG_E(TAG, "Key is reset, cannot add credit");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(key->eeprom[0x06] == 0 || key->eeprom[0x06] == 0xFFFFFFFF) {
|
||||
FURI_LOG_E(TAG, "Key has no vendor");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate current credit
|
||||
uint16_t precedent_credit;
|
||||
uint16_t actual_credit = mykey_get_current_credit(key);
|
||||
FURI_LOG_I(TAG, "Current credit: %u cents", actual_credit);
|
||||
|
||||
// Get current transaction position
|
||||
uint8_t current = get_current_transaction_offset(key);
|
||||
FURI_LOG_I(TAG, "Current transaction offset: %u", current);
|
||||
|
||||
// Split credit into multiple transactions. Stop at 5 cent.
|
||||
do {
|
||||
// Save current credit to precedent
|
||||
precedent_credit = actual_credit;
|
||||
|
||||
// Choose current recharge
|
||||
if(cents / 200 > 0) {
|
||||
cents -= 200;
|
||||
actual_credit += 200;
|
||||
} else if(cents / 100 > 0) {
|
||||
cents -= 100;
|
||||
actual_credit += 100;
|
||||
} else if(cents / 50 > 0) {
|
||||
cents -= 50;
|
||||
actual_credit += 50;
|
||||
} else if(cents / 20 > 0) {
|
||||
cents -= 20;
|
||||
actual_credit += 20;
|
||||
} else if(cents / 10 > 0) {
|
||||
cents -= 10;
|
||||
actual_credit += 10;
|
||||
} else if(cents / 5 > 0) {
|
||||
cents -= 5;
|
||||
actual_credit += 5;
|
||||
} else {
|
||||
// Less than 5 cents
|
||||
actual_credit += cents;
|
||||
cents = 0;
|
||||
}
|
||||
|
||||
// Point to new credit position
|
||||
current = (current == 7) ? 0 : current + 1;
|
||||
|
||||
// Save new credit to history blocks
|
||||
uint32_t txn_block = (uint32_t)day << 27 | (uint32_t)month << 23 |
|
||||
(uint32_t)year << 16 | actual_credit;
|
||||
key->eeprom[0x34 + current] = txn_block;
|
||||
|
||||
FURI_LOG_I(TAG, "Transaction %u: %u cents at block 0x%02X",
|
||||
current, actual_credit, 0x34 + current);
|
||||
} while(cents > 5);
|
||||
|
||||
FURI_LOG_I(TAG, "Final credit: %u cents, precedent: %u cents",
|
||||
actual_credit, precedent_credit);
|
||||
|
||||
// Save new credit to 21 and 25
|
||||
key->eeprom[0x21] = actual_credit;
|
||||
calculate_block_checksum(&key->eeprom[0x21], 0x21);
|
||||
encode_decode_block(&key->eeprom[0x21]);
|
||||
key->eeprom[0x21] ^= key->encryption_key;
|
||||
|
||||
key->eeprom[0x25] = actual_credit;
|
||||
calculate_block_checksum(&key->eeprom[0x25], 0x25);
|
||||
encode_decode_block(&key->eeprom[0x25]);
|
||||
key->eeprom[0x25] ^= key->encryption_key;
|
||||
|
||||
// Save precedent credit to 23 and 27
|
||||
key->eeprom[0x23] = precedent_credit;
|
||||
calculate_block_checksum(&key->eeprom[0x23], 0x23);
|
||||
encode_decode_block(&key->eeprom[0x23]);
|
||||
|
||||
key->eeprom[0x27] = precedent_credit;
|
||||
calculate_block_checksum(&key->eeprom[0x27], 0x27);
|
||||
encode_decode_block(&key->eeprom[0x27]);
|
||||
|
||||
// Save transaction pointer to block 3C
|
||||
key->eeprom[0x3C] = current << 16;
|
||||
calculate_block_checksum(&key->eeprom[0x3C], 0x3C);
|
||||
encode_decode_block(&key->eeprom[0x3C]);
|
||||
key->eeprom[0x3C] ^= key->eeprom[0x07] & 0x00FFFFFF;
|
||||
|
||||
// Increment operation counter (block 0x12, lower 24 bits)
|
||||
uint32_t op_count = (key->eeprom[0x12] & 0x00FFFFFF) + 1;
|
||||
key->eeprom[0x12] = (key->eeprom[0x12] & 0xFF000000) | (op_count & 0x00FFFFFF);
|
||||
FURI_LOG_I(TAG, "Operation counter incremented to: %lu", op_count);
|
||||
|
||||
// Mark as modified
|
||||
key->is_modified = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reset credit history and charge N cents
|
||||
bool mykey_set_cents(MyKeyData* key, uint16_t cents, uint8_t day, uint8_t month, uint8_t year) {
|
||||
// Backup precedent blocks
|
||||
uint32_t dump[10];
|
||||
memcpy(dump, &key->eeprom[0x21], SRIX_BLOCK_LENGTH);
|
||||
memcpy(dump + 1, &key->eeprom[0x34], 9 * SRIX_BLOCK_LENGTH);
|
||||
|
||||
key->eeprom[0x21] = 0;
|
||||
calculate_block_checksum(&key->eeprom[0x21], 0x21);
|
||||
encode_decode_block(&key->eeprom[0x21]);
|
||||
key->eeprom[0x21] ^= key->encryption_key;
|
||||
|
||||
// Reset transaction history and pointer (0x34-0x3C)
|
||||
memset(&key->eeprom[0x34], 0xFF, 9 * SRIX_BLOCK_LENGTH);
|
||||
|
||||
// If there is an error, restore precedent dump
|
||||
if(!mykey_add_cents(key, cents, day, month, year)) {
|
||||
memcpy(&key->eeprom[0x21], dump, SRIX_BLOCK_LENGTH);
|
||||
memcpy(&key->eeprom[0x34], dump + 1, 9 * SRIX_BLOCK_LENGTH);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reset a MyKey to associate it with another vendor
|
||||
void mykey_reset(MyKeyData* key) {
|
||||
for(uint8_t i = 0x10; i < SRIX4K_BLOCKS; i++) {
|
||||
uint32_t current_block;
|
||||
|
||||
switch(i) {
|
||||
case 0x10:
|
||||
case 0x14:
|
||||
case 0x3F:
|
||||
case 0x43: {
|
||||
// Key ID (first byte) + days elapsed from production
|
||||
uint32_t production_date = key->eeprom[0x08];
|
||||
|
||||
// Decode BCD (Binary Coded Decimal) production date
|
||||
uint8_t pday = (production_date >> 28 & 0x0F) * 10 + (production_date >> 24 & 0x0F);
|
||||
uint8_t pmonth = (production_date >> 20 & 0x0F) * 10 + (production_date >> 16 & 0x0F);
|
||||
uint16_t pyear = (production_date & 0x0F) * 1000 +
|
||||
(production_date >> 4 & 0x0F) * 100 +
|
||||
(production_date >> 12 & 0x0F) * 10 +
|
||||
(production_date >> 8 & 0x0F);
|
||||
|
||||
uint32_t elapsed = days_difference(pday, pmonth, pyear);
|
||||
current_block = ((key->eeprom[0x07] & 0xFF000000) >> 8) |
|
||||
(((elapsed / 1000 % 10) << 12) + ((elapsed / 100 % 10) << 8)) |
|
||||
(((elapsed / 10 % 10) << 4) + (elapsed % 10));
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x11:
|
||||
case 0x15:
|
||||
case 0x40:
|
||||
case 0x44:
|
||||
// Key ID [last three bytes]
|
||||
current_block = key->eeprom[0x07];
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
break;
|
||||
|
||||
case 0x22:
|
||||
case 0x26:
|
||||
case 0x51:
|
||||
case 0x55: {
|
||||
// Production date (last three bytes)
|
||||
uint32_t production_date = key->eeprom[0x08];
|
||||
current_block = (production_date & 0x0000FF00) << 8 | (production_date & 0x00FF0000) >> 8 |
|
||||
(production_date & 0xFF000000) >> 24;
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
encode_decode_block(¤t_block);
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x12:
|
||||
case 0x16:
|
||||
case 0x41:
|
||||
case 0x45:
|
||||
// Operations counter
|
||||
current_block = 1;
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
break;
|
||||
|
||||
case 0x13:
|
||||
case 0x17:
|
||||
case 0x42:
|
||||
case 0x46:
|
||||
// Generic blocks
|
||||
current_block = 0x00040013;
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
break;
|
||||
|
||||
case 0x18:
|
||||
case 0x1C:
|
||||
case 0x47:
|
||||
case 0x4B:
|
||||
// Generic blocks
|
||||
current_block = 0x0000FEDC;
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
encode_decode_block(¤t_block);
|
||||
break;
|
||||
|
||||
case 0x19:
|
||||
case 0x1D:
|
||||
case 0x48:
|
||||
case 0x4C:
|
||||
// Generic blocks
|
||||
current_block = 0x00000123;
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
encode_decode_block(¤t_block);
|
||||
break;
|
||||
|
||||
case 0x21:
|
||||
case 0x25:
|
||||
// Current credit (0,00€)
|
||||
mykey_calculate_encryption_key(key);
|
||||
current_block = 0;
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
encode_decode_block(¤t_block);
|
||||
current_block ^= key->encryption_key;
|
||||
break;
|
||||
|
||||
case 0x20:
|
||||
case 0x24:
|
||||
case 0x4F:
|
||||
case 0x53:
|
||||
// Generic blocks
|
||||
current_block = 0x00010000;
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
encode_decode_block(¤t_block);
|
||||
break;
|
||||
|
||||
case 0x1A:
|
||||
case 0x1B:
|
||||
case 0x1E:
|
||||
case 0x1F:
|
||||
case 0x23:
|
||||
case 0x27:
|
||||
case 0x49:
|
||||
case 0x4A:
|
||||
case 0x4D:
|
||||
case 0x4E:
|
||||
case 0x50:
|
||||
case 0x52:
|
||||
case 0x54:
|
||||
case 0x56:
|
||||
// Generic blocks
|
||||
current_block = 0;
|
||||
calculate_block_checksum(¤t_block, i);
|
||||
encode_decode_block(¤t_block);
|
||||
break;
|
||||
|
||||
default:
|
||||
current_block = 0xFFFFFFFF;
|
||||
break;
|
||||
}
|
||||
|
||||
// If this block has a different value than EEPROM, modify it
|
||||
if(key->eeprom[i] != current_block) {
|
||||
key->eeprom[i] = current_block;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as modified
|
||||
key->is_modified = true;
|
||||
}
|
||||
|
||||
|
||||
// Save raw card data to file for debugging
|
||||
bool mykey_save_raw_data(COGSMyKaiApp* app, const char* path) {
|
||||
if(!app->mykey.is_loaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* file = storage_file_alloc(storage);
|
||||
|
||||
if(!storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write header
|
||||
FuriString* line = furi_string_alloc();
|
||||
furi_string_printf(line, "MyKey Raw Data Dump\n");
|
||||
furi_string_cat_printf(line, "UID: %016llX\n", (unsigned long long)app->mykey.uid);
|
||||
furi_string_cat_printf(line, "Encryption Key: 0x%08lX\n\n", app->mykey.encryption_key);
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
|
||||
// Write all blocks
|
||||
for(size_t i = 0; i < SRIX4K_BLOCKS; i++) {
|
||||
furi_string_printf(line, "Block 0x%02zX: 0x%08lX\n", i, app->mykey.eeprom[i]);
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
}
|
||||
|
||||
furi_string_free(line);
|
||||
storage_file_close(file);
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
return true;
|
||||
}
|
||||
152
nfc_srix.c
Normal file
152
nfc_srix.c
Normal file
@ -0,0 +1,152 @@
|
||||
#include "cogs_mikai.h"
|
||||
#include <furi.h>
|
||||
#include <machine/endian.h>
|
||||
#include <nfc/nfc.h>
|
||||
#include <nfc/protocols/st25tb/st25tb.h>
|
||||
#include <nfc/protocols/st25tb/st25tb_poller_sync.h>
|
||||
|
||||
bool mykey_read_from_nfc(COGSMyKaiApp* app) {
|
||||
FURI_LOG_I(TAG, "Reading SRIX4K from NFC...");
|
||||
|
||||
bool success = false;
|
||||
Nfc* nfc = nfc_alloc();
|
||||
|
||||
// Detect ST25TB card type
|
||||
St25tbType type;
|
||||
St25tbError error = st25tb_poller_sync_detect_type(nfc, &type);
|
||||
|
||||
if(error != St25tbErrorNone) {
|
||||
FURI_LOG_E(TAG, "Failed to detect ST25TB card: %d", error);
|
||||
nfc_free(nfc);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's SRIX4K (ST25TBX512 or ST25TB04K or ST25TBX4K)
|
||||
if(type != St25tbTypeX512 && type != St25tbType04k && type != St25tbTypeX4k) {
|
||||
FURI_LOG_E(TAG, "Card is not SRIX4K compatible, type: %d", type);
|
||||
nfc_free(nfc);
|
||||
return false;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Detected ST25TB card type: %d", type);
|
||||
|
||||
// Allocate ST25TB data structure
|
||||
St25tbData* st25tb_data = st25tb_alloc();
|
||||
|
||||
// Read entire card
|
||||
error = st25tb_poller_sync_read(nfc, st25tb_data);
|
||||
|
||||
if(error != St25tbErrorNone) {
|
||||
FURI_LOG_E(TAG, "Failed to read ST25TB card: %d", error);
|
||||
st25tb_free(st25tb_data);
|
||||
nfc_free(nfc);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract UID (8 bytes for ST25TB)
|
||||
// ST25TB UID bytes are in order [0..7], we need to assemble them big-endian
|
||||
// to match libmikai: uid[0] is MSB (bits 56-63), uid[7] is LSB (bits 0-7)
|
||||
app->mykey.uid = 0;
|
||||
for(size_t i = 0; i < ST25TB_UID_SIZE && i < 8; i++) {
|
||||
app->mykey.uid |= ((uint64_t)st25tb_data->uid[i]) << ((7 - i) * 8);
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Card UID (big-endian): %016llX", app->mykey.uid);
|
||||
FURI_LOG_I(TAG, "UID bytes: %02X %02X %02X %02X %02X %02X %02X %02X",
|
||||
st25tb_data->uid[0], st25tb_data->uid[1], st25tb_data->uid[2], st25tb_data->uid[3],
|
||||
st25tb_data->uid[4], st25tb_data->uid[5], st25tb_data->uid[6], st25tb_data->uid[7]);
|
||||
|
||||
// Copy blocks to MyKey data structure
|
||||
// ST25TB stores data in blocks, we need to read all 128 blocks (512 bytes total)
|
||||
size_t num_blocks = st25tb_get_block_count(type);
|
||||
if(num_blocks > SRIX4K_BLOCKS) {
|
||||
num_blocks = SRIX4K_BLOCKS;
|
||||
}
|
||||
|
||||
// ST25TB blocks need byte-swapping to match libmikai's big-endian format
|
||||
// Flipper SDK stores blocks in little-endian, libmikai expects big-endian
|
||||
for(size_t i = 0; i < num_blocks; i++) {
|
||||
app->mykey.eeprom[i] = __bswap32(st25tb_data->blocks[i]);
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Blocks byte-swapped to big-endian format");
|
||||
|
||||
// Calculate encryption key from UID
|
||||
mykey_calculate_encryption_key(&app->mykey);
|
||||
|
||||
// Update cached values
|
||||
app->mykey.is_loaded = true;
|
||||
app->mykey.is_modified = false; // Fresh read from card
|
||||
app->mykey.is_reset = mykey_is_reset(&app->mykey);
|
||||
app->mykey.current_credit = mykey_get_current_credit(&app->mykey);
|
||||
|
||||
FURI_LOG_I(TAG, "Card loaded successfully. Credit: %d cents, Reset: %s",
|
||||
app->mykey.current_credit,
|
||||
app->mykey.is_reset ? "Yes" : "No");
|
||||
|
||||
success = true;
|
||||
|
||||
st25tb_free(st25tb_data);
|
||||
nfc_free(nfc);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool mykey_write_to_nfc(COGSMyKaiApp* app) {
|
||||
FURI_LOG_I(TAG, "Writing to SRIX4K via NFC...");
|
||||
|
||||
if(!app->mykey.is_loaded) {
|
||||
FURI_LOG_E(TAG, "No card data loaded, cannot write");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
Nfc* nfc = nfc_alloc();
|
||||
|
||||
// Detect ST25TB card type
|
||||
St25tbType type;
|
||||
St25tbError error = st25tb_poller_sync_detect_type(nfc, &type);
|
||||
|
||||
if(error != St25tbErrorNone) {
|
||||
FURI_LOG_E(TAG, "Failed to detect ST25TB card: %d", error);
|
||||
nfc_free(nfc);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's SRIX4K
|
||||
if(type != St25tbTypeX512 && type != St25tbType04k && type != St25tbTypeX4k) {
|
||||
FURI_LOG_E(TAG, "Card is not SRIX4K compatible, type: %d", type);
|
||||
nfc_free(nfc);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t num_blocks = st25tb_get_block_count(type);
|
||||
if(num_blocks > SRIX4K_BLOCKS) {
|
||||
num_blocks = SRIX4K_BLOCKS;
|
||||
}
|
||||
|
||||
// Write each block
|
||||
// Note: Block 0 (UID) is typically read-only, so we skip it
|
||||
for(size_t i = 1; i < num_blocks; i++) {
|
||||
// Byte-swap block back to little-endian for ST25TB card
|
||||
// Our internal format is big-endian, ST25TB expects little-endian
|
||||
uint32_t block_to_write = __bswap32(app->mykey.eeprom[i]);
|
||||
error = st25tb_poller_sync_write_block(nfc, i, block_to_write);
|
||||
|
||||
if(error != St25tbErrorNone) {
|
||||
FURI_LOG_E(TAG, "Failed to write block %zu: %d", i, error);
|
||||
success = false;
|
||||
// Continue trying to write remaining blocks
|
||||
}
|
||||
}
|
||||
|
||||
if(success) {
|
||||
FURI_LOG_I(TAG, "Card written successfully");
|
||||
} else {
|
||||
FURI_LOG_W(TAG, "Card write completed with errors");
|
||||
}
|
||||
|
||||
nfc_free(nfc);
|
||||
|
||||
return success;
|
||||
}
|
||||
160
parse_mykey_file.py
Normal file
160
parse_mykey_file.py
Normal file
@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
COGES MyKey File Parser
|
||||
Parses .myk files created by the COGES MyKai Flipper Zero application.
|
||||
- a luhf shitscript
|
||||
"""
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
def bswap32(val):
|
||||
"""Byte swap 32-bit value"""
|
||||
return ((val & 0xFF) << 24) | ((val & 0xFF00) << 8) | ((val & 0xFF0000) >> 8) | ((val >> 24) & 0xFF)
|
||||
|
||||
def encode_decode_block(block):
|
||||
"""libmikai encode_decode function"""
|
||||
block ^= ((block & 0x00C00000) << 6 | (block & 0x0000C000) << 12 | (block & 0x000000C0) << 18 |
|
||||
(block & 0x000C0000) >> 6 | (block & 0x00030000) >> 12 | (block & 0x00000300) >> 6)
|
||||
block ^= ((block & 0x30000000) >> 6 | (block & 0x0C000000) >> 12 | (block & 0x03000000) >> 18 |
|
||||
(block & 0x00003000) << 6 | (block & 0x00000030) << 12 | (block & 0x0000000C) << 6)
|
||||
block ^= ((block & 0x00C00000) << 6 | (block & 0x0000C000) << 12 | (block & 0x000000C0) << 18 |
|
||||
(block & 0x000C0000) >> 6 | (block & 0x00030000) >> 12 | (block & 0x00000300) >> 6)
|
||||
return block & 0xFFFFFFFF
|
||||
|
||||
def parse_mykey_file(filename):
|
||||
"""Parse a .myk file and display its contents"""
|
||||
try:
|
||||
with open(filename, 'r') as f:
|
||||
lines = f.readlines()
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File '{filename}' not found")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error reading file: {e}")
|
||||
return False
|
||||
|
||||
# Verify header
|
||||
if not lines[0].startswith("COGES_MYKEY_V1"):
|
||||
print("Error: Invalid file format (missing header)")
|
||||
return False
|
||||
|
||||
# Parse UID
|
||||
uid_line = lines[1].strip()
|
||||
if not uid_line.startswith("UID:"):
|
||||
print("Error: Invalid file format (missing UID)")
|
||||
return False
|
||||
uid = int(uid_line.split(":")[1].strip(), 16)
|
||||
|
||||
# Parse encryption key
|
||||
key_line = lines[2].strip()
|
||||
if not key_line.startswith("ENCRYPTION_KEY:"):
|
||||
print("Error: Invalid file format (missing encryption key)")
|
||||
return False
|
||||
encryption_key = int(key_line.split(":")[1].strip(), 16)
|
||||
|
||||
# Parse blocks
|
||||
blocks = {}
|
||||
for line in lines[3:]:
|
||||
line = line.strip()
|
||||
if line.startswith("BLOCK_"):
|
||||
parts = line.split(":")
|
||||
block_num = int(parts[0].split("_")[1])
|
||||
block_val = int(parts[1].strip(), 16)
|
||||
blocks[block_num] = block_val
|
||||
|
||||
if len(blocks) != 128:
|
||||
print(f"Warning: Expected 128 blocks, found {len(blocks)}")
|
||||
|
||||
# Display information
|
||||
print("=" * 60)
|
||||
print(" COGES MyKey Card Information")
|
||||
print("=" * 60)
|
||||
print(f"\nUID: 0x{uid:016X}")
|
||||
print(f"Encryption Key: 0x{encryption_key:08X}")
|
||||
|
||||
# Serial number (block 0x07 in BCD format)
|
||||
serial = blocks.get(0x07, 0)
|
||||
print(f"Serial Number: {serial:08X}")
|
||||
|
||||
# Decode credit from block 0x21
|
||||
block_21 = blocks.get(0x21, 0)
|
||||
current_credit_raw = block_21 ^ encryption_key
|
||||
current_credit_decoded = encode_decode_block(current_credit_raw)
|
||||
credit_value = current_credit_decoded & 0xFFFF
|
||||
print(f"\nCurrent Credit: {credit_value} cents ({credit_value/100:.2f} EUR)")
|
||||
|
||||
# Operation counter (block 0x12, lower 24 bits)
|
||||
block_12 = blocks.get(0x12, 0)
|
||||
op_count = block_12 & 0x00FFFFFF
|
||||
print(f"Operations: {op_count}")
|
||||
|
||||
# Check if reset
|
||||
block_18 = blocks.get(0x18, 0)
|
||||
block_19 = blocks.get(0x19, 0)
|
||||
is_reset = (block_18 == 0x8FCD0F48 and block_19 == 0xC0820007)
|
||||
print(f"Status: {'Reset' if is_reset else 'Active'}")
|
||||
|
||||
# Parse transaction history
|
||||
block_3C = blocks.get(0x3C, 0xFFFFFFFF)
|
||||
block_07 = blocks.get(0x07, 0)
|
||||
|
||||
if block_3C != 0xFFFFFFFF:
|
||||
block_3C_decrypted = block_3C ^ block_07
|
||||
starting_offset = ((block_3C_decrypted & 0x30000000) >> 28) | \
|
||||
((block_3C_decrypted & 0x00100000) >> 18)
|
||||
|
||||
if starting_offset < 8:
|
||||
# Count transactions
|
||||
transactions = []
|
||||
for i in range(8):
|
||||
txn_block = blocks.get(0x34 + ((starting_offset + i) % 8), 0xFFFFFFFF)
|
||||
if txn_block == 0xFFFFFFFF:
|
||||
break
|
||||
|
||||
day = txn_block >> 27
|
||||
month = (txn_block >> 23) & 0xF
|
||||
year = 2000 + ((txn_block >> 16) & 0x7F)
|
||||
credit = txn_block & 0xFFFF
|
||||
|
||||
transactions.append({
|
||||
'date': f"{day:02d}/{month:02d}/{year}",
|
||||
'credit': credit
|
||||
})
|
||||
|
||||
if transactions:
|
||||
print("\n" + "=" * 60)
|
||||
print(" Transaction History (Newest First)")
|
||||
print("=" * 60)
|
||||
for i, txn in enumerate(reversed(transactions), 1):
|
||||
print(f"{i}. {txn['date']} - {txn['credit']} cents ({txn['credit']/100:.2f} EUR)")
|
||||
|
||||
# Display interesting blocks
|
||||
print("\n" + "=" * 60)
|
||||
print(" Key Blocks")
|
||||
print("=" * 60)
|
||||
interesting_blocks = [0x06, 0x07, 0x12, 0x18, 0x19, 0x21, 0x23, 0x25, 0x27, 0x3C]
|
||||
for block_num in interesting_blocks:
|
||||
if block_num in blocks:
|
||||
print(f"Block 0x{block_num:02X}: 0x{blocks[block_num]:08X}")
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 parse_mykey_file.py <file.myk>")
|
||||
print("\nThis script parses COGES MyKey files created by the Flipper Zero app")
|
||||
print("and displays card information in a human-readable format.")
|
||||
sys.exit(1)
|
||||
|
||||
filename = sys.argv[1]
|
||||
if parse_mykey_file(filename):
|
||||
print("\n" + "=" * 60)
|
||||
print(" Parsing completed successfully")
|
||||
print("=" * 60)
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
#culo
|
||||
26
scenes/cogs_mikai_scene.c
Normal file
26
scenes/cogs_mikai_scene.c
Normal file
@ -0,0 +1,26 @@
|
||||
#include "../cogs_mikai.h"
|
||||
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
|
||||
void (* const cogs_mikai_scene_on_enter_handlers[])(void*) = {
|
||||
#include "cogs_mikai_scene_config.c"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
|
||||
bool (* const cogs_mikai_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
|
||||
#include "cogs_mikai_scene_config.c"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
|
||||
void (* const cogs_mikai_scene_on_exit_handlers[])(void*) = {
|
||||
#include "cogs_mikai_scene_config.c"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
const SceneManagerHandlers cogs_mikai_scene_handlers = {
|
||||
.on_enter_handlers = cogs_mikai_scene_on_enter_handlers,
|
||||
.on_event_handlers = cogs_mikai_scene_on_event_handlers,
|
||||
.on_exit_handlers = cogs_mikai_scene_on_exit_handlers,
|
||||
.scene_num = COGSMyKaiSceneCount,
|
||||
};
|
||||
25
scenes/cogs_mikai_scene_about.c
Normal file
25
scenes/cogs_mikai_scene_about.c
Normal file
@ -0,0 +1,25 @@
|
||||
#include "../cogs_mikai.h"
|
||||
|
||||
void cogs_mikai_scene_about_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
Widget* widget = app->widget;
|
||||
|
||||
widget_add_string_element(widget, 64, 5, AlignCenter, AlignTop, FontPrimary, "COGS MyKai");
|
||||
widget_add_string_element(widget, 64, 18, AlignCenter, AlignTop, FontSecondary, "v0.4");
|
||||
widget_add_string_element(widget, 64, 30, AlignCenter, AlignTop, FontSecondary, "COGES MyKey NFC");
|
||||
widget_add_string_element(widget, 64, 40, AlignCenter, AlignTop, FontSecondary, "Reader/Writer");
|
||||
widget_add_string_element(widget, 64, 52, AlignCenter, AlignTop, FontSecondary, "Based on libmikai, built by luhf");
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewWidget);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_about_on_event(void* context, SceneManagerEvent event) {
|
||||
UNUSED(context);
|
||||
UNUSED(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_about_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
widget_reset(app->widget);
|
||||
}
|
||||
207
scenes/cogs_mikai_scene_add_credit.c
Normal file
207
scenes/cogs_mikai_scene_add_credit.c
Normal file
@ -0,0 +1,207 @@
|
||||
#include "../cogs_mikai.h"
|
||||
#include <furi_hal_rtc.h>
|
||||
|
||||
enum {
|
||||
AddCreditSceneEventInput,
|
||||
AddCreditSceneEventWrite,
|
||||
AddCreditSceneEventSave,
|
||||
AddCreditSceneEventDiscard,
|
||||
};
|
||||
|
||||
static bool cogs_mikai_scene_add_credit_validator(const char* text, FuriString* error, void* context) {
|
||||
UNUSED(context);
|
||||
|
||||
if(strlen(text) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// digits and at most one decimal point
|
||||
bool has_decimal = false;
|
||||
for(size_t i = 0; text[i] != '\0'; i++) {
|
||||
if(text[i] == '.' || text[i] == ',') {
|
||||
if(has_decimal) {
|
||||
furi_string_set(error, "Only one decimal point");
|
||||
return false;
|
||||
}
|
||||
has_decimal = true;
|
||||
} else if(text[i] < '0' || text[i] > '9') {
|
||||
furi_string_set(error, "Only numbers and '.'");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_euros_to_cents(const char* text, uint16_t* cents) {
|
||||
if(!text || !cents) return false;
|
||||
|
||||
uint32_t integer_part = 0;
|
||||
uint32_t decimal_part = 0;
|
||||
uint32_t decimal_digits = 0;
|
||||
bool in_decimal = false;
|
||||
|
||||
for(size_t i = 0; text[i] != '\0'; i++) {
|
||||
if(text[i] == '.' || text[i] == ',') {
|
||||
in_decimal = true;
|
||||
} else if(text[i] >= '0' && text[i] <= '9') {
|
||||
if(in_decimal) {
|
||||
if(decimal_digits < 2) {
|
||||
decimal_part = decimal_part * 10 + (text[i] - '0');
|
||||
decimal_digits++;
|
||||
}
|
||||
} else {
|
||||
integer_part = integer_part * 10 + (text[i] - '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
if(decimal_digits == 1) {
|
||||
decimal_part *= 10;
|
||||
}
|
||||
|
||||
uint32_t total_cents = integer_part * 100 + decimal_part;
|
||||
if(total_cents > 99999) return false; // Max 999.99 EUR
|
||||
|
||||
*cents = (uint16_t)total_cents;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cogs_mikai_scene_add_credit_text_input_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, AddCreditSceneEventInput);
|
||||
}
|
||||
|
||||
static void cogs_mikai_scene_add_credit_popup_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_add_credit_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
|
||||
if(!app->mykey.is_loaded) {
|
||||
Popup* popup = app->popup;
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "No card loaded\nRead a card first", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_add_credit_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
return;
|
||||
}
|
||||
|
||||
TextInput* text_input = app->text_input;
|
||||
snprintf(app->text_buffer, sizeof(app->text_buffer), "5.00");
|
||||
|
||||
text_input_set_header_text(text_input, "Add Credit (EUR)");
|
||||
text_input_set_validator(text_input, cogs_mikai_scene_add_credit_validator, NULL);
|
||||
text_input_set_result_callback(
|
||||
text_input,
|
||||
cogs_mikai_scene_add_credit_text_input_callback,
|
||||
app,
|
||||
app->text_buffer,
|
||||
sizeof(app->text_buffer),
|
||||
false);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextInput);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_add_credit_on_event(void* context, SceneManagerEvent event) {
|
||||
COGSMyKaiApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == AddCreditSceneEventInput) {
|
||||
if(app->text_buffer[0] == '\0') {
|
||||
FURI_LOG_W(TAG, "Ignoring empty text_buffer (already processed)");
|
||||
consumed = true;
|
||||
return consumed;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Add credit input: '%s'", app->text_buffer);
|
||||
uint16_t cents = 0;
|
||||
bool parse_ok = parse_euros_to_cents(app->text_buffer, ¢s);
|
||||
FURI_LOG_I(TAG, "Parse result: %d, cents: %d", parse_ok, cents);
|
||||
|
||||
if(parse_ok && cents > 0) {
|
||||
FURI_LOG_I(TAG, "Valid amount: %d cents", cents);
|
||||
|
||||
DateTime datetime;
|
||||
furi_hal_rtc_get_datetime(&datetime);
|
||||
|
||||
bool success = mykey_add_cents(
|
||||
&app->mykey,
|
||||
cents,
|
||||
datetime.day,
|
||||
datetime.month,
|
||||
datetime.year - 2000);
|
||||
|
||||
if(success) {
|
||||
// cache updated credit
|
||||
app->mykey.current_credit = mykey_get_current_credit(&app->mykey);
|
||||
|
||||
// reset text input to prevent any further callbacks
|
||||
text_input_reset(app->text_input);
|
||||
|
||||
// clear text buffer to prevent re-triggering
|
||||
memset(app->text_buffer, 0, sizeof(app->text_buffer));
|
||||
|
||||
Popup* popup = app->popup;
|
||||
popup_set_header(popup, "Credit Added!", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Saved in memory\nUse 'Write to Card'", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_add_credit_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
notification_message(app->notifications, &sequence_success);
|
||||
} else {
|
||||
Popup* popup = app->popup;
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Failed to add credit", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_add_credit_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
notification_message(app->notifications, &sequence_error);
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Invalid amount: parse_ok=%d, cents=%d",
|
||||
parse_ok, cents);
|
||||
Popup* popup = app->popup;
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(
|
||||
popup,
|
||||
"Invalid amount\nEnter 0.01-999.99",
|
||||
64,
|
||||
25,
|
||||
AlignCenter,
|
||||
AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_add_credit_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
}
|
||||
consumed = true;
|
||||
} else {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart);
|
||||
consumed = true;
|
||||
}
|
||||
} else if(event.type == SceneManagerEventTypeBack) {
|
||||
if(app->mykey.is_modified) {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_add_credit_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
text_input_reset(app->text_input);
|
||||
popup_reset(app->popup);
|
||||
}
|
||||
11
scenes/cogs_mikai_scene_config.c
Normal file
11
scenes/cogs_mikai_scene_config.c
Normal file
@ -0,0 +1,11 @@
|
||||
ADD_SCENE(cogs_mikai, start, Start)
|
||||
ADD_SCENE(cogs_mikai, read, Read)
|
||||
ADD_SCENE(cogs_mikai, info, Info)
|
||||
ADD_SCENE(cogs_mikai, write_card, WriteCard)
|
||||
ADD_SCENE(cogs_mikai, add_credit, AddCredit)
|
||||
ADD_SCENE(cogs_mikai, set_credit, SetCredit)
|
||||
ADD_SCENE(cogs_mikai, reset, Reset)
|
||||
ADD_SCENE(cogs_mikai, save_file, SaveFile)
|
||||
ADD_SCENE(cogs_mikai, load_file, LoadFile)
|
||||
ADD_SCENE(cogs_mikai, debug, Debug)
|
||||
ADD_SCENE(cogs_mikai, about, About)
|
||||
26
scenes/cogs_mikai_scene_config.h
Normal file
26
scenes/cogs_mikai_scene_config.h
Normal file
@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/scene_manager.h>
|
||||
|
||||
// Generate scene on_enter handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
|
||||
#include "cogs_mikai_scene_config.c"
|
||||
#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 "cogs_mikai_scene_config.c"
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene on_exit handlers declaration
|
||||
#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
|
||||
#include "cogs_mikai_scene_config.c"
|
||||
#undef ADD_SCENE
|
||||
|
||||
// Generate scene configuration array
|
||||
#define ADD_SCENE(prefix, name, id) \
|
||||
{.on_enter = prefix##_scene_##name##_on_enter, .on_event = prefix##_scene_##name##_on_event, \
|
||||
.on_exit = prefix##_scene_##name##_on_exit},
|
||||
|
||||
extern const SceneManagerHandlers cogs_mikai_scene_handlers;
|
||||
177
scenes/cogs_mikai_scene_debug.c
Normal file
177
scenes/cogs_mikai_scene_debug.c
Normal file
@ -0,0 +1,177 @@
|
||||
#include "../cogs_mikai.h"
|
||||
#include <storage/storage.h>
|
||||
#include <machine/endian.h>
|
||||
#include <furi_hal_rtc.h>
|
||||
|
||||
enum {
|
||||
DebugSceneEventSaveData = 1,
|
||||
};
|
||||
|
||||
void cogs_mikai_scene_debug_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
TextBox* text_box = app->text_box;
|
||||
FuriString* text = app->text_box_store;
|
||||
|
||||
furi_string_reset(text);
|
||||
|
||||
if(!app->mykey.is_loaded) {
|
||||
furi_string_cat(text, "No Card Loaded\n\nPlease read a card first.");
|
||||
} else {
|
||||
furi_string_cat(text, "=== DEBUG INFO ===\n\n");
|
||||
|
||||
// UID
|
||||
furi_string_cat_printf(
|
||||
text, "UID: %016llX\n", (unsigned long long)app->mykey.uid);
|
||||
|
||||
// lower 32 bits of UID
|
||||
furi_string_cat_printf(
|
||||
text, "UID (lower 32): 0x%08lX\n", (uint32_t)app->mykey.uid);
|
||||
|
||||
// encryption key
|
||||
|
||||
furi_string_cat_printf(
|
||||
text, "Encryption Key: 0x%08lX\n\n", app->mykey.encryption_key);
|
||||
|
||||
// block 0x21 (credit block) analysis
|
||||
furi_string_cat(text, "--- Block 0x21 Analysis ---\n");
|
||||
uint32_t block21_raw = app->mykey.eeprom[0x21];
|
||||
furi_string_cat_printf(text, "Raw: 0x%08lX\n", block21_raw);
|
||||
|
||||
// show individual bytes
|
||||
furi_string_cat_printf(text, "Bytes: [%02X %02X %02X %02X]\n",
|
||||
(uint8_t)(block21_raw & 0xFF),
|
||||
(uint8_t)((block21_raw >> 8) & 0xFF),
|
||||
(uint8_t)((block21_raw >> 16) & 0xFF),
|
||||
(uint8_t)((block21_raw >> 24) & 0xFF));
|
||||
|
||||
// uint32_t block21_xor = block21_raw ^ app->mykey.encryption_key;
|
||||
// furi_string_cat_printf(text, "After XOR: 0x%08lX\n", block21_xor);
|
||||
|
||||
|
||||
// uint32_t block21_swapped = __bswap32(block21_raw);
|
||||
// furi_string_cat_printf(text, "Byte-swapped: 0x%08lX\n", block21_swapped);
|
||||
|
||||
// uint32_t block21_xor_swapped = block21_swapped ^ app->mykey.encryption_key;
|
||||
// furi_string_cat_printf(text, "Swap then XOR: 0x%08lX\n\n", block21_xor_swapped);
|
||||
|
||||
|
||||
// furi_string_cat(text, "--- Test Combinations ---\n");
|
||||
|
||||
// uint32_t test_a = block21_raw ^ app->mykey.encryption_key;
|
||||
// mykey_encode_decode_block(&test_a);
|
||||
// uint16_t credit_a = test_a & 0xFFFF;
|
||||
// furi_string_cat_printf(text, "A (Raw->XOR->Dec): %u.%02u\n", credit_a / 100, credit_a % 100);
|
||||
// uint32_t test_b = block21_swapped ^ app->mykey.encryption_key;
|
||||
// mykey_encode_decode_block(&test_b);
|
||||
// uint16_t credit_b = test_b & 0xFFFF;
|
||||
// furi_string_cat_printf(text, "B (Swap->XOR->Dec): %u.%02u\n", credit_b / 100, credit_b % 100);
|
||||
|
||||
// uint32_t test_c = __bswap32(block21_raw ^ app->mykey.encryption_key);
|
||||
// mykey_encode_decode_block(&test_c);
|
||||
// uint16_t credit_c = test_c & 0xFFFF;
|
||||
// furi_string_cat_printf(text, "C (XOR->Swap->Dec): %u.%02u\n", credit_c / 100, credit_c % 100);
|
||||
|
||||
// uint32_t test_d = block21_raw;
|
||||
// mykey_encode_decode_block(&test_d);
|
||||
// test_d ^= app->mykey.encryption_key;
|
||||
// uint16_t credit_d = test_d & 0xFFFF;
|
||||
// furi_string_cat_printf(text, "D (Dec->XOR): %u.%02u\n\n", credit_d / 100, credit_d % 100);
|
||||
|
||||
// credit calculations
|
||||
furi_string_cat(text, "--- Credit Readings ---\n");
|
||||
|
||||
// libmikai
|
||||
uint16_t credit_libmikai = mykey_get_current_credit(&app->mykey);
|
||||
furi_string_cat_printf(
|
||||
text, "libmikai (0x21): %u.%02u EUR\n", credit_libmikai / 100, credit_libmikai % 100);
|
||||
|
||||
// transaction history
|
||||
uint16_t credit_history = mykey_get_credit_from_history(&app->mykey);
|
||||
if(credit_history != 0xFFFF) {
|
||||
furi_string_cat_printf(
|
||||
text,
|
||||
"History (0x34+): %u.%02u EUR\n\n",
|
||||
credit_history / 100,
|
||||
credit_history % 100);
|
||||
} else {
|
||||
furi_string_cat(text, "History: Not available\n\n");
|
||||
}
|
||||
|
||||
// key blocks
|
||||
furi_string_cat(text, "--- Key Blocks ---\n");
|
||||
furi_string_cat_printf(text, "Block 0x05: 0x%08lX\n", app->mykey.eeprom[0x05]);
|
||||
furi_string_cat_printf(text, "Block 0x06: 0x%08lX\n", app->mykey.eeprom[0x06]);
|
||||
furi_string_cat_printf(text, "Block 0x07: 0x%08lX\n", app->mykey.eeprom[0x07]);
|
||||
furi_string_cat_printf(text, "Block 0x12: 0x%08lX\n", app->mykey.eeprom[0x12]);
|
||||
furi_string_cat_printf(text, "Block 0x18: 0x%08lX\n", app->mykey.eeprom[0x18]);
|
||||
furi_string_cat_printf(text, "Block 0x19: 0x%08lX\n", app->mykey.eeprom[0x19]);
|
||||
furi_string_cat_printf(text, "Block 0x21: 0x%08lX\n", app->mykey.eeprom[0x21]);
|
||||
furi_string_cat_printf(text, "Block 0x3C: 0x%08lX\n\n", app->mykey.eeprom[0x3C]);
|
||||
|
||||
furi_string_cat(text, "\n--- Raw data saved in SD ---\n");
|
||||
furi_string_cat(text, "Use back to exit, or\n");
|
||||
furi_string_cat(text, "check logs for raw data");
|
||||
}
|
||||
|
||||
text_box_set_text(text_box, furi_string_get_cstr(text));
|
||||
text_box_set_font(text_box, TextBoxFontText);
|
||||
text_box_set_focus(text_box, TextBoxFocusStart);
|
||||
|
||||
|
||||
if(app->mykey.is_loaded) {
|
||||
DateTime datetime;
|
||||
furi_hal_rtc_get_datetime(&datetime);
|
||||
|
||||
FuriString* file_path = furi_string_alloc();
|
||||
furi_string_printf(
|
||||
file_path,
|
||||
"/ext/mykey_debug_%04d%02d%02d_%02d%02d%02d.txt",
|
||||
datetime.year,
|
||||
datetime.month,
|
||||
datetime.day,
|
||||
datetime.hour,
|
||||
datetime.minute,
|
||||
datetime.second);
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* file = storage_file_alloc(storage);
|
||||
|
||||
if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||
|
||||
storage_file_write(file, furi_string_get_cstr(text), furi_string_size(text));
|
||||
|
||||
|
||||
storage_file_write(file, "\n\n--- Raw Data Dump ---\n", strlen("\n\n--- Raw Data Dump ---\n"));
|
||||
|
||||
FuriString* line = furi_string_alloc();
|
||||
for(size_t i = 0; i < SRIX4K_BLOCKS; i++) {
|
||||
furi_string_printf(line, "Block 0x%02zX: 0x%08lX\n", i, app->mykey.eeprom[i]);
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
}
|
||||
furi_string_free(line);
|
||||
|
||||
FURI_LOG_I(TAG, "Debug data saved to: %s", furi_string_get_cstr(file_path));
|
||||
storage_file_close(file);
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to save debug data");
|
||||
}
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
furi_string_free(file_path);
|
||||
}
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextBox);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_debug_on_event(void* context, SceneManagerEvent event) {
|
||||
UNUSED(context);
|
||||
UNUSED(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_debug_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
text_box_reset(app->text_box);
|
||||
furi_string_reset(app->text_box_store);
|
||||
}
|
||||
119
scenes/cogs_mikai_scene_info.c
Normal file
119
scenes/cogs_mikai_scene_info.c
Normal file
@ -0,0 +1,119 @@
|
||||
#include "../cogs_mikai.h"
|
||||
#include <machine/endian.h>
|
||||
|
||||
void cogs_mikai_scene_info_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
TextBox* text_box = app->text_box;
|
||||
FuriString* text = app->text_box_store;
|
||||
|
||||
furi_string_reset(text);
|
||||
|
||||
if(!app->mykey.is_loaded) {
|
||||
furi_string_cat(text, "No Card Loaded\n\nPlease read a card first.");
|
||||
} else {
|
||||
|
||||
furi_string_cat_printf(text, "Serial: %08lX\n", (uint32_t)app->mykey.eeprom[0x07]);
|
||||
|
||||
// vendor ID - calculated from blocks 0x18 and 0x19
|
||||
uint32_t block18 = app->mykey.eeprom[0x18];
|
||||
uint32_t block19 = app->mykey.eeprom[0x19];
|
||||
mykey_encode_decode_block(&block18);
|
||||
mykey_encode_decode_block(&block19);
|
||||
uint64_t vendor = (((uint64_t)block18 << 16) | (block19 & 0x0000FFFF)) + 1;
|
||||
|
||||
furi_string_cat_printf(text, "Vendor: %llX\n", vendor);
|
||||
|
||||
// current credit
|
||||
furi_string_cat_printf(
|
||||
text,
|
||||
"Credit: %u.%02u EUR\n",
|
||||
app->mykey.current_credit / 100,
|
||||
app->mykey.current_credit % 100);
|
||||
|
||||
// card status
|
||||
furi_string_cat_printf(text, "Status: %s\n", app->mykey.is_reset ? "Reset" : "Active");
|
||||
|
||||
// operation count (block 0x12, lower 24 bits)
|
||||
uint32_t op_count = app->mykey.eeprom[0x12] & 0x00FFFFFF;
|
||||
furi_string_cat_printf(text, "Operations: %lu\n", (unsigned long)op_count);
|
||||
|
||||
// UID
|
||||
furi_string_cat_printf(
|
||||
text,
|
||||
"UID: %08lX%08lX\n",
|
||||
(uint32_t)(app->mykey.uid >> 32),
|
||||
(uint32_t)(app->mykey.uid & 0xFFFFFFFF));
|
||||
|
||||
// parse and display full transaction history
|
||||
uint32_t block3C = app->mykey.eeprom[0x3C];
|
||||
if(block3C != 0xFFFFFFFF) {
|
||||
block3C ^= app->mykey.eeprom[0x07];
|
||||
uint32_t starting_offset =
|
||||
((block3C & 0x30000000) >> 28) | ((block3C & 0x00100000) >> 18);
|
||||
|
||||
if(starting_offset < 8) {
|
||||
// first, find how many transactions exist by going forward from starting_offset
|
||||
int num_transactions = 0;
|
||||
for(int i = 0; i < 8; i++) {
|
||||
uint32_t txn_block = app->mykey.eeprom[0x34 + ((starting_offset + i) % 8)];
|
||||
if(txn_block == 0xFFFFFFFF) {
|
||||
break;
|
||||
}
|
||||
num_transactions++;
|
||||
}
|
||||
|
||||
if(num_transactions > 0) {
|
||||
furi_string_cat(text, "\n=== Transaction History ===\n");
|
||||
furi_string_cat(text, "(Newest first)\n\n");
|
||||
|
||||
// display transactions in reverse order (newest first)
|
||||
for(int i = num_transactions - 1; i >= 0; i--) {
|
||||
uint32_t txn_block = app->mykey.eeprom[0x34 + ((starting_offset + i) % 8)];
|
||||
|
||||
// extract transaction fields directly from big-endian block
|
||||
uint8_t day = txn_block >> 27;
|
||||
uint8_t month = (txn_block >> 23) & 0xF;
|
||||
uint16_t year = 2000 + ((txn_block >> 16) & 0x7F);
|
||||
uint16_t credit = txn_block & 0xFFFF;
|
||||
|
||||
furi_string_cat_printf(
|
||||
text,
|
||||
"%d. %02d/%02d/%04d - %d.%02d EUR\n",
|
||||
num_transactions - i,
|
||||
day,
|
||||
month,
|
||||
year,
|
||||
credit / 100,
|
||||
credit % 100);
|
||||
}
|
||||
} else {
|
||||
furi_string_cat(text, "\nNo transaction history\n");
|
||||
}
|
||||
} else {
|
||||
furi_string_cat(text, "\nTransaction history:\n");
|
||||
furi_string_cat(text, "Invalid offset\n");
|
||||
}
|
||||
} else {
|
||||
furi_string_cat(text, "\nTransaction history:\n");
|
||||
furi_string_cat(text, "Not available\n");
|
||||
}
|
||||
}
|
||||
|
||||
text_box_set_text(text_box, furi_string_get_cstr(text));
|
||||
text_box_set_font(text_box, TextBoxFontText);
|
||||
text_box_set_focus(text_box, TextBoxFocusStart);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextBox);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_info_on_event(void* context, SceneManagerEvent event) {
|
||||
UNUSED(context);
|
||||
UNUSED(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_info_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
text_box_reset(app->text_box);
|
||||
furi_string_reset(app->text_box_store);
|
||||
}
|
||||
191
scenes/cogs_mikai_scene_load_file.c
Normal file
191
scenes/cogs_mikai_scene_load_file.c
Normal file
@ -0,0 +1,191 @@
|
||||
#include "../cogs_mikai.h"
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <storage/storage.h>
|
||||
#include <toolbox/path.h>
|
||||
#include <string.h>
|
||||
|
||||
// Manual hex parser (sscanf %X doesn't work reliably on Flipper)
|
||||
static bool parse_hex64(const char* str, uint64_t* value) {
|
||||
if(!str || !value) return false;
|
||||
*value = 0;
|
||||
for(size_t i = 0; str[i] != '\0' && i < 16; i++) {
|
||||
char c = str[i];
|
||||
uint8_t digit;
|
||||
if(c >= '0' && c <= '9') digit = c - '0';
|
||||
else if(c >= 'A' && c <= 'F') digit = c - 'A' + 10;
|
||||
else if(c >= 'a' && c <= 'f') digit = c - 'a' + 10;
|
||||
else break;
|
||||
*value = (*value << 4) | digit;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_hex32(const char* str, uint32_t* value) {
|
||||
if(!str || !value) return false;
|
||||
*value = 0;
|
||||
for(size_t i = 0; str[i] != '\0' && i < 8; i++) {
|
||||
char c = str[i];
|
||||
uint8_t digit;
|
||||
if(c >= '0' && c <= '9') digit = c - '0';
|
||||
else if(c >= 'A' && c <= 'F') digit = c - 'A' + 10;
|
||||
else if(c >= 'a' && c <= 'f') digit = c - 'a' + 10;
|
||||
else break;
|
||||
*value = (*value << 4) | digit;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cogs_mikai_scene_load_file_popup_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_load_file_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
Popup* popup = app->popup;
|
||||
|
||||
// Show file browser starting in the app's data folder
|
||||
FuriString* file_path = furi_string_alloc();
|
||||
furi_string_set(file_path, "/ext/apps_data/cogs_mikai");
|
||||
|
||||
// Ensure directory exists
|
||||
Storage* storage_mkdir = furi_record_open(RECORD_STORAGE);
|
||||
storage_simply_mkdir(storage_mkdir, "/ext/apps_data/cogs_mikai");
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
dialog_file_browser_set_basic_options(&browser_options, ".myk", NULL);
|
||||
browser_options.hide_ext = false;
|
||||
|
||||
if(dialog_file_browser_show(app->dialogs, file_path, file_path, &browser_options)) {
|
||||
// User selected a file
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* file = storage_file_alloc(storage);
|
||||
|
||||
if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
// Read entire file
|
||||
size_t file_size = storage_file_size(file);
|
||||
char* file_buffer = malloc(file_size + 1);
|
||||
bool success = false;
|
||||
|
||||
if(file_buffer) {
|
||||
size_t bytes_read = storage_file_read(file, file_buffer, file_size);
|
||||
file_buffer[bytes_read] = '\0';
|
||||
|
||||
// Parse the file - manual line parsing without strtok
|
||||
if(bytes_read > 14 && strncmp(file_buffer, "COGES_MYKEY_V1", 14) == 0) {
|
||||
char* ptr = file_buffer;
|
||||
char line[128];
|
||||
|
||||
// Helper to read next line
|
||||
auto bool read_line(char** p, char* buf, size_t max_len) {
|
||||
size_t i = 0;
|
||||
while(**p && **p != '\n' && i < max_len - 1) {
|
||||
buf[i++] = *(*p)++;
|
||||
}
|
||||
buf[i] = '\0';
|
||||
if(**p == '\n') (*p)++;
|
||||
return i > 0;
|
||||
};
|
||||
|
||||
// Skip header line
|
||||
read_line(&ptr, line, sizeof(line));
|
||||
|
||||
// Read UID
|
||||
if(read_line(&ptr, line, sizeof(line))) {
|
||||
char* uid_str = strstr(line, "UID: ");
|
||||
if(uid_str && parse_hex64(uid_str + 5, &app->mykey.uid)) {
|
||||
FURI_LOG_I(TAG, "Loaded UID: %016llX", app->mykey.uid);
|
||||
|
||||
// Read encryption key
|
||||
if(read_line(&ptr, line, sizeof(line))) {
|
||||
char* key_str = strstr(line, "ENCRYPTION_KEY: ");
|
||||
if(key_str && parse_hex32(key_str + 16, &app->mykey.encryption_key)) {
|
||||
FURI_LOG_I(TAG, "Loaded key: %08lX", app->mykey.encryption_key);
|
||||
success = true;
|
||||
|
||||
// Read blocks
|
||||
for(size_t i = 0; i < SRIX4K_BLOCKS && success; i++) {
|
||||
if(read_line(&ptr, line, sizeof(line))) {
|
||||
char* block_str = strstr(line, ": ");
|
||||
if(block_str && parse_hex32(block_str + 2, &app->mykey.eeprom[i])) {
|
||||
// Success
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to parse block %zu: %s", i, line);
|
||||
success = false;
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to read line for block %zu", i);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to parse encryption key: %s", line);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Failed to parse UID: %s", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(file_buffer);
|
||||
}
|
||||
|
||||
storage_file_close(file);
|
||||
|
||||
if(success) {
|
||||
app->mykey.is_loaded = true;
|
||||
app->mykey.is_modified = false; // Fresh load from file
|
||||
app->mykey.is_reset = mykey_is_reset(&app->mykey);
|
||||
app->mykey.current_credit = mykey_get_current_credit(&app->mykey);
|
||||
|
||||
popup_set_header(popup, "Success!", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Card loaded from file", 64, 25, AlignCenter, AlignTop);
|
||||
notification_message(app->notifications, &sequence_success);
|
||||
} else {
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Invalid file format", 64, 25, AlignCenter, AlignTop);
|
||||
notification_message(app->notifications, &sequence_error);
|
||||
}
|
||||
} else {
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Failed to open file", 64, 25, AlignCenter, AlignTop);
|
||||
notification_message(app->notifications, &sequence_error);
|
||||
}
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
} else {
|
||||
// User cancelled - search back to start scene and switch (forces menu rebuild)
|
||||
furi_string_free(file_path);
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart);
|
||||
return;
|
||||
}
|
||||
|
||||
furi_string_free(file_path);
|
||||
|
||||
popup_set_callback(popup, cogs_mikai_scene_load_file_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_load_file_on_event(void* context, SceneManagerEvent event) {
|
||||
COGSMyKaiApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
// Popup timeout - search back to start scene and switch (forces menu rebuild)
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_load_file_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
popup_reset(app->popup);
|
||||
}
|
||||
57
scenes/cogs_mikai_scene_read.c
Normal file
57
scenes/cogs_mikai_scene_read.c
Normal file
@ -0,0 +1,57 @@
|
||||
#include "../cogs_mikai.h"
|
||||
|
||||
static void cogs_mikai_scene_read_popup_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_read_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
|
||||
// Show popup for card detection
|
||||
Popup* popup = app->popup;
|
||||
popup_set_header(popup, "Reading Card", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Place COGES MyKey\non Flipper's back", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_icon(popup, 0, 0, NULL);
|
||||
popup_set_callback(popup, cogs_mikai_scene_read_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 3000);
|
||||
popup_enable_timeout(popup);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
|
||||
// Attempt to read NFC card
|
||||
if(mykey_read_from_nfc(app)) {
|
||||
// Calculate encryption key
|
||||
mykey_calculate_encryption_key(&app->mykey);
|
||||
app->mykey.is_loaded = true;
|
||||
app->mykey.is_modified = false; // Fresh read from card
|
||||
app->mykey.is_reset = mykey_is_reset(&app->mykey);
|
||||
app->mykey.current_credit = mykey_get_current_credit(&app->mykey);
|
||||
|
||||
popup_set_header(popup, "Success!", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Card read successfully", 64, 25, AlignCenter, AlignTop);
|
||||
notification_message(app->notifications, &sequence_success);
|
||||
} else {
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Failed to read card\nsegmentaion fault", 64, 25, AlignCenter, AlignTop);
|
||||
notification_message(app->notifications, &sequence_error);
|
||||
}
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_read_on_event(void* context, SceneManagerEvent event) {
|
||||
COGSMyKaiApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
consumed = true;
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_read_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
popup_reset(app->popup);
|
||||
}
|
||||
57
scenes/cogs_mikai_scene_reset.c
Normal file
57
scenes/cogs_mikai_scene_reset.c
Normal file
@ -0,0 +1,57 @@
|
||||
#include "../cogs_mikai.h"
|
||||
|
||||
static void cogs_mikai_scene_reset_popup_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_reset_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
Popup* popup = app->popup;
|
||||
|
||||
if(!app->mykey.is_loaded) {
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "No card loaded\nRead a card first", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_reset_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the card
|
||||
mykey_reset(&app->mykey);
|
||||
|
||||
// Update cached values
|
||||
app->mykey.is_reset = mykey_is_reset(&app->mykey);
|
||||
app->mykey.current_credit = mykey_get_current_credit(&app->mykey);
|
||||
|
||||
// Show confirmation - saved in memory, not written to card
|
||||
popup_set_header(popup, "Card Reset!", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Reset in memory\nUse 'Write to Card'", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_reset_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
notification_message(app->notifications, &sequence_success);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_reset_on_event(void* context, SceneManagerEvent event) {
|
||||
COGSMyKaiApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
consumed = true;
|
||||
// Search back to start scene and switch (forces menu rebuild)
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart);
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_reset_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
popup_reset(app->popup);
|
||||
}
|
||||
158
scenes/cogs_mikai_scene_save_file.c
Normal file
158
scenes/cogs_mikai_scene_save_file.c
Normal file
@ -0,0 +1,158 @@
|
||||
#include "../cogs_mikai.h"
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <storage/storage.h>
|
||||
#include <toolbox/path.h>
|
||||
|
||||
enum {
|
||||
SaveFileSceneEventInput,
|
||||
};
|
||||
|
||||
static bool cogs_mikai_scene_save_file_validator(const char* text, FuriString* error, void* context) {
|
||||
UNUSED(context);
|
||||
|
||||
if(strlen(text) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for(size_t i = 0; text[i] != '\0'; i++) {
|
||||
char c = text[i];
|
||||
if(!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-')) {
|
||||
furi_string_set(error, "Only a-z, 0-9, _, -");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cogs_mikai_scene_save_file_text_input_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, SaveFileSceneEventInput);
|
||||
}
|
||||
|
||||
static void cogs_mikai_scene_save_file_popup_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_save_file_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
Popup* popup = app->popup;
|
||||
|
||||
if(!app->mykey.is_loaded) {
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "No card loaded\nRead a card first", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_save_file_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
return;
|
||||
}
|
||||
|
||||
TextInput* text_input = app->text_input;
|
||||
snprintf(app->text_buffer, sizeof(app->text_buffer), "mykey_save");
|
||||
|
||||
text_input_set_header_text(text_input, "Enter filename (.myk)");
|
||||
text_input_set_validator(text_input, cogs_mikai_scene_save_file_validator, NULL);
|
||||
text_input_set_result_callback(
|
||||
text_input,
|
||||
cogs_mikai_scene_save_file_text_input_callback,
|
||||
app,
|
||||
app->text_buffer,
|
||||
sizeof(app->text_buffer),
|
||||
false);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextInput);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_save_file_on_event(void* context, SceneManagerEvent event) {
|
||||
COGSMyKaiApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == SaveFileSceneEventInput) {
|
||||
if(app->text_buffer[0] == '\0') {
|
||||
FURI_LOG_W(TAG, "Ignoring empty text_buffer (already processed)");
|
||||
consumed = true;
|
||||
return consumed;
|
||||
}
|
||||
|
||||
char filename[32];
|
||||
strncpy(filename, app->text_buffer, sizeof(filename) - 1);
|
||||
filename[sizeof(filename) - 1] = '\0';
|
||||
|
||||
text_input_reset(app->text_input);
|
||||
memset(app->text_buffer, 0, sizeof(app->text_buffer));
|
||||
|
||||
FuriString* file_path = furi_string_alloc();
|
||||
furi_string_printf(file_path, "/ext/apps_data/cogs_mikai/%s.myk", filename);
|
||||
|
||||
FURI_LOG_I(TAG, "Attempting to save file: %s", furi_string_get_cstr(file_path));
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
storage_simply_mkdir(storage, "/ext/apps_data/cogs_mikai");
|
||||
|
||||
File* file = storage_file_alloc(storage);
|
||||
bool success = false;
|
||||
|
||||
if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||
const char* header = "COGES_MYKEY_V1\n";
|
||||
storage_file_write(file, header, strlen(header));
|
||||
|
||||
FuriString* line = furi_string_alloc();
|
||||
furi_string_printf(line, "UID: %016llX\n", app->mykey.uid);
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
|
||||
furi_string_printf(line, "ENCRYPTION_KEY: %08lX\n", app->mykey.encryption_key);
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
|
||||
for(size_t i = 0; i < SRIX4K_BLOCKS; i++) {
|
||||
furi_string_printf(line, "BLOCK_%03zu: %08lX\n", i, app->mykey.eeprom[i]);
|
||||
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
|
||||
}
|
||||
|
||||
furi_string_free(line);
|
||||
storage_file_close(file);
|
||||
success = true;
|
||||
}
|
||||
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
Popup* popup = app->popup;
|
||||
if(success) {
|
||||
popup_set_header(popup, "Success!", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "File saved to\napps_data/cogs_mikai/", 64, 25, AlignCenter, AlignTop);
|
||||
notification_message(app->notifications, &sequence_success);
|
||||
FURI_LOG_I(TAG, "File saved: %s", furi_string_get_cstr(file_path));
|
||||
} else {
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Failed to create file", 64, 25, AlignCenter, AlignTop);
|
||||
notification_message(app->notifications, &sequence_error);
|
||||
FURI_LOG_E(TAG, "Failed to save file: %s", furi_string_get_cstr(file_path));
|
||||
}
|
||||
|
||||
furi_string_free(file_path);
|
||||
|
||||
popup_set_callback(popup, cogs_mikai_scene_save_file_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 3000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
|
||||
consumed = true;
|
||||
} else {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_save_file_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
text_input_reset(app->text_input);
|
||||
popup_reset(app->popup);
|
||||
}
|
||||
193
scenes/cogs_mikai_scene_set_credit.c
Normal file
193
scenes/cogs_mikai_scene_set_credit.c
Normal file
@ -0,0 +1,193 @@
|
||||
#include "../cogs_mikai.h"
|
||||
#include <furi_hal_rtc.h>
|
||||
|
||||
enum {
|
||||
SetCreditSceneEventInput,
|
||||
};
|
||||
|
||||
static bool cogs_mikai_scene_set_credit_validator(const char* text, FuriString* error, void* context) {
|
||||
UNUSED(context);
|
||||
|
||||
if(strlen(text) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool has_decimal = false;
|
||||
for(size_t i = 0; text[i] != '\0'; i++) {
|
||||
if(text[i] == '.' || text[i] == ',') {
|
||||
if(has_decimal) {
|
||||
furi_string_set(error, "Only one decimal point");
|
||||
return false;
|
||||
}
|
||||
has_decimal = true;
|
||||
} else if(text[i] < '0' || text[i] > '9') {
|
||||
furi_string_set(error, "Only numbers and '.'");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool parse_euros_to_cents(const char* text, uint16_t* cents) {
|
||||
if(!text || !cents) return false;
|
||||
|
||||
uint32_t integer_part = 0;
|
||||
uint32_t decimal_part = 0;
|
||||
uint32_t decimal_digits = 0;
|
||||
bool in_decimal = false;
|
||||
|
||||
for(size_t i = 0; text[i] != '\0'; i++) {
|
||||
if(text[i] == '.' || text[i] == ',') {
|
||||
in_decimal = true;
|
||||
} else if(text[i] >= '0' && text[i] <= '9') {
|
||||
if(in_decimal) {
|
||||
if(decimal_digits < 2) {
|
||||
decimal_part = decimal_part * 10 + (text[i] - '0');
|
||||
decimal_digits++;
|
||||
}
|
||||
} else {
|
||||
integer_part = integer_part * 10 + (text[i] - '0');
|
||||
}
|
||||
}
|
||||
}
|
||||
if(decimal_digits == 1) {
|
||||
decimal_part *= 10;
|
||||
}
|
||||
|
||||
uint32_t total_cents = integer_part * 100 + decimal_part;
|
||||
if(total_cents > 99999) return false; // Max 999.99 EUR
|
||||
|
||||
*cents = (uint16_t)total_cents;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cogs_mikai_scene_set_credit_text_input_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, SetCreditSceneEventInput);
|
||||
}
|
||||
|
||||
static void cogs_mikai_scene_set_credit_popup_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_set_credit_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
|
||||
if(!app->mykey.is_loaded) {
|
||||
Popup* popup = app->popup;
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "No card loaded\nRead a card first", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_set_credit_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
return;
|
||||
}
|
||||
|
||||
TextInput* text_input = app->text_input;
|
||||
snprintf(app->text_buffer, sizeof(app->text_buffer), "10.00");
|
||||
|
||||
text_input_set_header_text(text_input, "Set Credit (EUR)");
|
||||
text_input_set_validator(text_input, cogs_mikai_scene_set_credit_validator, NULL);
|
||||
text_input_set_result_callback(
|
||||
text_input,
|
||||
cogs_mikai_scene_set_credit_text_input_callback,
|
||||
app,
|
||||
app->text_buffer,
|
||||
sizeof(app->text_buffer),
|
||||
false);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewTextInput);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_set_credit_on_event(void* context, SceneManagerEvent event) {
|
||||
COGSMyKaiApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == SetCreditSceneEventInput) {
|
||||
if(app->text_buffer[0] == '\0') {
|
||||
FURI_LOG_W(TAG, "Ignoring empty text_buffer (already processed)");
|
||||
consumed = true;
|
||||
return consumed;
|
||||
}
|
||||
FURI_LOG_I(TAG, "Set credit input: '%s'", app->text_buffer);
|
||||
uint16_t cents = 0;
|
||||
bool parse_ok = parse_euros_to_cents(app->text_buffer, ¢s);
|
||||
FURI_LOG_I(TAG, "Parse result: %d, cents: %d", parse_ok, cents);
|
||||
|
||||
if(parse_ok) {
|
||||
FURI_LOG_I(TAG, "Valid amount: %d cents", cents);
|
||||
|
||||
DateTime datetime;
|
||||
furi_hal_rtc_get_datetime(&datetime);
|
||||
|
||||
bool success = mykey_set_cents(
|
||||
&app->mykey,
|
||||
cents,
|
||||
datetime.day,
|
||||
datetime.month,
|
||||
datetime.year - 2000);
|
||||
|
||||
if(success) {
|
||||
app->mykey.current_credit = mykey_get_current_credit(&app->mykey);
|
||||
text_input_reset(app->text_input);
|
||||
|
||||
memset(app->text_buffer, 0, sizeof(app->text_buffer));
|
||||
Popup* popup = app->popup;
|
||||
popup_set_header(popup, "Credit Set!", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Saved in memory\nUse 'Write to Card'", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_set_credit_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
notification_message(app->notifications, &sequence_success);
|
||||
} else {
|
||||
Popup* popup = app->popup;
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Failed to set credit", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_set_credit_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
notification_message(app->notifications, &sequence_error);
|
||||
}
|
||||
} else {
|
||||
FURI_LOG_E(TAG, "Invalid amount: parse_ok=%d, cents=%d",
|
||||
parse_ok, cents);
|
||||
Popup* popup = app->popup;
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(
|
||||
popup,
|
||||
"Invalid amount\nEnter 0.00-999.99",
|
||||
64,
|
||||
25,
|
||||
AlignCenter,
|
||||
AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_set_credit_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
}
|
||||
consumed = true;
|
||||
} else {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_set_credit_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
text_input_reset(app->text_input);
|
||||
popup_reset(app->popup);
|
||||
}
|
||||
158
scenes/cogs_mikai_scene_start.c
Normal file
158
scenes/cogs_mikai_scene_start.c
Normal file
@ -0,0 +1,158 @@
|
||||
#include "../cogs_mikai.h"
|
||||
|
||||
typedef enum {
|
||||
SubmenuIndexRead,
|
||||
SubmenuIndexInfo,
|
||||
SubmenuIndexWriteCard,
|
||||
SubmenuIndexAddCredit,
|
||||
SubmenuIndexSetCredit,
|
||||
SubmenuIndexReset,
|
||||
SubmenuIndexSaveFile,
|
||||
SubmenuIndexLoadFile,
|
||||
SubmenuIndexDebug,
|
||||
SubmenuIndexAbout,
|
||||
} SubmenuIndex;
|
||||
|
||||
static void cogs_mikai_scene_start_submenu_callback(void* context, uint32_t index) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_start_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
Submenu* submenu = app->submenu;
|
||||
|
||||
// Always rebuild menu to reflect current state (e.g., is_modified flag)
|
||||
submenu_reset(submenu);
|
||||
|
||||
if(app->mykey.is_loaded) {
|
||||
submenu_set_header(submenu, "[Card Loaded]");
|
||||
}
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"Read Card",
|
||||
SubmenuIndexRead,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"View Info",
|
||||
SubmenuIndexInfo,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
|
||||
// Show "Write to Card" only if data has been modified
|
||||
if(app->mykey.is_modified) {
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
">>> Write to Card <<<",
|
||||
SubmenuIndexWriteCard,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
}
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"Add Credit",
|
||||
SubmenuIndexAddCredit,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"Set Credit",
|
||||
SubmenuIndexSetCredit,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"Reset Card",
|
||||
SubmenuIndexReset,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"Save to File",
|
||||
SubmenuIndexSaveFile,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"Load from File",
|
||||
SubmenuIndexLoadFile,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"Debug Info",
|
||||
SubmenuIndexDebug,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
|
||||
submenu_add_item(
|
||||
submenu,
|
||||
"About",
|
||||
SubmenuIndexAbout,
|
||||
cogs_mikai_scene_start_submenu_callback,
|
||||
app);
|
||||
|
||||
submenu_set_selected_item(
|
||||
submenu, scene_manager_get_scene_state(app->scene_manager, COGSMyKaiSceneStart));
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewSubmenu);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_start_on_event(void* context, SceneManagerEvent event) {
|
||||
COGSMyKaiApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
scene_manager_set_scene_state(app->scene_manager, COGSMyKaiSceneStart, event.event);
|
||||
consumed = true;
|
||||
switch(event.event) {
|
||||
case SubmenuIndexRead:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneRead);
|
||||
break;
|
||||
case SubmenuIndexInfo:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneInfo);
|
||||
break;
|
||||
case SubmenuIndexWriteCard:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneWriteCard);
|
||||
break;
|
||||
case SubmenuIndexAddCredit:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneAddCredit);
|
||||
break;
|
||||
case SubmenuIndexSetCredit:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneSetCredit);
|
||||
break;
|
||||
case SubmenuIndexReset:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneReset);
|
||||
break;
|
||||
case SubmenuIndexSaveFile:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneSaveFile);
|
||||
break;
|
||||
case SubmenuIndexLoadFile:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneLoadFile);
|
||||
break;
|
||||
case SubmenuIndexDebug:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneDebug);
|
||||
break;
|
||||
case SubmenuIndexAbout:
|
||||
scene_manager_next_scene(app->scene_manager, COGSMyKaiSceneAbout);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_start_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
74
scenes/cogs_mikai_scene_write_card.c
Normal file
74
scenes/cogs_mikai_scene_write_card.c
Normal file
@ -0,0 +1,74 @@
|
||||
#include "../cogs_mikai.h"
|
||||
|
||||
static void cogs_mikai_scene_write_card_popup_callback(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_write_card_on_enter(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
Popup* popup = app->popup;
|
||||
|
||||
if(!app->mykey.is_loaded) {
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "No card loaded", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_write_card_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!app->mykey.is_modified) {
|
||||
popup_set_header(popup, "No Changes", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Card data not modified", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_write_card_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
return;
|
||||
}
|
||||
|
||||
popup_set_header(popup, "Writing...", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Place card on reader", 64, 25, AlignCenter, AlignTop);
|
||||
popup_set_callback(popup, cogs_mikai_scene_write_card_popup_callback);
|
||||
popup_set_context(popup, app);
|
||||
popup_set_timeout(popup, 5000);
|
||||
popup_enable_timeout(popup);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, COGSMyKaiViewPopup);
|
||||
|
||||
if(mykey_write_to_nfc(app)) {
|
||||
app->mykey.is_modified = false;
|
||||
|
||||
popup_set_header(popup, "Success!", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Card updated", 64, 25, AlignCenter, AlignTop);
|
||||
notification_message(app->notifications, &sequence_success);
|
||||
} else {
|
||||
popup_set_header(popup, "Error", 64, 10, AlignCenter, AlignTop);
|
||||
popup_set_text(popup, "Write failed\nTry again", 64, 25, AlignCenter, AlignTop);
|
||||
notification_message(app->notifications, &sequence_error);
|
||||
}
|
||||
|
||||
popup_set_timeout(popup, 2000);
|
||||
popup_enable_timeout(popup);
|
||||
}
|
||||
|
||||
bool cogs_mikai_scene_write_card_on_event(void* context, SceneManagerEvent event) {
|
||||
COGSMyKaiApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, COGSMyKaiSceneStart);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void cogs_mikai_scene_write_card_on_exit(void* context) {
|
||||
COGSMyKaiApp* app = context;
|
||||
popup_reset(app->popup);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user