From 6aa67962b66e383774c25d6da00415a7ac60ee43 Mon Sep 17 00:00:00 2001 From: Hoggormino <125792648+Hoggormino@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:28:16 +0200 Subject: [PATCH] NFC scan: describe what the tag data shows, not the vendor The scan only sees NFC data. It cannot sense whether a label's display listens for infrared or radio, so the messages now say what was found instead of naming a vendor or a transport as fact. - "VUSION tag / SES-imagotag uses radio, not IR" becomes "Likely radio tag / Link: nfc.imagotag.com / TagTinker is IR-only". - Match an http:// or https:// link whose host is nfc.imagotag.com (case-insensitive), as prefix code 0x03/0x04 or inline after 0x00, instead of the substring "imagotag" anywhere in the URL. A "://" inside a path, query or fragment is not taken as the scheme. - "Not a Pricer tag" becomes "Unrecognized tag / No ID TagTinker can decode"; "Unsupported chip" becomes "Unreadable chip / Chip answered but no data was read". - Move the decision into tagtinker_nfc_classify() so it can be tested off-device. - De-duplicate on UID and result together. The firmware reports a read that stops partway as a success with fewer pages, so a UID-only check could keep a tag stuck on "Unrecognized tag" after a partial first read. - Announce an unreadable chip once until the field is empty. Those reads carry no UID, so UID de-duplication never applied and the message repeated on every poll. - Bring the prompt back from the scene tick instead of a popup callback. A popup with a callback consumes every short press, so Back only dismissed the message and a second Back was needed to leave. - Stop the scanner on the Back event rather than waiting for on_exit, so the poll loop is already winding down when the scene is popped and the join there is as short as possible. On device, leaving from a message takes exactly one Back. - Move custom event ids to 200+. NfcScanEventSuccess was 100, the same id as the target menu's "+ Type Barcode" item. - Stop the scan LED when "Target list full" ends scanning. - README: add "Which tags work", with vendor-documented radio examples and a table of what each scan message means. Co-Authored-By: Claude Opus 5 --- README.md | 24 ++++ nfc/tagtinker_nfc.c | 193 +++++++++++++++++++++++---- nfc/tagtinker_nfc.h | 44 ++++++- scenes/tagtinker_scene_nfc_scan.c | 211 +++++++++++++++++++++++------- 4 files changed, 397 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index bdfdb77..a8cca2d 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,30 @@ This tool is built for IoT security curiosity, learning about obscure protocols, > [!WARNING] > **Hardware Warning:** Many infrared ESL tags store their firmware, address, and display data in volatile RAM to save cost and energy. If you remove the battery or let it fully discharge, the tag will lose all programming and become unresponsive ("dead"). It usually cannot be recovered without the original base station. +## Which tags work + +TagTinker only transmits infrared, through the Flipper's IR LED. It works with infrared ESLs whose type code is in the app's profile table. The type code is digits 13 to 16 of the 17-character barcode, and the [image preparer](https://i12bp8.github.io/TagTinker/) lists the graphics types. + +TagTinker has no way to drive ESLs that are updated over radio, whatever their barcode or NFC tag says. For example: + +- SES-imagotag's VUSION access points talk to their labels over a [proprietary 2.4 GHz radio](https://www.ses-imagotag.com/wp-content/uploads/2023/01/VUSION_Datasheet_Retail_IoT_Connector_en.pdf), and in 2023 SES-imagotag [announced Bluetooth LE support](https://www.vusion.com/newsroom/ses-imagotag-expands-vusion-capabilities-to-bluetooth-based-iot-protocol) for the platform. +- Hanshow documents labels such as the [Stellar Pro-266](https://www.hanshow.com/en/resource/the-hanshow-esl:-a-stellar-solution-for-retail-transformation) and [Nebular Pro-346](https://www.hanshow.com/en/resource/elevating-the-museum-and-gallery-experience-with-hanshow-price-tags-unveiling-the-nebular-pro-346) as RF devices working at 2402 to 2480 MHz. + +**What `+ Scan NFC` tells you** + +The scan only reads the tag's NFC data. It cannot sense whether the display listens for infrared or radio. + +| Message | What the Flipper found | +| --- | --- | +| Tag actions open | The NFC link carries an ID TagTinker decodes. `Show Tag Info` shows the model, or `Model: Unknown` when the type code is not in the profile table. | +| Likely radio tag | No decodable ID, and the NFC link points to `nfc.imagotag.com`, the host in the [public VUSION label dump](https://github.com/i12bp8/TagTinker/issues/51). The link alone does not prove the model. | +| Unrecognized tag | The chip was read, but its NFC data holds no ID TagTinker can decode. Everyday NFC cards land here too. For an infrared tag, try `+ Type Barcode`. | +| Unreadable chip | An NFC-A chip answered, but no page could be read. The scan reads only NTAG/Ultralight chips; other chip types give this or "Unrecognized tag", depending on how they answer. If the tag moved during the read, take it away and present it again. | +| Target list full | All 16 target slots are in use. Delete a saved tag first. | +| Nothing happens | No NFC-A chip answered. The tag may have no NFC chip, or one of a type the scan does not look for. | + +Only test tags you own or are allowed to test. + ## Features - **TagTinker Flipper App:** High-performance, zero-allocation RLE streaming IR engine. diff --git a/nfc/tagtinker_nfc.c b/nfc/tagtinker_nfc.c index d972a61..6be0fc4 100644 --- a/nfc/tagtinker_nfc.c +++ b/nfc/tagtinker_nfc.c @@ -1,14 +1,16 @@ /* * 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(). + * Supported ESL tags carry an NDEF URI whose last path segment is a + * 10-character id in a custom base64 alphabet. This module walks the tag's TLV + * area, pulls out the URI, and decodes that id into the 17-character barcode + * format expected by tagtinker_barcode_to_plid(). */ #include "tagtinker_nfc.h" #include +#include +#include /* Direct ASCII-to-index lookup table, -1 = not in alphabet */ static const int8_t CHAR_LUT[128] = { @@ -48,8 +50,11 @@ static bool decode_tag(const char* tag, char barcode[18]) { uint32_t val1 = decode_b64(tag + 5, 5); uint32_t val2 = decode_b64(tag, 5); - char raw[20]; + /* Each value is 30 bits, so it can reach 10 digits; give snprintf room for + * both fields plus the terminator instead of letting it truncate. */ + char raw[24]; snprintf(raw, sizeof(raw), "%09lu%09lu", (unsigned long)val1, (unsigned long)val2); + if(strlen(raw) != 18) return false; int lc = (raw[0] - '0') * 10 + (raw[1] - '0'); if(lc > 25) return false; @@ -69,40 +74,170 @@ static bool decode_tag(const char* tag, char barcode[18]) { return (cs % 10) == (barcode[16] - '0'); } -static bool extract_from_pages(const MfUltralightData* mfu, char barcode[18]) { - if(mfu->pages_read < 11) return false; +/* Read one byte of the TLV area, which starts at page 4. Returns false once + * the offset runs past the pages the poller actually managed to read. */ +static bool tlv_byte(const MfUltralightData* mfu, uint32_t offset, uint8_t* out) { + uint32_t page = 4U + (offset / 4U); + if(page >= mfu->pages_read) return false; + *out = mfu->page[page].data[offset % 4U]; + return true; +} - const uint8_t* p3 = mfu->page[3].data; - if(p3[0] != 0xE1) return false; +/* Locate the NDEF TLV (type 0x03), skipping NULL / lock-control / + * memory-control TLVs that factory-formatted NTAG21x tags put in front of it. */ +static bool + find_ndef_tlv(const MfUltralightData* mfu, uint32_t* value_start, uint32_t* value_len) { + uint32_t offset = 0; + uint8_t type = 0; - 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; + while(tlv_byte(mfu, offset, &type)) { + offset++; + if(type == 0x00) continue; /* NULL TLV: no length and no value */ + if(type == 0xFE) return false; /* terminator */ - uint8_t flat[28]; - for(int i = 0; i < 7; i++) { - memcpy(flat + i * 4, mfu->page[4 + i].data, 4); + uint8_t len_byte = 0; + if(!tlv_byte(mfu, offset, &len_byte)) return false; + offset++; + + uint32_t len = len_byte; + if(len_byte == 0xFF) { /* three-byte length form */ + uint8_t hi = 0, lo = 0; + if(!tlv_byte(mfu, offset, &hi) || !tlv_byte(mfu, offset + 1, &lo)) return false; + offset += 2; + len = ((uint32_t)hi << 8) | lo; + } + + if(type == 0x03) { + *value_start = offset; + *value_len = len; + return true; + } + offset += len; + } + return false; +} + +/* Extract the URI body and report the NDEF URI prefix code alongside it. */ +static bool + extract_uri(const MfUltralightData* mfu, char* url, size_t url_size, uint8_t* prefix_code) { + if(!mfu || !url || url_size == 0) return false; + url[0] = '\0'; + + if(mfu->pages_read < 5) return false; + if(mfu->page[3].data[0] != 0xE1) return false; /* NDEF capability container */ + + uint32_t start = 0, value_len = 0; + if(!find_ndef_tlv(mfu, &start, &value_len)) return false; + if(value_len < 4) return false; + + uint8_t header = 0, type_len = 0, payload_len = 0; + uint32_t offset = start; + if(!tlv_byte(mfu, offset++, &header)) return false; + if(!(header & 0x10)) return false; /* short record only */ + if((header & 0x07) != 0x01) return false; /* TNF must be NFC Forum well-known */ + if(!tlv_byte(mfu, offset++, &type_len)) return false; + if(!tlv_byte(mfu, offset++, &payload_len)) return false; + + uint8_t id_len = 0; + if(header & 0x08) { /* IL flag: an ID length byte follows */ + if(!tlv_byte(mfu, offset++, &id_len)) return false; } - int payload_end = 6 + (ndef_len - 4); - if(payload_end > 28) payload_end = 28; + if(type_len != 1) return false; + uint8_t type = 0; + if(!tlv_byte(mfu, offset, &type)) return false; + if(type != 0x55) return false; /* 'U' = URI record */ + offset += type_len; + offset += id_len; - 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]; + if(payload_len < 2) return false; + + /* payload[0] is the URI prefix code ("https://" and friends); the body + * follows it. Copy the body only, bounded by the record length, the + * caller's buffer and the pages we actually read. */ + uint8_t prefix = 0; + if(!tlv_byte(mfu, offset, &prefix)) return false; + if(prefix_code) *prefix_code = prefix; + + size_t out = 0; + for(uint32_t i = 1; i < payload_len; i++) { + uint8_t c = 0; + if(!tlv_byte(mfu, offset + i, &c)) break; + if(c == 0x00 || c == 0xFE) break; + if(out + 1 >= url_size) break; + url[out++] = (char)c; } + url[out] = '\0'; + return out > 0; +} - const char* last_slash = strrchr(url_body, '/'); +bool tagtinker_nfc_extract_url(const MfUltralightData* mfu, char* url, size_t url_size) { + return extract_uri(mfu, url, url_size, NULL); +} + +bool tagtinker_nfc_decode_url(const char* url, char barcode[18]) { + if(!url || !barcode) return false; + barcode[0] = '\0'; + + const char* last_slash = strrchr(url, '/'); 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); +bool tagtinker_nfc_url_host_is(const char* url, const char* host) { + if(!url || !host || !*host) return false; + + /* NDEF URI bodies normally have the scheme stripped into the prefix code, + * but a record written with prefix code 0x00 keeps it inline. A "://" + * after the first '/', '?' or '#' belongs to the path, query or fragment. */ + const char* scheme_end = strstr(url, "://"); + if(scheme_end && scheme_end < url + strcspn(url, "/?#")) url = scheme_end + 3; + + size_t host_len = strlen(host); + if(strncasecmp(url, host, host_len) != 0) return false; + + char next = url[host_len]; + return next == '\0' || next == '/' || next == ':' || next == '?' || next == '#'; +} + +bool tagtinker_nfc_decode_barcode(const MfUltralightData* mfu_data, char barcode[18]) { + if(!mfu_data || !barcode) return false; + barcode[0] = '\0'; + + char url[TAGTINKER_NFC_URL_LEN]; + if(!tagtinker_nfc_extract_url(mfu_data, url, sizeof(url))) return false; + return tagtinker_nfc_decode_url(url, barcode); +} + +TagTinkerNfcResult tagtinker_nfc_classify(const MfUltralightData* mfu_data, char barcode[18]) { + if(barcode) barcode[0] = '\0'; + + /* The firmware poller reports success with zero pages when a chip was + * activated but its first page read failed. Any NFC-A chip that is not an + * Ultralight/NTAG ends up here, as can a tag that moved during the read. */ + if(!mfu_data || mfu_data->pages_read == 0) return TagTinkerNfcResultUnreadable; + + char url[TAGTINKER_NFC_URL_LEN]; + uint8_t prefix = 0; + if(!extract_uri(mfu_data, url, sizeof(url), &prefix)) { + return TagTinkerNfcResultUnrecognized; + } + + if(barcode && tagtinker_nfc_decode_url(url, barcode)) return TagTinkerNfcResultDecoded; + if(barcode) barcode[0] = '\0'; + + /* The one public VUSION label dump (i12bp8/TagTinker#51) carries + * https://nfc.imagotag.com/. Match that host exactly rather than any + * URL that happens to contain the word. Prefix codes 0x01/0x02 add "www." + * to the host, and other codes are not web links, so only accept + * http:// / https:// as prefix code 0x03 / 0x04 or written inline after + * prefix code 0x00. */ + bool inline_web = prefix == 0x00 && (strncasecmp(url, "http://", 7) == 0 || + strncasecmp(url, "https://", 8) == 0); + bool web_link = prefix == 0x03 || prefix == 0x04 || inline_web; + if(web_link && tagtinker_nfc_url_host_is(url, "nfc.imagotag.com")) { + return TagTinkerNfcResultImagotagLink; + } + + return TagTinkerNfcResultUnrecognized; } diff --git a/nfc/tagtinker_nfc.h b/nfc/tagtinker_nfc.h index 51ae045..1531355 100644 --- a/nfc/tagtinker_nfc.h +++ b/nfc/tagtinker_nfc.h @@ -1,14 +1,54 @@ /* * TagTinker — ESL NFC tag decoder * - * Decodes NDEF URI from ESL Mifare Ultralight tags - * into the 17-character barcode format used by TagTinker. + * Reads the NDEF URI from a Mifare Ultralight / NTAG tag and decodes the + * ESL id in its last path segment into the 17-character barcode format used + * by TagTinker. */ #pragma once #include #include +#include #include +/* Longest NDEF URI body (scheme prefix excluded) we keep. */ +#define TAGTINKER_NFC_URL_LEN 96 + +/* Extract the NDEF URI body (without the "https://" style prefix) from a tag. + * Walks the TLV area so lock/memory-control TLVs before the NDEF TLV are + * skipped, and reads as many pages as the record actually spans. */ +bool tagtinker_nfc_extract_url(const MfUltralightData* mfu_data, char* url, size_t url_size); + +/* Decode the ESL id in the URL's last path segment into a barcode. */ +bool tagtinker_nfc_decode_url(const char* url, char barcode[18]); + +/* True when the URL's host is `host` (case-insensitive). The URL may be an + * NDEF URI body without a scheme, or a full URL with one. The caller decides + * whether the stripped NDEF prefix code (for example "https://www.") changes + * the real host. */ +bool tagtinker_nfc_url_host_is(const char* url, const char* host); + +/* Convenience: extract the URL from the tag and decode it in one call. */ bool tagtinker_nfc_decode_barcode(const MfUltralightData* mfu_data, char barcode[18]); + +/* What a completed read contains, from the app's point of view. None of these + * results identifies a vendor or a display technology; they only describe the + * NFC data. */ +typedef enum { + /* A chip was activated but no page could be read: not an Ultralight/NTAG, + * or a tag that moved during the read. */ + TagTinkerNfcResultUnreadable, + /* The NDEF URI carries an id TagTinker decodes; barcode is filled. */ + TagTinkerNfcResultDecoded, + /* No decodable id, and the NDEF URI is an http:// or https:// link whose + * host is nfc.imagotag.com. */ + TagTinkerNfcResultImagotagLink, + /* Readable, but no NDEF URI or no decodable id in it. */ + TagTinkerNfcResultUnrecognized, +} TagTinkerNfcResult; + +/* Classify a completed read. barcode is filled only for ..Decoded and is an + * empty string otherwise. */ +TagTinkerNfcResult tagtinker_nfc_classify(const MfUltralightData* mfu_data, char barcode[18]); diff --git a/scenes/tagtinker_scene_nfc_scan.c b/scenes/tagtinker_scene_nfc_scan.c index 5e6ecb9..112e98f 100644 --- a/scenes/tagtinker_scene_nfc_scan.c +++ b/scenes/tagtinker_scene_nfc_scan.c @@ -1,44 +1,146 @@ /* - * NFC Scan scene — scan an ESL NFC tag to fill barcode + * NFC Scan scene — read an ESL's NFC tag to fill the barcode + * + * The scanner keeps running after a tag is rejected, so an unsupported tag no + * longer ends the session: present another one and it is read straight away. + * Readable tags are de-duplicated by UID, and an unreadable chip is announced + * once, so a tag left resting on the reader does not re-trigger its message + * every poll. + * + * The messages only describe the NFC data. The app cannot sense whether a + * label's display is driven by infrared or radio. */ #include "../tagtinker_app.h" +#include +/* How long a rejection message stays before the prompt returns. */ +#define NFC_SCAN_MESSAGE_MS 2500U + +/* Custom event ids. After Back, an event queued just before it is delivered to + * the target menu, which uses 0..15 for saved targets and 99/100 for its add + * items, so keep these well clear of both. */ enum { - NfcScanEventSuccess = 1, - NfcScanEventNotEsl = 2, + NfcScanEventSuccess = 200, + NfcScanEventUnrecognized = 201, + NfcScanEventImagotagLink = 202, + NfcScanEventUnreadable = 203, }; +/* The prompt comes back on a scene tick, not through the popup callback: a + * popup with a callback consumes every short key press, Back included, so + * one Back would only dismiss the message instead of leaving the scene. */ +static bool nfc_scan_rearm_pending = false; +static uint32_t nfc_scan_rearm_since = 0; + +static void nfc_scan_show_prompt(TagTinkerApp* app) { + nfc_scan_rearm_pending = false; + popup_reset(app->popup); + popup_disable_timeout(app->popup); + popup_set_callback(app->popup, NULL); + 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); +} + +/* Show a transient message. When rearm is set the scanner is still running, so + * the prompt comes back by itself; otherwise the message stays until Back. */ +static void + nfc_scan_show_message(TagTinkerApp* app, const char* header, const char* text, bool rearm) { + popup_reset(app->popup); + popup_set_header(app->popup, header, 64, 20, AlignCenter, AlignCenter); + popup_set_text(app->popup, text, 64, 38, AlignCenter, AlignCenter); + popup_set_callback(app->popup, NULL); + popup_disable_timeout(app->popup); + nfc_scan_rearm_pending = rearm; + nfc_scan_rearm_since = furi_get_tick(); + view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewPopup); +} + static int32_t nfc_scan_thread(void* ctx) { TagTinkerApp* app = ctx; + uint8_t last_uid[10]; + uint8_t last_uid_len = 0; + bool unreadable_announced = false; + TagTinkerNfcResult last_result = TagTinkerNfcResultUnreadable; while(app->nfc_scanning) { MfUltralightData* mfu_data = mf_ultralight_alloc(); - MfUltralightError err = - mf_ultralight_poller_sync_read_card(app->nfc, mfu_data, NULL); + 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); + if(!app->nfc_scanning) { mf_ultralight_free(mfu_data); + break; + } - if(!app->nfc_scanning) return 0; + if(err != MfUltralightErrorNone) { + /* No chip was activated, or the poller failed before reading one. + * Treat the field as empty so the next tag is announced. */ + last_uid_len = 0; + unreadable_announced = false; + mf_ultralight_free(mfu_data); + furi_delay_ms(100); + continue; + } - 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; + char barcode[18]; + TagTinkerNfcResult result = tagtinker_nfc_classify(mfu_data, barcode); + bool repeat = false; + + if(result == TagTinkerNfcResultUnreadable) { + /* An unreadable read carries no UID, so it cannot be de-duplicated + * by UID. Announce it once until the field is empty again. */ + repeat = unreadable_announced; + unreadable_announced = true; + } else { + uint8_t uid_len = mfu_data->iso14443_3a_data->uid_len; + if(uid_len > sizeof(last_uid)) uid_len = sizeof(last_uid); + const uint8_t* uid = mfu_data->iso14443_3a_data->uid; + + /* Compare the result too: a read that stopped partway comes back + * as a success with fewer pages, and a later full read of the + * same tag must still be able to decode it. */ + repeat = uid_len > 0 && uid_len == last_uid_len && + memcmp(uid, last_uid, uid_len) == 0 && result == last_result; + memcpy(last_uid, uid, uid_len); + last_uid_len = uid_len; + last_result = result; } mf_ultralight_free(mfu_data); + if(repeat) { + /* Same tag still resting on the reader; do not announce it again. */ + furi_delay_ms(100); + continue; + } + + uint32_t event = NfcScanEventUnrecognized; + switch(result) { + case TagTinkerNfcResultDecoded: + memcpy(app->barcode, barcode, TAGTINKER_BC_LEN); + app->barcode[TAGTINKER_BC_LEN] = '\0'; + event = NfcScanEventSuccess; + break; + case TagTinkerNfcResultImagotagLink: + event = NfcScanEventImagotagLink; + break; + case TagTinkerNfcResultUnreadable: + event = NfcScanEventUnreadable; + break; + case TagTinkerNfcResultUnrecognized: + event = NfcScanEventUnrecognized; + break; + } + if(!app->nfc_scanning) break; + view_dispatcher_send_custom_event(app->view_dispatcher, event); + + /* On success the scene moves on; stop polling so the field is not left + * running underneath the next scene. */ + if(event == NfcScanEventSuccess) return 0; + furi_delay_ms(100); } @@ -48,12 +150,7 @@ static int32_t nfc_scan_thread(void* ctx) { 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); + nfc_scan_show_prompt(app); notification_message(app->notifications, &sequence_blink_start_cyan); @@ -68,21 +165,36 @@ void tagtinker_scene_nfc_scan_on_enter(void* ctx) { bool tagtinker_scene_nfc_scan_on_event(void* ctx, SceneManagerEvent event) { TagTinkerApp* app = ctx; + if(event.type == SceneManagerEventTypeBack) { + /* Stop the scanner as soon as Back is pressed rather than waiting for + * on_exit, so the poll loop is already winding down when the scene is + * popped and the join there is as short as possible. Returning false + * lets the scene manager pop exactly one scene, as before. */ + app->nfc_scanning = false; + nfc_scan_rearm_pending = false; + return false; + } + + if(event.type == SceneManagerEventTypeTick) { + if(nfc_scan_rearm_pending && + furi_get_tick() - nfc_scan_rearm_since >= furi_ms_to_ticks(NFC_SCAN_MESSAGE_MS)) { + nfc_scan_show_prompt(app); + } + return false; + } + 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); + /* The barcode came out of the decoder, so it always parses; a + * failure here means there is no free target slot left. The scan + * thread has already stopped, so stop the scan LED too. */ + notification_message(app->notifications, &sequence_blink_stop); + nfc_scan_show_message( + app, "Target list full", "Delete a target\nand scan again", false); return true; } @@ -102,16 +214,24 @@ bool tagtinker_scene_nfc_scan_on_event(void* ctx, SceneManagerEvent event) { 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); + if(event.event == NfcScanEventImagotagLink) { + /* The link points at nfc.imagotag.com, the host in the public VUSION + * label dump (i12bp8/TagTinker#51). SES-imagotag's VUSION access point + * datasheet documents a proprietary 2.4 GHz label radio, and this app + * only transmits infrared. The link alone does not prove the model or + * its radio, hence "Likely". */ + nfc_scan_show_message( + app, "Likely radio tag", "Link: nfc.imagotag.com\nTagTinker is IR-only", true); + return true; + } + + if(event.event == NfcScanEventUnrecognized) { + nfc_scan_show_message(app, "Unrecognized tag", "No ID TagTinker\ncan decode", true); + return true; + } + + if(event.event == NfcScanEventUnreadable) { + nfc_scan_show_message(app, "Unreadable chip", "Chip answered but\nno data was read", true); return true; } @@ -129,6 +249,9 @@ void tagtinker_scene_nfc_scan_on_exit(void* ctx) { app->nfc = NULL; } + nfc_scan_rearm_pending = false; notification_message(app->notifications, &sequence_blink_stop); popup_reset(app->popup); + popup_disable_timeout(app->popup); + popup_set_callback(app->popup, NULL); }