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 <noreply@anthropic.com>
A tag whose type code is not in the profile table has no dimensions, so
after the unknown-profile guard its Set Text / Set Image / WiFi Plugins
actions are hidden. This adds a "Set Custom Size" action for those tags:
enter width, height and colour, and the graphics actions become usable.
The IR framing is identical to a table profile; only the screen geometry
differs, so no protocol is guessed. Custom size is applied only when the
barcode's type is not in the table, so a real profile is never overridden.
- New scene (VariableItemList): Width and Height in 8 px steps from 8 to
800, Colour mono/red/yellow, and Save.
- tagtinker_target_refresh_profile() synthesizes a "Custom" profile from
the stored size for unknown-type targets, so Show Tag Info, Set Text
and Set Image all see real dimensions.
- targets.txt gains three optional fields (barcode|name|cw|ch|ccolor);
older two-field lines still load, with no custom size.
- "Set Custom Size" appears in Target Actions only for unknown-type
targets (tagtinker_type_is_known()).
Builds clean on release SDK 1.4.3 (API 87.1) and dev SDK; no new
clang-format drift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cloud_client.h hard-coded the worker URL, and wrangler.toml pointed users
at a "WiFi Setup -> Server URL" field that has never existed, so anyone
self-hosting cloud-plugins/ had to edit a header. cloud_client_set_url()
exists but has no callers, and the UART protocol has no frame for it.
Add CONFIG_TT_CLOUD_URL (Kconfig.projbuild, menu "TagTinker") defaulting
to the current URL, and take TT_CLOUD_DEFAULT_URL from it. Setting it in
sdkconfig.defaults or through menuconfig is now enough; rebuild and
reflash still apply, and there is no hidden run-time channel.
Also says so in wrangler.toml and the README WiFi Plugins bullet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sdkconfig.defaults asked for a USB-Serial-JTAG console, but the ESP32-S2
has no such peripheral: the option depends on SOC_USB_SERIAL_JTAG_SUPPORTED,
which esp32s2 does not define. The choice therefore fell back to
ESP_CONSOLE_UART_DEFAULT, so boot and WiFi logs went out on UART0 at
115200 before wifi_link_init() switched it to 230400. That is the same
wire the framed protocol uses, and a log line printed mid-frame corrupts
it; the FAP then drops the frame on the CRC check.
- Select CONFIG_ESP_CONSOLE_NONE, as the Flipper's own devboard firmware
does. Verified in the generated sdkconfig, not just the defaults file.
- Rename CONFIG_ESP32_WIFI_*_BUFFER_NUM to CONFIG_ESP_WIFI_*, which
ESP-IDF v5.2 warned about on every build.
- wifi_link.c described UART1 on GPIO17/18. The code uses UART0, whose
default pins are the ones wired to the Flipper header.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- README: the IR engine allocates heap for payloads, so drop
"zero-allocation"; the drop folder takes the 1-bit and two-plane BMPs
the image preparer exports, not any BMP, and type 1626 targets cap
files at 24 KB; NFC scan only fills the barcode when the tag's NFC
data decodes; there is no test-pattern feature; the FAQ now says this
app needs a Flipper even though the protocol does not.
- CONTRIBUTING: the Android companion app was removed in d608afb.
- wrangler.toml pointed users at a "WiFi Setup -> Server URL" field that
never existed. Say how the URL is actually set today.
- cloud_client.h described a runtime override through
cloud_client_set_url(), which nothing calls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tx_bmp_open() accepted 24- and 32-bit BMPs of any width, then each source
row was read into uint8_t row_buf[128] on the stack. A 24-bit BMP wider
than 42 px, or a 1-bit BMP wider than 1024 px, overran it. The decoder
also reads one bit per pixel, so 24/32-bit files were misread anyway.
A host build of the previous code under AddressSanitizer reports a
132-byte write into the 128-byte buffer for a 24-bit, 43 px wide file.
- Accept only 1bpp and the stacked-plane "2bpp" layout, reject zero or
over-16-bit sizes, and reject rows wider than TX_BMP_ROW_BUF_SIZE.
- Read header fields as uint32_t. Shifting into bit 31 of an int and
negating INT32_MIN were undefined.
- The result screen showed "Flipped ;)" and played the success sound
even when the send failed or was cancelled. Show "Send failed" with
the error sound, or "Stopped" with no sound when cancelled with Back.
- Check for a stop request after the gap between frames of a sequence.
tagtinker_ir_transmit() clears the stop flag on entry, so a Back
pressed during the gap was lost and the next frame went out in full.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tag whose type code is not in the profile table gets a zeroed profile:
kind Unknown, 0x0, Mono. NFC scan, targets.txt and web_job.txt (and the
unreachable phone-sync code) all create such targets, and because only
Segment was excluded they were offered Set Text, Set Image and WiFi
Plugins. Set Text on a 0x0 job reaches malloc(0), which the firmware
treats as a fatal error.
- Offer Set Text, Set Image and WiFi Plugins only for dot-matrix
profiles found in the table; the app has no display size for others.
- Show Tag Info says the type is not in the profile table and which
actions are hidden, instead of reporting "Size: 0x0" and "Color: Mono".
- Refuse a 0x0 text job before either sized text path allocates.
- Manual barcode entry with all 16 slots used now shows "Target List
Full". It used to open Target Actions with selected_target -1, where
Show Tag Info and LED Test index targets[-1].
- Widget buttons call back on press, short press and release, so the
Back button went back twice. Act on the short press only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Export IDF_TARGET=esp32s2 so ESP-IDF performs the same target consistency
check locally that the CI action gets from its IDF_TARGET env: a stale
sdkconfig generated for another chip is now refused instead of built.
- Print an absolute path in the export.sh failure hint; zsh does not
tilde-expand VAR=~/path, so the old hint produced a literal "~".
- launch depends on build-fap and flash-esp on build-esp, so `make -j`
cannot run two builders in the same tree.
- clean/distclean refuse to run outside the repository root (they delete
cwd-relative paths).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- .github/workflows/build.yml: build the Flipper FAP (ufbt, release + dev
SDK channels), the Cloudflare Worker (tsc + wrangler dry-run bundle) and
the ESP32-S2 firmware (ESP-IDF v5.2.8) on every push to main and every
pull request; syntax-check the inline scripts of the web image-prep page;
run `ufbt lint` as a non-blocking advisory job. No path filters, because
the shared UART protocol header is compiled into both the FAP and the
ESP firmware.
- Makefile: one entry point for developers (setup, build-all, build-fap,
build-worker, build-esp, launch, flash-esp, serve-web, lint, clean).
Works with the stock GNU Make 3.81 on macOS. Auto-detects the ESP-IDF
Python virtualenv when export.sh would otherwise look for one matching
a newer system python3.
- esp32-wifi-fw/sdkconfig.defaults: pin CONFIG_IDF_TARGET="esp32s2" so
clean builds (CI, fresh clones) no longer fall back to plain esp32.
- cloud-plugins/package.json: add the `build` script (wrangler dry-run
bundle) that the Makefile and CI rely on.
- README/CONTRIBUTING: document the make targets and CI; add a build badge.
- .gitignore: ignore .venv/ and the .clang-format generated by ufbt lint.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Default image page is 2 unless the user picked 2-7. Set Text packs planes instead of 45KB buffers. WiFi and odd-size BMPs use glass 296x152 then transpose.
Co-authored-by: Cursor <cursoragent@cursor.com>
These tags look like 296x152 but the firmware only takes 152x296, so landscape BMPs get transposed. Tested on a real 2.6 color tag.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Trading-card layout: accent stripe with github.com/<login> link,
square dithered avatar with frame, faux-bold display name,
flexible bio that wraps cleanly (no mid-word cuts, no ellipsis),
3-column stats panel (STARS / FOLLOWERS / REPOS).
- Bio falls back to a deterministic filler line when the user has
none, so the slot never reads as a render bug.
- Tighter 7-px line height for the bio gets one extra line into
the same band on 208x112.
- Drop heatmap + month axis (cleaner card; halves render time).
- Add month-initial axis under the heatmap
- Add current-streak counter in caption (accent-highlighted)
- Replace stats/bio fight with a single icon stat row (star, followers, repos)
- Fix bullet/middle-dot rendering bug (U+00B7 -> U+2022, in font table)
- Allow display name at scale 2 on wide tags only; safer truncation
- Robust avatar URL ?s= append (handles avatars without query string)
- Tighter geometry tuned for 208x112; still scales to 296x128
Worker (cloud-plugins/):
- New plugin 'GitHub Profile': avatar dithered upper-left, display
name + @handle (with accent star) + word-wrapped bio in the right
column, followers/repos line, and a 53x7 contribution heatmap
with an accent palette (red dots for everyday activity, black for
the rare hottest days). Tuned for 208x112 ESLs and adapts to
296x128 with a bigger avatar and roomier heatmap.
- Identicon plugin gains DiceBear style selector (pixel-art /
bottts / lorelei / adventurer / micah / fun-emoji / shapes) on
top of the original symmetric grid 'local' mode. Both are
rendered through the new shared image_util pipeline.
- Crypto plugin redesigned: editorial layout with corner brackets,
bold price headline, white-knockout delta badge with a real
triangle glyph, currency symbol prefix using the canvas font's
new euro / pound / yen glyphs, and a stippled accent fill under
the sparkline.
Canvas:
- setPixel is now last-write-wins across planes, so drawing black
on top of an accent-filled area produces a clean black pixel
instead of the muddy plane-conflict the e-paper driver was
showing previously. Every pixel is therefore exactly one of
white / black / accent which is what plugin authors intuit.
- New whitePixel + drawTextWhite primitives for knockout text
(e.g. white labels punched through a coloured badge).
- FONT_EXTRA shipped with the canvas for currency / arrow / shape
/ star / bracket glyphs. Plugins can now write '\u20AC1234' and
have the euro symbol Just Work.
Image pipeline (new image_util.ts):
- One fetchImageGray() that sniffs PNG vs JPEG, decodes via
upng-js / jpeg-js, composites alpha over white, and returns a
tightly-packed grayscale buffer.
- One blitGrayDither() that nearest-neighbour scales the gray
buffer to a destination rect and serpentine Floyd-Steinberg
dithers it on the way in. Used by identicon and github.
FAP:
- WiFi BMP writer is now plane-aware: writes the same biPlanes=1,
biBitCount=2, 3-entry palette layout the rest of the IR TX
pipeline already understood for web-image-prep BMPs. Tri-colour
tags now actually receive accent ink from cloud-rendered plugins.
- WiFi run scene un-forces accent: plugins are invoked with the
tag's actual accent capability and the BMP is opened with the
matching plane count.
- Run scene now calls bmp_abort on exit so an aborted transfer
doesn't leak the ~10 KB pixel buffer.
- Plugin manifest cache capped at TT_WIFI_MAX_FAP_PLUGINS=8 (down
from 16) and the UART RX stream buffer shrunk to 8 KB. Together
these cut WiFi-flow heap pressure by ~23 KB and fix the OOM
crashes that occasionally happened when opening WiFi Plugins.
- Broadcast scene auto-transmit fix: enter_callback now filters
by the >> Transmit << row index, and the cursor defaults to
row 0 instead of Transmit. Previously a stale OK key-release
from the prior submenu would land on the Transmit row and
blast the radio without any user action.
Previous version pulled accent ink too aggressively (accentPull 0.85)
so anything red-leaning collapsed to solid red. Real photographic feel
from a 3-ink palette comes from W/B halftones simulating greys with
the chromatic ink only sparsely overlaid - the newsprint illusion.
Three knobs now make this work:
- L_WEIGHT 3.5 (was 1.6): lightness completely dominates the metric
- CHROMA_CAP: source a/b axes pre-scaled to the achievable accent
chroma so the dither isn't asked for more colour than one ink can
deliver. Excess chroma gets dropped via FS error diffusion through
B+W neighbours.
- accentPull 0.06 (was 0.85): only a tiebreaker bonus, used in the
most strongly hue-aligned pixels.
The old quantiser pulled accent ink toward any chromatic pixel using a
flat chroma bonus, which smeared red across shadows and produced flat
red blobs without depth. Two fixes:
- Distance now weights L (lightness) ~1.6x more than chroma, so dark
pixels go to black and light pixels to white before colour competes.
That preserves the photograph's tonal structure.
- Accent attraction is the squared *signed projection* of the pixel's
chroma onto the accent direction. Red ink only pulls red-leaning
pixels (positive a*); blue/green never picks red. The squaring gives
a smooth ramp, so faint reds get sparse red dither and saturated
reds get dense red - matching how a real risograph layers a single
chromatic ink under W/B halftones.
Also flipped Floyd-Steinberg to a serpentine scan with mirrored kernel
on reverse rows, killing the diagonal worm artefacts that gave smooth
gradients an obviously-dithered look.
- Flipper FAP: WiFi Setup, Plugins picker, plugin-run scene wired into
Targeted Payloads -> <tag> -> WiFi Plugins
- Framed UART link to the dev board with bulk-read RX path and 16 KB
stream buffer to keep up with full plugin bursts
- ESP firmware: 900 KB UART<->HTTPS bridge, USB-Serial-JTAG console,
cert-bundle TLS, 16 KB worker stack to host mbedTLS handshake
- Cloudflare Worker: Crypto, Weather, Identicon plugins out of the
box; new plugins are a single TypeScript file, no Flipper reflash
- Shared protocol header + 1bpp BMP writer that matches web-image-prep
palette/stride conventions
- Add web-image-prep/: self-contained HTML5 image preparer with
perceptual Oklab 3-colour quantisation, Floyd-Steinberg/Atkinson/
Bayer dither, all supported tag profiles, BMPs that match the FAP
reader exactly. Auto-deployed to GitHub Pages.
- Add 'Set Image' under Targeted Payloads. Lists every BMP in
apps_data/tagtinker/dropped/, scrolls long names, marks foreign-
resolution files with a '~' prefix.
- Stream-time nearest-neighbour rescaling so any BMP can target any
tag at any page; image options screen is now Page-only.
- Remove Phone Sync menu and the Android companion app: replaced by
the browser preparer + drop folder.
- Static, IR-friendly transmit screen ('Tinkering Tag', point hint,
3-dot loader, 'Flipped ;)').
Add "+ Scan NFC" option in the Targeted Payloads menu.
Scans Mifare Ultralight NDEF tags, decodes the encoded
ESL ID, and auto-creates the target — no manual barcode
entry needed. Cyan LED blinks during scan. Loops until
a tag is found or the user presses Back.
New files:
- nfc/tagtinker_nfc.c/.h — NDEF URI decoder module
- scenes/tagtinker_scene_nfc_scan.c — NFC scan scene
Version bumped to 2.1, NFC credit added to About screen.
Pricer barcodes validate with (sum of all 17 chars) mod 10 == 0,
with the letter prefix contributing by its ASCII offset from '0'.
The old formula compared sum of the first 16 digits to the 17th,
rejecting valid codes like A4120365539013704.