Add multi-tech HF card selection

This commit is contained in:
CinderSocket
2026-03-08 17:19:04 -07:00
parent 34e08a75bf
commit f7d1d0a95c
8 changed files with 243 additions and 58 deletions
+1
View File
@@ -3,6 +3,7 @@ ADD_SCENE(seader, sam_present, SamPresent)
ADD_SCENE(seader, sam_missing, SamMissing)
ADD_SCENE(seader, sam_wrong, SamWrong)
ADD_SCENE(seader, read, Read)
ADD_SCENE(seader, read_card_type, ReadCardType)
ADD_SCENE(seader, read_config_card, ReadConfigCard)
ADD_SCENE(seader, read_card_success, ReadCardSuccess)
ADD_SCENE(seader, read_config_card_success, ReadConfigCardSuccess)
+10
View File
@@ -16,6 +16,10 @@ void seader_scene_read_on_enter(void* context) {
seader_scene_read_prepare(seader);
seader_credential_clear(seader->credential);
if(seader->selected_read_type == SeaderCredentialTypeNone) {
seader->detected_card_type_count = 0;
memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types));
}
seader_worker_start(
seader->worker,
SeaderWorkerStateReading,
@@ -43,8 +47,14 @@ bool seader_scene_read_on_event(void* context, SceneManagerEvent event) {
event.event == SeaderWorkerEventSuccess) {
scene_manager_next_scene(seader->scene_manager, SeaderSceneReadCardSuccess);
consumed = true;
} else if(event.event == SeaderWorkerEventSelectCardType) {
scene_manager_next_scene(seader->scene_manager, SeaderSceneReadCardType);
consumed = true;
}
} else if(event.type == SceneManagerEventTypeBack) {
seader->selected_read_type = SeaderCredentialTypeNone;
seader->detected_card_type_count = 0;
memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types));
scene_manager_search_and_switch_to_previous_scene(
seader->scene_manager, SeaderSceneSamPresent);
consumed = true;
+3
View File
@@ -20,6 +20,9 @@ void seader_scene_read_card_success_on_enter(void* context) {
SeaderCredential* credential = seader->credential;
PluginWiegand* plugin = seader->plugin_wiegand;
Widget* widget = seader->widget;
seader->selected_read_type = SeaderCredentialTypeNone;
seader->detected_card_type_count = 0;
memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types));
// Use reusable strings instead of allocating new ones
FuriString* type_str = seader->temp_string1;
+68
View File
@@ -0,0 +1,68 @@
#include "../seader_i.h"
static const char* seader_scene_read_card_type_label(SeaderCredentialType type) {
switch(type) {
case SeaderCredentialType14A:
return "Read as 14443A";
case SeaderCredentialTypeMifareClassic:
return "Read as Mifare Classic";
case SeaderCredentialTypePicopass:
return "Read as Picopass";
default:
return "Read";
}
}
void seader_scene_read_card_type_submenu_callback(void* context, uint32_t index) {
Seader* seader = context;
view_dispatcher_send_custom_event(seader->view_dispatcher, index);
}
void seader_scene_read_card_type_on_enter(void* context) {
Seader* seader = context;
Submenu* submenu = seader->submenu;
submenu_reset(submenu);
for(size_t i = 0; i < seader->detected_card_type_count; i++) {
const SeaderCredentialType type = seader->detected_card_types[i];
submenu_add_item(
submenu,
seader_scene_read_card_type_label(type),
type,
seader_scene_read_card_type_submenu_callback,
seader);
}
view_dispatcher_switch_to_view(seader->view_dispatcher, SeaderViewMenu);
}
bool seader_scene_read_card_type_on_event(void* context, SceneManagerEvent event) {
Seader* seader = context;
bool consumed = false;
if(event.type == SceneManagerEventTypeCustom) {
const SeaderCredentialType type = event.event;
if(type == SeaderCredentialType14A || type == SeaderCredentialTypeMifareClassic ||
type == SeaderCredentialTypePicopass) {
seader->selected_read_type = type;
seader->detected_card_type_count = 0;
memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types));
scene_manager_next_scene(seader->scene_manager, SeaderSceneRead);
consumed = true;
}
} else if(event.type == SceneManagerEventTypeBack) {
seader->selected_read_type = SeaderCredentialTypeNone;
seader->detected_card_type_count = 0;
memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types));
scene_manager_search_and_switch_to_previous_scene(
seader->scene_manager, SeaderSceneSamPresent);
consumed = true;
}
return consumed;
}
void seader_scene_read_card_type_on_exit(void* context) {
Seader* seader = context;
submenu_reset(seader->submenu);
}
+3
View File
@@ -33,6 +33,9 @@ Seader* seader_alloc() {
seader->samCommand = SamCommand_PR_NOTHING;
seader->sam_state = SeaderSamStateIdle;
seader->sam_intent = SeaderSamIntentNone;
memset(seader->detected_card_types, 0, sizeof(seader->detected_card_types));
seader->detected_card_type_count = 0;
seader->selected_read_type = SeaderCredentialTypeNone;
seader->worker = seader_worker_alloc();
seader->view_dispatcher = view_dispatcher_alloc();
+7 -3
View File
@@ -62,9 +62,10 @@
WorkerEvtCtrlLineSet | WorkerEvtSamTxComplete)
#define WORKER_ALL_TX_EVENTS (WorkerEvtTxStop | WorkerEvtSamRx)
#define SEADER_TEXT_STORE_SIZE 128
#define SEADER_MAX_ATR_SIZE 33
#define MAX_FRAME_HEADERS 32
#define SEADER_TEXT_STORE_SIZE 128
#define SEADER_MAX_ATR_SIZE 33
#define MAX_FRAME_HEADERS 32
#define SEADER_MAX_DETECTED_CARD_TYPES 3
enum SeaderCustomEvent {
// Reserve first 100 events for button types and indexes, starting from 0
@@ -154,6 +155,9 @@ struct Seader {
PicopassPoller* picopass_poller;
NfcDevice* nfc_device;
SeaderCredentialType detected_card_types[SEADER_MAX_DETECTED_CARD_TYPES];
size_t detected_card_type_count;
SeaderCredentialType selected_read_type;
PluginManager* plugin_manager;
PluginWiegand* plugin_wiegand;
+150 -55
View File
@@ -19,6 +19,135 @@ void seader_send_card_detected(SeaderUartBridge* seader_uart, CardDetails_t* car
void seader_worker_reading(Seader* seader);
void seader_worker_poller_conversation(Seader* seader, SeaderPollerContainer* spc);
typedef struct {
bool done;
bool detected;
} SeaderPicopassDetectContext;
static NfcCommand
seader_worker_picopass_detect_callback(PicopassPollerEvent event, void* context) {
SeaderPicopassDetectContext* detect_context = context;
if(event.type == PicopassPollerEventTypeCardDetected ||
event.type == PicopassPollerEventTypeSuccess) {
detect_context->detected = true;
detect_context->done = true;
return NfcCommandStop;
} else if(event.type == PicopassPollerEventTypeFail) {
detect_context->done = true;
return NfcCommandStop;
}
return NfcCommandContinue;
}
static bool seader_worker_detect_picopass(Nfc* nfc) {
bool detected = false;
PicopassPoller* poller = picopass_poller_alloc(nfc);
SeaderPicopassDetectContext detect_context = {0};
picopass_poller_start(poller, seader_worker_picopass_detect_callback, &detect_context);
for(uint8_t i = 0; i < 10 && !detect_context.done; i++) {
furi_delay_ms(10);
}
detected = detect_context.detected;
picopass_poller_stop(poller);
picopass_poller_free(poller);
return detected;
}
static void seader_worker_add_detected_type(
SeaderCredentialType* detected_types,
size_t* detected_type_count,
SeaderCredentialType type) {
for(size_t i = 0; i < *detected_type_count; i++) {
if(detected_types[i] == type) {
return;
}
}
if(*detected_type_count < SEADER_MAX_DETECTED_CARD_TYPES) {
detected_types[*detected_type_count] = type;
(*detected_type_count)++;
}
}
static size_t seader_worker_detect_supported_types(
Seader* seader,
SeaderCredentialType* detected_types,
size_t detected_capacity) {
UNUSED(detected_capacity);
size_t detected_type_count = 0;
NfcPoller* poller_detect = nfc_poller_alloc(seader->nfc, NfcProtocolIso14443_4a);
if(nfc_poller_detect(poller_detect)) {
seader_worker_add_detected_type(
detected_types, &detected_type_count, SeaderCredentialType14A);
}
nfc_poller_free(poller_detect);
poller_detect = nfc_poller_alloc(seader->nfc, NfcProtocolMfClassic);
if(nfc_poller_detect(poller_detect)) {
seader_worker_add_detected_type(
detected_types, &detected_type_count, SeaderCredentialTypeMifareClassic);
}
nfc_poller_free(poller_detect);
if(seader_worker_detect_picopass(seader->nfc)) {
seader_worker_add_detected_type(
detected_types, &detected_type_count, SeaderCredentialTypePicopass);
}
return detected_type_count;
}
static bool seader_worker_start_read_for_type(Seader* seader, SeaderCredentialType type) {
NfcPoller* poller_detect = NULL;
if(type == SeaderCredentialType14A) {
poller_detect = nfc_poller_alloc(seader->nfc, NfcProtocolIso14443_4a);
if(!nfc_poller_detect(poller_detect)) {
nfc_poller_free(poller_detect);
return false;
}
FURI_LOG_I(TAG, "Detected ISO14443-4A card");
nfc_poller_free(poller_detect);
seader->poller = nfc_poller_alloc(seader->nfc, NfcProtocolIso14443_4a);
seader->worker->stage = SeaderPollerEventTypeCardDetect;
seader->credential->type = SeaderCredentialType14A;
nfc_poller_start(seader->poller, seader_worker_poller_callback_iso14443_4a, seader);
return true;
} else if(type == SeaderCredentialTypeMifareClassic) {
poller_detect = nfc_poller_alloc(seader->nfc, NfcProtocolMfClassic);
if(!nfc_poller_detect(poller_detect)) {
nfc_poller_free(poller_detect);
return false;
}
FURI_LOG_I(TAG, "Detected Mifare Classic card");
nfc_poller_free(poller_detect);
seader->poller = nfc_poller_alloc(seader->nfc, NfcProtocolMfClassic);
seader->worker->stage = SeaderPollerEventTypeCardDetect;
seader->credential->type = SeaderCredentialTypeMifareClassic;
nfc_poller_start(seader->poller, seader_worker_poller_callback_mfc, seader);
return true;
} else if(type == SeaderCredentialTypePicopass) {
if(!seader_worker_detect_picopass(seader->nfc)) {
return false;
}
FURI_LOG_I(TAG, "Detected Picopass card");
seader->picopass_poller = picopass_poller_alloc(seader->nfc);
seader->worker->stage = SeaderPollerEventTypeCardDetect;
seader->credential->type = SeaderCredentialTypePicopass;
picopass_poller_start(
seader->picopass_poller, seader_worker_poller_callback_picopass, seader);
return true;
}
return false;
}
/***************************** Seader Worker API *******************************/
SeaderWorker* seader_worker_alloc() {
@@ -307,67 +436,33 @@ void seader_worker_reading(Seader* seader) {
while(seader_worker->state == SeaderWorkerStateReading) {
bool detected = false;
SeaderPollerEventType result_stage = SeaderPollerEventTypeFail;
SeaderCredentialType type_to_read = seader->selected_read_type;
// 1. HF Discovery (ISO14443-4A)
NfcPoller* poller_detect = nfc_poller_alloc(seader->nfc, NfcProtocolIso14443_4a);
if(nfc_poller_detect(poller_detect)) {
FURI_LOG_I(TAG, "Detected ISO14443-4A card");
nfc_poller_free(poller_detect);
poller_detect = NULL;
if(type_to_read == SeaderCredentialTypeNone) {
SeaderCredentialType detected_types[SEADER_MAX_DETECTED_CARD_TYPES] = {0};
const size_t detected_type_count = seader_worker_detect_supported_types(
seader, detected_types, COUNT_OF(detected_types));
seader->poller = nfc_poller_alloc(seader->nfc, NfcProtocolIso14443_4a);
seader_worker->stage = SeaderPollerEventTypeCardDetect;
seader->credential->type = SeaderCredentialType14A;
nfc_poller_start(seader->poller, seader_worker_poller_callback_iso14443_4a, seader);
detected = true;
} else {
nfc_poller_free(poller_detect);
poller_detect = NULL;
}
// 2. HF Discovery (Mifare Classic)
if(!detected) {
poller_detect = nfc_poller_alloc(seader->nfc, NfcProtocolMfClassic);
if(nfc_poller_detect(poller_detect)) {
FURI_LOG_I(TAG, "Detected Mifare Classic card");
nfc_poller_free(poller_detect);
poller_detect = NULL;
seader->poller = nfc_poller_alloc(seader->nfc, NfcProtocolMfClassic);
seader_worker->stage = SeaderPollerEventTypeCardDetect;
seader->credential->type = SeaderCredentialTypeMifareClassic;
nfc_poller_start(seader->poller, seader_worker_poller_callback_mfc, seader);
detected = true;
} else {
nfc_poller_free(poller_detect);
poller_detect = NULL;
}
}
// 3. HF Discovery (Picopass/iCLASS)
if(!detected) {
seader->picopass_poller = picopass_poller_alloc(seader->nfc);
seader_worker->stage = SeaderPollerEventTypeCardDetect;
seader->credential->type = SeaderCredentialTypePicopass;
picopass_poller_start(
seader->picopass_poller, seader_worker_poller_callback_picopass, seader);
// Wait up to 100ms for picopass detection
for(int i = 0; i < 10; i++) {
if(seader_worker->stage != SeaderPollerEventTypeCardDetect) {
detected = true;
break;
if(detected_type_count > 1) {
memcpy(
seader->detected_card_types,
detected_types,
sizeof(seader->detected_card_types));
seader->detected_card_type_count = detected_type_count;
if(seader_worker->callback) {
seader_worker->callback(
SeaderWorkerEventSelectCardType, seader_worker->context);
}
furi_delay_ms(10);
}
if(!detected) {
picopass_poller_stop(seader->picopass_poller);
picopass_poller_free(seader->picopass_poller);
seader->picopass_poller = NULL;
break;
} else if(detected_type_count == 1) {
type_to_read = detected_types[0];
}
}
if(type_to_read != SeaderCredentialTypeNone) {
detected = seader_worker_start_read_for_type(seader, type_to_read);
}
if(detected) {
// Wait for conversation to finish
while(seader_worker->stage != SeaderPollerEventTypeComplete &&
+1
View File
@@ -38,6 +38,7 @@ typedef enum {
SeaderWorkerEventSamMissing,
SeaderWorkerEventNoCardDetected,
SeaderWorkerEventStartReading,
SeaderWorkerEventSelectCardType,
SeaderWorkerEventAPDURunnerUpdate,
SeaderWorkerEventAPDURunnerSuccess,
SeaderWorkerEventAPDURunnerError,