diff --git a/README.md b/README.md
index 9bacc96..1d529bb 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# TagTinker V2.0
+# TagTinker V2.1
Infrared ESL Research Toolkit for Flipper Zero
@@ -29,6 +29,7 @@ This tool is built for IoT security curiosity, learning about obscure protocols,
- **TagTinker Flipper App:** High-performance, zero-allocation RLE streaming IR engine.
- **TagTinker Android Companion:** Edit and dither images directly on your phone and sync them instantly to the Flipper Zero over BLE.
+- **NFC Tag Scan:** Instantly identify ESL targets by scanning their NFC tag — no manual barcode entry needed.
- Display text, custom images, and test-patterns.
- Support for monochrome and accent-color (red/yellow) graphics tags.
@@ -61,6 +62,8 @@ To understand the underlying protocol, signal structure, and history, please rea
- **Furrtek’s ESL research:** [https://www.furrtek.org/?a=esl](https://www.furrtek.org/?a=esl)
- **PrecIR reference implementation:** [https://github.com/furrtek/PrecIR](https://github.com/furrtek/PrecIR)
+NFC tag decoding contributed by **7h30th3r0n3**.
+
## Disclaimer
> [!CAUTION]
diff --git a/application.fam b/application.fam
index 5f73695..53a1021 100644
--- a/application.fam
+++ b/application.fam
@@ -3,13 +3,13 @@ App(
name="TagTinker",
apptype=FlipperAppType.EXTERNAL,
entry_point="tagtinker_app_main",
- requires=["gui", "notification", "dialogs", "storage", "bt"],
+ requires=["gui", "notification", "dialogs", "storage", "bt", "nfc"],
stack_size=12 * 1024,
fap_icon="tagtinker_10px.png",
fap_category="Infrared",
fap_author="i12bp8",
fap_description="Educational ESL study tool for owned hardware",
- fap_version="2.0",
+ fap_version="2.1",
sources=[
"tagtinker_app.c",
"ir/tagtinker_ir.c",
@@ -32,5 +32,7 @@ App(
"scenes/tagtinker_scene_text_box.c",
"scenes/tagtinker_scene_warning.c",
"views/numlock_input.c",
+ "nfc/tagtinker_nfc.c",
+ "scenes/tagtinker_scene_nfc_scan.c",
],
)
diff --git a/nfc/tagtinker_nfc.c b/nfc/tagtinker_nfc.c
new file mode 100644
index 0000000..d972a61
--- /dev/null
+++ b/nfc/tagtinker_nfc.c
@@ -0,0 +1,108 @@
+/*
+ * TagTinker — ESL NFC tag decoder (implementation)
+ *
+ * ESL tags contain an NDEF URI whose last 10 characters
+ * encode the ESL ID using a custom base64 alphabet.
+ * This module decodes them into the 17-char barcode format
+ * expected by tagtinker_barcode_to_plid().
+ */
+
+#include "tagtinker_nfc.h"
+#include
+
+/* Direct ASCII-to-index lookup table, -1 = not in alphabet */
+static const int8_t CHAR_LUT[128] = {
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,35,-1,-1,
+ 28,24,30,38,58, 5,23, 6, 3,40,-1,-1,-1,-1,-1,-1,
+ -1,20,15,54,16,44,46,63, 4,48,34,19,37, 0,26, 1,
+ 8,41,31, 2,45,55,60,12,11,57,33,-1,-1,-1,-1,50,
+ -1,13,39, 9,43,18,29,52,59, 7,61,62,14,25,32,56,
+ 42,47,53,22,36,49,10,21,17,27,51,-1,-1,-1,-1,-1,
+};
+
+static int alphabet_index(char c) {
+ uint8_t idx = (uint8_t)c;
+ if(idx >= 128) return -1;
+ return CHAR_LUT[idx];
+}
+
+static uint32_t decode_b64(const char* s, int len) {
+ uint32_t r = 0;
+ for(int i = 0; i < len; i++) {
+ int idx = alphabet_index(s[(len - 1) - i]);
+ if(idx < 0) return 0;
+ r = (r * 64) + (uint32_t)idx;
+ }
+ return r;
+}
+
+static bool decode_tag(const char* tag, char barcode[18]) {
+ if(strlen(tag) != 10) return false;
+
+ for(int i = 0; i < 10; i++) {
+ if(alphabet_index(tag[i]) < 0) return false;
+ }
+
+ uint32_t val1 = decode_b64(tag + 5, 5);
+ uint32_t val2 = decode_b64(tag, 5);
+
+ char raw[20];
+ snprintf(raw, sizeof(raw), "%09lu%09lu", (unsigned long)val1, (unsigned long)val2);
+
+ int lc = (raw[0] - '0') * 10 + (raw[1] - '0');
+ if(lc > 25) return false;
+ char letter = (char)(lc + 65);
+
+ barcode[0] = letter;
+ memcpy(barcode + 1, raw + 2, 16);
+ barcode[17] = '\0';
+
+ if(barcode[1] != '4') return false;
+
+ int cs = 0;
+ for(int i = 0; i < 16; i++) {
+ char c = barcode[i];
+ cs += (c >= 'a' && c <= 'z') ? (c - 32) : c;
+ }
+ return (cs % 10) == (barcode[16] - '0');
+}
+
+static bool extract_from_pages(const MfUltralightData* mfu, char barcode[18]) {
+ if(mfu->pages_read < 11) return false;
+
+ const uint8_t* p3 = mfu->page[3].data;
+ if(p3[0] != 0xE1) return false;
+
+ const uint8_t* p4 = mfu->page[4].data;
+ if(p4[0] != 0x03) return false;
+ uint8_t ndef_len = p4[1];
+ if(ndef_len < 5) return false;
+
+ uint8_t flat[28];
+ for(int i = 0; i < 7; i++) {
+ memcpy(flat + i * 4, mfu->page[4 + i].data, 4);
+ }
+
+ int payload_end = 6 + (ndef_len - 4);
+ if(payload_end > 28) payload_end = 28;
+
+ char url_body[40] = {0};
+ int j = 0;
+ for(int i = 6; i < payload_end && j < 39; i++) {
+ if(flat[i] == 0xFE) break;
+ url_body[j++] = (char)flat[i];
+ }
+
+ const char* last_slash = strrchr(url_body, '/');
+ if(!last_slash) return false;
+
+ return decode_tag(last_slash + 1, barcode);
+}
+
+bool tagtinker_nfc_decode_barcode(const MfUltralightData* mfu_data, char barcode[18]) {
+ if(!mfu_data) return false;
+ barcode[0] = '\0';
+ return extract_from_pages(mfu_data, barcode);
+}
diff --git a/nfc/tagtinker_nfc.h b/nfc/tagtinker_nfc.h
new file mode 100644
index 0000000..51ae045
--- /dev/null
+++ b/nfc/tagtinker_nfc.h
@@ -0,0 +1,14 @@
+/*
+ * TagTinker — ESL NFC tag decoder
+ *
+ * Decodes NDEF URI from ESL Mifare Ultralight tags
+ * into the 17-character barcode format used by TagTinker.
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+bool tagtinker_nfc_decode_barcode(const MfUltralightData* mfu_data, char barcode[18]);
diff --git a/scenes/tagtinker_scene.c b/scenes/tagtinker_scene.c
index 5c86df3..a13bdf1 100644
--- a/scenes/tagtinker_scene.c
+++ b/scenes/tagtinker_scene.c
@@ -21,6 +21,7 @@ void(*const tagtinker_scene_on_enter_handlers[])(void*) = {
tagtinker_scene_transmit_on_enter,
tagtinker_scene_about_on_enter,
tagtinker_scene_text_box_on_enter,
+ tagtinker_scene_nfc_scan_on_enter,
};
bool(*const tagtinker_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
@@ -40,6 +41,7 @@ bool(*const tagtinker_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
tagtinker_scene_transmit_on_event,
tagtinker_scene_about_on_event,
tagtinker_scene_text_box_on_event,
+ tagtinker_scene_nfc_scan_on_event,
};
void(*const tagtinker_scene_on_exit_handlers[])(void*) = {
@@ -59,6 +61,7 @@ void(*const tagtinker_scene_on_exit_handlers[])(void*) = {
tagtinker_scene_transmit_on_exit,
tagtinker_scene_about_on_exit,
tagtinker_scene_text_box_on_exit,
+ tagtinker_scene_nfc_scan_on_exit,
};
const SceneManagerHandlers tagtinker_scene_handlers = {
diff --git a/scenes/tagtinker_scene.h b/scenes/tagtinker_scene.h
index 5e038f9..aa24341 100644
--- a/scenes/tagtinker_scene.h
+++ b/scenes/tagtinker_scene.h
@@ -23,6 +23,7 @@ typedef enum {
TagTinkerSceneTransmit,
TagTinkerSceneAbout,
TagTinkerSceneTextBox,
+ TagTinkerSceneNfcScan,
TagTinkerSceneCount,
} TagTinkerScene;
@@ -89,3 +90,7 @@ void tagtinker_scene_about_on_exit(void* ctx);
void tagtinker_scene_text_box_on_enter(void* ctx);
bool tagtinker_scene_text_box_on_event(void* ctx, SceneManagerEvent event);
void tagtinker_scene_text_box_on_exit(void* ctx);
+
+void tagtinker_scene_nfc_scan_on_enter(void* ctx);
+bool tagtinker_scene_nfc_scan_on_event(void* ctx, SceneManagerEvent event);
+void tagtinker_scene_nfc_scan_on_exit(void* ctx);
diff --git a/scenes/tagtinker_scene_about.c b/scenes/tagtinker_scene_about.c
index 440c0b6..df376c9 100644
--- a/scenes/tagtinker_scene_about.c
+++ b/scenes/tagtinker_scene_about.c
@@ -524,6 +524,7 @@ static void about_draw_cb(Canvas* canvas, void* _model) {
canvas_draw_str_aligned(canvas, 64, 10, AlignCenter, AlignTop, TAGTINKER_DISPLAY_NAME " v" TAGTINKER_VERSION);
canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignTop, "Ported by I12BP8");
canvas_draw_str_aligned(canvas, 64, 34, AlignCenter, AlignTop, "Research by furrtek");
+ canvas_draw_str_aligned(canvas, 64, 44, AlignCenter, AlignTop, "NFC by 7h30th3r0n3");
}
}
diff --git a/scenes/tagtinker_scene_nfc_scan.c b/scenes/tagtinker_scene_nfc_scan.c
new file mode 100644
index 0000000..5e6ecb9
--- /dev/null
+++ b/scenes/tagtinker_scene_nfc_scan.c
@@ -0,0 +1,134 @@
+/*
+ * NFC Scan scene — scan an ESL NFC tag to fill barcode
+ */
+
+#include "../tagtinker_app.h"
+
+enum {
+ NfcScanEventSuccess = 1,
+ NfcScanEventNotEsl = 2,
+};
+
+static int32_t nfc_scan_thread(void* ctx) {
+ TagTinkerApp* app = ctx;
+
+ while(app->nfc_scanning) {
+ MfUltralightData* mfu_data = mf_ultralight_alloc();
+ MfUltralightError err =
+ mf_ultralight_poller_sync_read_card(app->nfc, mfu_data, NULL);
+
+ if(err == MfUltralightErrorNone) {
+ char barcode[18];
+ bool decoded = tagtinker_nfc_decode_barcode(mfu_data, barcode);
+ mf_ultralight_free(mfu_data);
+
+ if(!app->nfc_scanning) return 0;
+
+ if(decoded) {
+ memcpy(app->barcode, barcode, TAGTINKER_BC_LEN);
+ app->barcode[TAGTINKER_BC_LEN] = '\0';
+ view_dispatcher_send_custom_event(
+ app->view_dispatcher, NfcScanEventSuccess);
+ } else {
+ view_dispatcher_send_custom_event(
+ app->view_dispatcher, NfcScanEventNotEsl);
+ }
+ return 0;
+ }
+
+ mf_ultralight_free(mfu_data);
+
+ if(!app->nfc_scanning) break;
+ furi_delay_ms(100);
+ }
+
+ return 0;
+}
+
+void tagtinker_scene_nfc_scan_on_enter(void* ctx) {
+ TagTinkerApp* app = ctx;
+
+ popup_reset(app->popup);
+ popup_set_header(app->popup, "Scan NFC Tag", 64, 10, AlignCenter, AlignTop);
+ popup_set_text(
+ app->popup, "Hold ESL tag\nto Flipper back", 64, 32, AlignCenter, AlignCenter);
+
+ view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewPopup);
+
+ notification_message(app->notifications, &sequence_blink_start_cyan);
+
+ app->nfc = nfc_alloc();
+ app->nfc_scanning = true;
+
+ furi_thread_set_callback(app->nfc_thread, nfc_scan_thread);
+ furi_thread_set_context(app->nfc_thread, app);
+ furi_thread_start(app->nfc_thread);
+}
+
+bool tagtinker_scene_nfc_scan_on_event(void* ctx, SceneManagerEvent event) {
+ TagTinkerApp* app = ctx;
+
+ if(event.type != SceneManagerEventTypeCustom) return false;
+
+ if(event.event == NfcScanEventSuccess) {
+ int8_t idx = tagtinker_ensure_target(app, app->barcode);
+
+ if(idx < 0) {
+ popup_reset(app->popup);
+ popup_set_header(
+ app->popup, "Decode Error", 64, 20, AlignCenter, AlignCenter);
+ popup_set_text(
+ app->popup, "Tag read but\nbarcode invalid", 64, 36, AlignCenter, AlignCenter);
+ popup_set_timeout(app->popup, 2000);
+ popup_enable_timeout(app->popup);
+ popup_set_callback(app->popup, NULL);
+ view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewPopup);
+ return true;
+ }
+
+ tagtinker_select_target(app, (uint8_t)idx);
+
+ FURI_LOG_I(
+ TAGTINKER_TAG,
+ "NFC: %s -> PLID %02X%02X%02X%02X",
+ app->barcode,
+ app->plid[3],
+ app->plid[2],
+ app->plid[1],
+ app->plid[0]);
+
+ notification_message(app->notifications, &sequence_success);
+ scene_manager_next_scene(app->scene_manager, TagTinkerSceneTargetActions);
+ return true;
+ }
+
+ if(event.event == NfcScanEventNotEsl) {
+ popup_reset(app->popup);
+ popup_set_header(
+ app->popup, "Not an ESL tag", 64, 20, AlignCenter, AlignCenter);
+ popup_set_text(
+ app->popup, "Tag detected but\nno valid ESL data", 64, 36, AlignCenter, AlignCenter);
+ popup_set_timeout(app->popup, 2000);
+ popup_enable_timeout(app->popup);
+ popup_set_callback(app->popup, NULL);
+ view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewPopup);
+ return true;
+ }
+
+ return false;
+}
+
+void tagtinker_scene_nfc_scan_on_exit(void* ctx) {
+ TagTinkerApp* app = ctx;
+
+ app->nfc_scanning = false;
+
+ if(app->nfc) {
+ furi_thread_join(app->nfc_thread);
+ nfc_free(app->nfc);
+ app->nfc = NULL;
+ }
+
+ notification_message(app->notifications, &sequence_blink_stop);
+ popup_reset(app->popup);
+}
diff --git a/scenes/tagtinker_scene_target_menu.c b/scenes/tagtinker_scene_target_menu.c
index 017e108..d3df7e5 100644
--- a/scenes/tagtinker_scene_target_menu.c
+++ b/scenes/tagtinker_scene_target_menu.c
@@ -5,6 +5,7 @@
#include "../tagtinker_app.h"
enum {
+ TargetMenuScanNfc = 99,
TargetMenuAddNew = 100,
};
@@ -20,7 +21,8 @@ void tagtinker_scene_target_menu_on_enter(void* ctx) {
submenu_set_header(app->submenu, "Targeted Payloads");
/* Add new target */
- submenu_add_item(app->submenu, "+ New Target", TargetMenuAddNew, target_menu_cb, app);
+ submenu_add_item(app->submenu, "+ Scan NFC", TargetMenuScanNfc, target_menu_cb, app);
+ submenu_add_item(app->submenu, "+ Type Barcode", TargetMenuAddNew, target_menu_cb, app);
/* List saved targets */
for(uint8_t i = 0; i < app->target_count; i++) {
@@ -40,6 +42,11 @@ bool tagtinker_scene_target_menu_on_event(void* ctx, SceneManagerEvent event) {
TagTinkerApp* app = ctx;
if(event.type != SceneManagerEventTypeCustom) return false;
+ if(event.event == TargetMenuScanNfc) {
+ scene_manager_next_scene(app->scene_manager, TagTinkerSceneNfcScan);
+ return true;
+ }
+
if(event.event == TargetMenuAddNew) {
/* Go to barcode input, then come back */
scene_manager_set_scene_state(
diff --git a/tagtinker_app.c b/tagtinker_app.c
index 05d8a37..ecd4a22 100644
--- a/tagtinker_app.c
+++ b/tagtinker_app.c
@@ -740,6 +740,14 @@ static void app_free(TagTinkerApp* app) {
furi_thread_free(app->tx_thread);
+ /* NFC cleanup */
+ if(app->nfc) {
+ app->nfc_scanning = false;
+ furi_thread_join(app->nfc_thread);
+ nfc_free(app->nfc);
+ }
+ furi_thread_free(app->nfc_thread);
+
furi_record_close(RECORD_GUI);
furi_record_close(RECORD_NOTIFICATION);
furi_record_close(RECORD_DIALOGS);
@@ -883,6 +891,13 @@ static TagTinkerApp* app_alloc(void) {
furi_thread_set_priority(app->tx_thread, FuriThreadPriorityHighest);
furi_thread_set_context(app->tx_thread, app);
+ /* NFC scan thread */
+ app->nfc_thread = furi_thread_alloc();
+ furi_thread_set_name(app->nfc_thread, "TagTinkerNfc");
+ furi_thread_set_stack_size(app->nfc_thread, 2048);
+ app->nfc = NULL;
+ app->nfc_scanning = false;
+
return app;
}
diff --git a/tagtinker_app.h b/tagtinker_app.h
index aec30bb..42f2338 100644
--- a/tagtinker_app.h
+++ b/tagtinker_app.h
@@ -21,15 +21,20 @@
#include
#include
+#include
+#include
+#include
+
#include "views/numlock_input.h"
#include "scenes/tagtinker_scene.h"
#include "protocol/tagtinker_proto.h"
#include "ir/tagtinker_ir.h"
+#include "nfc/tagtinker_nfc.h"
#define TAGTINKER_TAG "TagTinker"
#define TAGTINKER_DISPLAY_NAME "TagTinker"
-#define TAGTINKER_VERSION "2.0"
+#define TAGTINKER_VERSION "2.1"
#define TAGTINKER_BC_LEN 17
#define TAGTINKER_HEX_LEN 64
#define TAGTINKER_MAX_TARGETS 16
@@ -127,6 +132,11 @@ struct TagTinkerApp {
bool tx_active;
FuriThread* tx_thread;
+ /* NFC scan state */
+ Nfc* nfc;
+ FuriThread* nfc_thread;
+ volatile bool nfc_scanning;
+
/* Broadcast settings */
uint8_t broadcast_type;
uint8_t page;