Skip to content

feat(ir): add Tag Tinker ESL app - #2856

Open
sosnek wants to merge 37 commits into
BruceDevices:devfrom
sosnek:esl-ir-port
Open

feat(ir): add Tag Tinker ESL app#2856
sosnek wants to merge 37 commits into
BruceDevices:devfrom
sosnek:esl-ir-port

Conversation

@sosnek

@sosnek sosnek commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Proposed Changes

Add a 1:1 user-facing port of TagTinker (I12BP8) so Bruce can talk to infrared electronic shelf labels the same way the Flipper app does.

The app lives under Infrared → Tag Tinker (src/modules/ir/esl/). Wire protocol is vendored TagTinker C (esl_proto.h only). UI, storage, NFC decode, Set Image / Set Text, LED Test, broadcast Change Page / Diagnostic Page, and WiFi Plugins (Bruce STA → https://tagtinker.jhackerr.workers.dev) match Flipper labels and behavior. BLE phone-sync and the companion-UART path are out of scope.

Small core touch: ScrollableTextArea::show() also dismisses on Esc so About Back works. Tag Tinker raises loopTask to 16 KB via the Arduino stack hook so nested IR menus + WiFi scan do not Cache/MMU-fault on ESP32-S3.

Types of Changes

  • New feature (Infrared → Tag Tinker)
  • Minor core UX fix (scrollable text dismisses on Esc)

Verification

Host tests (no hardware):

cd tools/esl_host_tests && make test

Device: LilyGo T-Embed CC1101, SmartTAG Color 2.6.

  • Main / Settings / About (credits + description)
  • Targeted: Type Barcode, Scan NFC (PN532 prefix pages 0–11), Rename, Tag Info
  • Set Image (296x152 BMP), Set Text, LED Test
  • Broadcast Change Page / Diagnostic Page
  • WiFi Plugins: connect, Refresh catalog, render Crypto Price → Page / Transmit
  • Esc aborts TX and dismisses About

pio run -e lilygo-t-embed-cc1101 (full, non-LITE) and flash to /dev/cu.usbmodem2101.

Testing

Covered by tools/esl_host_tests (BMP, PP4, TX sequences, store, NFC barcode decode, text, WiFi manifest/header parse, menu labels). Network and IR TX are device-verified on T-Embed as above; host tests do not hit the radio or the worker.

Linked Issues

Closes #2686.

User-Facing Change

Infrared: add Tag Tinker (Flipper-faithful ESL app). Send images, text, LED test, broadcast pages, NFC scan, and WiFi Plugins to supported infrared shelf labels. About now dismisses on Back/Esc.

Further Comments

Credits in-app: Developed by I12BP8, Ported by sosnek, Research by furrtek, NFC by 7h30th3r0n3.

Use only on tags you own or are authorized to test.

Ninja-jr pushed a commit to Ninja-jr/Bruce_firmware that referenced this pull request Sep 8, 2026
sosnek and others added 28 commits September 9, 2026 14:43
Spec for a faithful port of TagTinker's IR ESL transmit flow onto Bruce
ESP32 firmware: vendored wire protocol from TagTinker PR BruceDevices#53, a new
ESP-IDF v5 RMT PP4 IR driver (~1.25 MHz hardware carrier), and an image
TX UX. Verification target is the SmartTAG Color 2.6 (type 1626) on a
LilyGo T-Embed CC1101. M0 (IR first-light) and M1 (image TX) specced in
detail; M2-M5 recorded as an approved roadmap.

Pins the load-bearing details that a first implementation attempt would
otherwise get wrong: exact per-stage frame repeat counts, the extern "C"
linkage rule for the vendored C core, RMT idle level (IR LED must idle
off), the copy encoder and carrier polarity, and the encode-fully-to-RAM
ordering invariant that keeps SD reads from straddling IR pin setup.
Seven TDD tasks. The protocol core, PP4 symbol timing, frame sequencing and
BMP pixel mapping are all pure C with no hardware dependency, so they are
unit-tested on the host with cc; only the RMT driver and the Bruce UI need
the device. Golden test vectors were generated by compiling the real PR BruceDevices#53
protocol on the host, so frame bytes and CRCs are measured rather than
derived by hand.

Also corrects two things in the spec that this planning work uncovered:

- The vendored files must live in esl/protocol/ with the compat shim at
  esl/tagtinker_app.h, because tagtinker_proto.c includes
  "../tagtinker_app.h" and that path has to resolve to the shim.
- The 80 MHz tick values were slightly wrong ({4840, 19352, 9676, 14512}).
  Derived properly they are burst 3226 and gaps {4838, 19353, 9676, 14515},
  and the plan computes them from the 64 MHz cycle counts at compile time so
  no converted constant is ever hardcoded.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Re-reviewed both documents against the upstream TagTinker source and
removed every place the port had quietly substituted its own choice.

- No hardcoded barcode anywhere. M0 becomes a wake-frame-only timing
  checkpoint driven by the real barcode prompt; upstream has no "send a
  blank image" operation, so the synthetic payload is gone too.
- Restore support for every dot-matrix profile. M1 no longer refuses
  non-1626 tags; the hardware-verification limit is stated, not enforced
  as a feature gate.
- Add the page picker (0-7), matching scene_image_options. Position,
  compression and frame-repeat stay fixed, as upstream deliberately does.
- Split data-frame pacing per family: Color 2.6 waits 50 ms after every
  frame, generic waits 1 ms after every 32nd. Sharing one policy would
  have given generic tags a gap they never see upstream.
- Accept bpp 1/2/24/32 with upstream's per-bpp strides; refusing non-1/2
  is the Color 2.6 send path's job, not the parser's.
- Use TX_COLOR26_BMP_MAX (24576) rather than an invented cap, and thread
  data_frame_repeats through instead of baking in the default.

Also corrects an error in my own earlier review: chunked streaming is
used only by the text path upstream, never by tx_stream_bmp_image, so it
belongs to M2 and is now explicitly forbidden in the image path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… tests

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…bootstrap

Co-authored-by: Cursor <cursoragent@cursor.com>
…order independent

Co-authored-by: Cursor <cursoragent@cursor.com>
…to.h

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ncel

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…branches

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…h guess

Co-authored-by: Cursor <cursoragent@cursor.com>
…the default

Co-authored-by: Cursor <cursoragent@cursor.com>
- Page remap is a prepare-time default, not a wire invariant. Upstream
  resolves in prepare_bmp_tx, the picker overwrites job->page with the raw
  index, and the send path transmits it unresolved — so an explicit page 0
  must reach the wire as 0.
- SD remount is recorded as a known gap rather than a delivered behaviour:
  it never triggers on the verification board's pinout, and every existing
  Bruce IR feature has the same gap.
- data_frame_repeats is threaded as a parameter; the Settings screen that
  exposes 1-10 is M2, so M1 using the default is correct.
- Drop the stale claim that generic tags ship as a tested refusal; they are
  implemented and unit-tested, with only hardware verification deferred.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
sosnek and others added 9 commits September 9, 2026 14:43
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Raise loopTask stack and stream HTTPS bodies so plugin fetch/render
survive TLS. Scan only UL pages 0–11. Dismiss About on Esc.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@sosnek
sosnek changed the base branch from main to dev September 9, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add tagtinker to bruce

2 participants