TagTinker V1.3 Android Companion, Extra Settings, ESL Type Detection, Bug Fixes

This commit is contained in:
i12bp8
2026-04-07 19:58:34 +02:00
parent f3095f389f
commit 09f593dcbb
32 changed files with 3243 additions and 497 deletions
+7
View File
@@ -5,10 +5,17 @@
*.fap
*.map
dist/
build/
# ufbt
.ufbt/
# Android companion
android-companion/.gradle/
android-companion/build/
android-companion/app/build/
android-companion/local.properties
# IDE
.vscode/
.idea/
+4
View File
@@ -0,0 +1,4 @@
.gradle/
build/
app/build/
local.properties
+50
View File
@@ -0,0 +1,50 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.i12bp8.tagtinker.companion"
compileSdk = 35
defaultConfig {
applicationId = "com.i12bp8.tagtinker.companion"
minSdk = 26
targetSdk = 35
versionCode = 13
versionName = "1.3.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.14"
}
}
dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.activity:activity-compose:1.9.2")
implementation(platform("androidx.compose:compose-bom:2024.09.02"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.6")
implementation("com.google.android.gms:play-services-code-scanner:16.1.0")
}
+2
View File
@@ -0,0 +1,2 @@
# no custom rules
# Intentionally minimal for first prototype build.
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<application
android:allowBackup="true"
android:label="TagTinker Badge Creator"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
+4
View File
@@ -0,0 +1,4 @@
plugins {
id("com.android.application") version "8.5.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
}
+5
View File
@@ -0,0 +1,5 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
kotlin.code.style=official
android.nonTransitiveRClass=true
android.suppressUnsupportedCompileSdk=35
+18
View File
@@ -0,0 +1,18 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "TagTinkerBadgeCreator"
include(":app")
+2 -2
View File
@@ -9,7 +9,7 @@ App(
fap_category="Infrared",
fap_author="i12bp8",
fap_description="Educational ESL study tool for owned hardware",
fap_version="1.1",
fap_version="1.3",
sources=[
"tagtinker_app.c",
"ir/tagtinker_ir.c",
@@ -19,10 +19,10 @@ App(
"scenes/tagtinker_scene_barcode_input.c",
"scenes/tagtinker_scene_broadcast.c",
"scenes/tagtinker_scene_broadcast_menu.c",
"scenes/tagtinker_scene_image_upload.c",
"scenes/tagtinker_scene_image_options.c",
"scenes/tagtinker_scene_main_menu.c",
"scenes/tagtinker_scene_preset_list.c",
"scenes/tagtinker_scene_synced_image_list.c",
"scenes/tagtinker_scene_settings.c",
"scenes/tagtinker_scene_size_picker.c",
"scenes/tagtinker_scene_target_actions.c",
+41 -130
View File
@@ -1,23 +1,8 @@
/**
* TagTinker IR transmitter - implementation (v2)
/*
* IR transmitter.
*
* Drives the Flipper Zero's built-in IR LEDs at ~1.255 MHz carrier
* for the ESL pulse-position modulation (PPM) protocol used here.
*
* v2 changes from v1:
* - Removed TIM2 ISR approach (TIM2 conflicts with Flipper's IR RX subsystem!)
* - Uses DWT cycle counter for precise timing instead (zero timer conflicts)
* - Properly handles TIM1 bus state (was crashing if already enabled)
* - Non-blocking repeat loop with cancellation support
*
* Architecture:
* TIM1 Channel 3N: 1.255 MHz PWM carrier on built-in IR LED (PB9)
* DWT->CYCCNT: Cycle-accurate timing for PPM symbol encoding
*
* CRITICAL: The built-in IR LED is on PB9 = TIM1_CH3N (complementary output).
* NOT CH3! The carrier is gated by toggling OC3M bits in TIM1->CCMR2
* (which control both CH3 and CH3N). The firmware uses PWM2 mode.
* For CH3N (complementary), Force Inactive = LED off, PWM2 = carrier on.
* TIM1 CH3N drives the built-in IR LED carrier.
* DWT->CYCCNT handles the symbol timing so we do not need another timer.
*/
#include "tagtinker_ir.h"
@@ -31,109 +16,68 @@
#include <stm32wbxx_ll_tim.h>
/* ─── Carrier configuration ───
* Target: 1.25 MHz carrier for ESL signaling
* Flipper: 64 MHz system clock, TIM1 PSC=0
* ARR = 51-1 = 50 → 64MHz/51 = 1,254,901 Hz (+4.9 kHz off, within ±10kHz)
*/
/* Carrier setup for the built-in IR LED on TIM1 CH3N. */
#define CARRIER_TIM TIM1
#define CARRIER_ARR (51 - 1)
#define CARRIER_CCR 25 /* ~50% duty cycle */
#define CARRIER_CCR 25
/* ─── PP4 timing in CPU cycles (64 MHz) ───
*
* Protocol reference: ESL Blaster FW03 ir.c, furrtek.org ESL page
* Base period t = 1/32768 Hz = 30.518 µs
*
* Burst duration: ~40 µs = 50 carrier cycles at 1.25 MHz
* ESL Blaster: 4 ticks × 10.08µs = 40.32 µs
* Our value: 40 µs × 64 = 2560 cycles
*
* Symbol gap durations (index = 2-bit symbol value):
* From ir.c pp4_steps ordering:
* pp4_steps[0] = 5 ticks → sym 00: 6 × 10.08 = 60.48 µs
* pp4_steps[1] = 23 ticks → sym 01: 24 × 10.08 = 241.92 µs
* pp4_steps[2] = 11 ticks → sym 10: 12 × 10.08 = 120.96 µs
* pp4_steps[3] = 17 ticks → sym 11: 18 × 10.08 = 181.44 µs
*
* Converting to 64 MHz cycles:
*/
#define PP4_BURST_CYCLES 2581 /* 40.33 µs */
/* PP4 sends two bits per symbol. The gap selects the symbol value. */
#define PP4_BURST_CYCLES 2581
static const uint32_t pp4_gap_cycles[4] = {
3871, /* Symbol 00: 60.48 µs */
15483, /* Symbol 01: 241.92 µs */
7741, /* Symbol 10: 120.96 µs */
11612, /* Symbol 11: 181.44 µs */
3871,
15483,
7741,
11612,
};
/* ─── PP16 timing in CPU cycles (64 MHz) ───
* Derived from esl_blaster ir.c: TIM16 steps of 4us.
* Gap values (27us to 107us) mapped to 16 symbols.
* Burst: 21us * 64 = 1344 cycles.
*/
/* PP16 sends four bits per symbol. */
#define PP16_BURST_CYCLES 1344
static const uint32_t pp16_gap_cycles[16] = {
1728, // 0000: 27µs
3264, // 0001: 51µs
2240, // 0010: 35µs
2752, // 0011: 43µs
9408, // 0100: 147µs
7872, // 0101: 123µs
8896, // 0110: 139µs
8384, // 0111: 131µs
5312, // 1000: 83µs
3776, // 1001: 59µs
4800, // 1010: 75µs
4288, // 1011: 67µs
5824, // 1100: 91µs
7360, // 1101: 115µs
6336, // 1110: 99µs
6848 // 1111: 107µs
1728,
3264,
2240,
2752,
9408,
7872,
8896,
8384,
5312,
3776,
4800,
4288,
5824,
7360,
6336,
6848
};
/* ─── Module state ─── */
static bool ir_initialized = false;
static volatile bool ir_stop_requested = false;
/* ─── Carrier control (TIM1 OC3M register manipulation) ─── */
static inline void carrier_on(void) {
/* OC3M = PWM Mode 2 (111) — matching Flipper firmware.
* For CH3N (complementary): PWM2 inverts → CH3N gets active-low PWM = carrier burst.
* This matches INFRARED_TX_CCMR_HIGH in furi_hal_infrared.c */
/* PWM2 on CH3N gives us the carrier burst on the built-in LED. */
uint32_t ccmr2 = CARRIER_TIM->CCMR2;
ccmr2 &= ~(TIM_CCMR2_OC3M);
ccmr2 |= (TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_0); /* PWM mode 2 */
ccmr2 |= (TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_0);
CARRIER_TIM->CCMR2 = ccmr2;
}
static inline void carrier_off(void) {
/* OC3M = Force Inactive (100) — CH3N goes idle = LED off.
* This matches INFRARED_TX_CCMR_LOW in furi_hal_infrared.c */
/* Force-inactive stops the carrier between symbols. */
uint32_t ccmr2 = CARRIER_TIM->CCMR2;
ccmr2 &= ~(TIM_CCMR2_OC3M);
ccmr2 |= TIM_CCMR2_OC3M_2;
CARRIER_TIM->CCMR2 = ccmr2;
}
/* ─── Cycle-accurate delay using DWT ─── */
static inline void delay_cycles(uint32_t cycles) {
uint32_t start = DWT->CYCCNT;
while((DWT->CYCCNT - start) < cycles) {
/* Busy wait — handles wraparound via unsigned subtraction */
}
}
/* ─── Send a single PP4-encoded frame ───
*
* PPM on-air sequence for N symbols:
* [burst][gap₀][burst][gap₁]...[burst][gapₙ₋₁][burst]
* = N+1 bursts with N gaps, each gap encoding one 2-bit symbol.
*
* Data is sent LSB first, 2 bits at a time per byte (matching ir.c).
*/
static void send_frame_pp4(const uint8_t* data, size_t len) {
/* PP4 walks each byte from least-significant bits upward, two bits at a time. */
for(size_t byte_idx = 0; byte_idx < len; byte_idx++) {
uint8_t current_byte = data[byte_idx];
@@ -141,26 +85,21 @@ static void send_frame_pp4(const uint8_t* data, size_t len) {
uint8_t symbol = current_byte & 0x03;
current_byte >>= 2;
/* Burst (carrier ON) */
carrier_on();
delay_cycles(PP4_BURST_CYCLES);
/* Gap (carrier OFF) — duration encodes the symbol */
carrier_off();
delay_cycles(pp4_gap_cycles[symbol]);
}
}
/* Final burst (N+1th burst, required by PPM protocol) */
carrier_on();
delay_cycles(PP4_BURST_CYCLES);
carrier_off();
}
/* ─── Send a single PP16-encoded frame ───
* Encodes 4 bits per symbol, resulting in half the pulses of PP4.
*/
static void send_frame_pp16(const uint8_t* data, size_t len) {
/* PP16 uses the same pattern, but four bits per symbol. */
for(size_t byte_idx = 0; byte_idx < len; byte_idx++) {
uint8_t current_byte = data[byte_idx];
@@ -168,35 +107,28 @@ static void send_frame_pp16(const uint8_t* data, size_t len) {
uint8_t symbol = current_byte & 0x0F;
current_byte >>= 4;
/* Burst (carrier ON) */
carrier_on();
delay_cycles(PP16_BURST_CYCLES);
/* Gap (carrier OFF) */
carrier_off();
delay_cycles(pp16_gap_cycles[symbol]);
}
}
/* Final burst */
carrier_on();
delay_cycles(PP16_BURST_CYCLES);
carrier_off();
}
/* ─── Public API ─── */
void tagtinker_ir_init(void) {
if(ir_initialized) return;
/* Safely claim TIM1: must handle case where it's already enabled
* by the firmware's IR subsystem. Bus enable asserts if already on! */
/* Claim TIM1 from the stock IR stack before configuring our own carrier. */
if(furi_hal_bus_is_enabled(FuriHalBusTIM1)) {
furi_hal_bus_disable(FuriHalBusTIM1);
}
furi_hal_bus_enable(FuriHalBusTIM1);
/* Configure GPIO for IR TX (built-in IR LEDs) */
furi_hal_gpio_init_ex(
&gpio_infrared_tx,
GpioModeAltFunctionPushPull,
@@ -204,26 +136,17 @@ void tagtinker_ir_init(void) {
GpioSpeedVeryHigh,
GpioAltFn1TIM1);
/* Configure TIM1 as 1.255 MHz carrier */
LL_TIM_SetPrescaler(CARRIER_TIM, 0);
LL_TIM_SetAutoReload(CARRIER_TIM, CARRIER_ARR);
LL_TIM_SetCounter(CARRIER_TIM, 0);
/* Channel 3 config — controls both CH3 and CH3N outputs.
* The IR LED is on CH3N (PB9), so we must enable CC3NE.
* PWM2 mode with preload, matching the firmware. */
LL_TIM_OC_SetMode(CARRIER_TIM, LL_TIM_CHANNEL_CH3, LL_TIM_OCMODE_PWM2);
LL_TIM_OC_SetCompareCH3(CARRIER_TIM, CARRIER_CCR);
LL_TIM_OC_EnablePreload(CARRIER_TIM, LL_TIM_CHANNEL_CH3);
/* CRITICAL: Enable CH3N (complementary output), NOT CH3!
* IR LED is on PB9 = TIM1_CH3N. Without this, no IR output at all. */
LL_TIM_CC_EnableChannel(CARRIER_TIM, LL_TIM_CHANNEL_CH3N);
/* Main output enable (required for TIM1 advanced timer) */
LL_TIM_EnableAllOutputs(CARRIER_TIM);
/* Start timer but force carrier OFF initially */
carrier_off();
LL_TIM_EnableCounter(CARRIER_TIM);
LL_TIM_GenerateEvent_UPDATE(CARRIER_TIM);
@@ -240,18 +163,15 @@ void tagtinker_ir_deinit(void) {
tagtinker_ir_stop();
/* Force carrier off and disable TIM1 */
carrier_off();
LL_TIM_DisableAllOutputs(CARRIER_TIM);
LL_TIM_CC_DisableChannel(CARRIER_TIM, LL_TIM_CHANNEL_CH3N);
LL_TIM_DisableCounter(CARRIER_TIM);
/* Reset TIM1 bus so firmware can reclaim it for normal IR */
if(furi_hal_bus_is_enabled(FuriHalBusTIM1)) {
furi_hal_bus_disable(FuriHalBusTIM1);
}
/* Restore GPIO to safe state */
furi_hal_gpio_init(&gpio_infrared_tx, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
ir_initialized = false;
@@ -263,17 +183,13 @@ bool tagtinker_ir_transmit(const uint8_t* data, size_t len, uint16_t repeats_raw
if(len == 0 || len > 255) return false;
ir_stop_requested = false;
// MSB of repeats indicates PP16 protocol!
bool is_pp16 = (repeats_raw & 0x8000) != 0;
uint32_t repeats = repeats_raw & 0x7FFF;
FURI_LOG_I("TagTinker", "TX start: %zu bytes, %lu repeats (PP%d), %u delay",
len, repeats, is_pp16 ? 16 : 4, delay);
/* Transmit frame with repeats.
* Between repeats we yield briefly so FreeRTOS stays happy
* and the user can cancel via tagtinker_ir_stop(). */
for(uint32_t rep = 0; rep <= repeats; rep++) {
if(ir_stop_requested) {
FURI_LOG_I("TagTinker", "TX cancelled at repeat %lu", rep);
@@ -281,34 +197,29 @@ bool tagtinker_ir_transmit(const uint8_t* data, size_t len, uint16_t repeats_raw
return false;
}
/* Send one frame (interrupts stay enabled — DWT handles timing) */
if(is_pp16) {
send_frame_pp16(data, len);
} else {
send_frame_pp4(data, len);
}
/* Delay between repeats: delay × 500µs (matching ESL Blaster).
* ESL Blaster: TickCounter = RepeatDelay * 50 ticks, each ~10µs = 500µs per unit */
if(rep < repeats) {
/* Delay units are 500 us to match the ESL timing tools. */
uint32_t delay_us = (uint32_t)delay * 500;
if(delay_us > 0) {
uint32_t delay_ms_yield = delay_us / 1000;
uint32_t delay_us_busy = delay_us % 1000;
/* Yield to FreeRTOS for the bulk of the delay to allow the GUI to animate! */
if(delay_ms_yield > 0) {
furi_delay_ms(delay_ms_yield);
}
/* Busy loop only for the sub-millisecond remainder */
if(delay_us_busy > 0) {
delay_cycles(delay_us_busy * 64); /* 64 cycles per µs */
delay_cycles(delay_us_busy * 64);
}
}
}
/* Fallback yield to FreeRTOS every 10 repeats if the delay parameter was 0 or 1 */
if(((uint32_t)delay * 500) < 1000 && (rep % 10) == 9) {
furi_delay_ms(1);
}
@@ -319,7 +230,7 @@ bool tagtinker_ir_transmit(const uint8_t* data, size_t len, uint16_t repeats_raw
}
bool tagtinker_ir_is_busy(void) {
return false; /* Transmit is blocking in this version */
return false;
}
void tagtinker_ir_stop(void) {
+4 -32
View File
@@ -1,12 +1,7 @@
/**
* TagTinker IR transmitter - header
/*
* IR transmitter API.
*
* Low-level IR transmitter for the ESL protocol used by this project.
* Drives the Flipper Zero's built-in IR LEDs at 1.255 MHz carrier
* by directly programming TIM1 registers, bypassing furi_hal_infrared.
*
* Uses DWT cycle counter for symbol timing (no timer conflicts).
* PP4 mode (2-bit symbols) for this POC.
* Sends already-built ESL frames over the built-in IR LED.
*/
#pragma once
@@ -15,36 +10,13 @@
#include <stddef.h>
#include <stdbool.h>
/**
* Initialize the IR transmitter.
* Claims TIM1 for carrier, configures GPIO for IR output.
*/
void tagtinker_ir_init(void);
/**
* Deinitialize the IR transmitter.
* Releases TIM1, restores GPIO state.
*/
void tagtinker_ir_deinit(void);
/**
* Transmit a frame using PP4 modulation.
* Blocking call — returns when all repeats complete or cancelled.
*
* @param data frame bytes
* @param len byte count
* @param repeats times to repeat (0 = send once)
* @param delay inter-repeat delay (×500µs, 10 = 5ms like ESL Blaster)
* @return true if completed, false if cancelled/error
*/
/* The high bit of repeats selects PP16. The low 15 bits are the repeat count. */
bool tagtinker_ir_transmit(const uint8_t* data, size_t len, uint16_t repeats, uint8_t delay);
/**
* Check if transmitting.
*/
bool tagtinker_ir_is_busy(void);
/**
* Stop any ongoing transmission.
*/
void tagtinker_ir_stop(void);
+21 -23
View File
@@ -1,7 +1,10 @@
/*
* TagTinker - ESL protocol helpers (implementation)
* ESL protocol helpers.
*
* Ported from furrtek/TagTinker tools_python/pr.py and img2dm.py
* This file covers three jobs:
* 1. Decode a barcode into the tag address and known display profile.
* 2. Pack pixels into the tag's raw or RLE bitmap format.
* 3. Wrap those bytes into the IR frames that the tag understands.
*
* SPDX-License-Identifier: MIT
*/
@@ -11,8 +14,6 @@
#include <string.h>
#include <stdlib.h>
/* ── Helpers ────────────────────────────────────────────────── */
typedef struct {
uint16_t type_code;
uint16_t width;
@@ -21,6 +22,7 @@ typedef struct {
TagTinkerTagColor color;
} TagTinkerProfileEntry;
/* Known type codes seen in ESL barcodes. */
static const TagTinkerProfileEntry profile_table[] = {
{1206, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono},
{1207, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono},
@@ -86,6 +88,7 @@ static size_t terminate(uint8_t* buf, size_t len) {
static size_t raw_frame(uint8_t* buf, uint8_t proto,
const uint8_t plid[4], uint8_t cmd) {
/* Every addressed frame starts with protocol byte, PLID, then command. */
buf[0] = proto;
buf[1] = plid[3]; buf[2] = plid[2];
buf[3] = plid[1]; buf[4] = plid[0];
@@ -94,6 +97,7 @@ static size_t raw_frame(uint8_t* buf, uint8_t proto,
}
static size_t mcu_frame(uint8_t* buf, const uint8_t plid[4], uint8_t cmd) {
/* Image upload is tunneled through command 0x34 with an inner MCU opcode. */
size_t p = raw_frame(buf, TAGTINKER_PROTO_DM, plid, 0x34);
buf[p++] = 0x00;
buf[p++] = 0x00;
@@ -102,8 +106,6 @@ static size_t mcu_frame(uint8_t* buf, const uint8_t plid[4], uint8_t cmd) {
return p;
}
/* ── CRC-16 (poly 0x8408, init 0x8408) ─────────────────────── */
uint16_t tagtinker_crc16(const uint8_t* data, size_t len) {
uint16_t crc = 0x8408;
for(size_t i = 0; i < len; i++) {
@@ -114,8 +116,6 @@ uint16_t tagtinker_crc16(const uint8_t* data, size_t len) {
return crc;
}
/* ── Barcode → PLID ─────────────────────────────────────────── */
bool tagtinker_barcode_to_plid(const char* barcode, uint8_t plid[4]) {
if(!barcode || strlen(barcode) != 17) return false;
for(int i = 2; i < 12; i++)
@@ -166,8 +166,6 @@ bool tagtinker_barcode_to_profile(const char* barcode, TagTinkerTagProfile* prof
return true;
}
/* ── Frame builders ─────────────────────────────────────────── */
size_t tagtinker_build_broadcast_page_frame(
uint8_t* buf, uint8_t page, bool forever, uint16_t duration) {
@@ -231,10 +229,9 @@ static void record_run(uint8_t* out, size_t* pos, size_t cap, uint32_t run_count
for(int i = 0; i < n / 2; i++) {
uint8_t t = bits[i]; bits[i] = bits[n - 1 - i]; bits[n - 1 - i] = t;
}
/* Prefix zeros (n-1 of them, skip leading 1) */
/* Runs are unary-prefixed: zeros mark bit-length, then the count bits follow. */
for(int i = 1; i < n; i++)
if(*pos < cap) out[(*pos)++] = 0;
/* The bits themselves */
for(int i = 0; i < n; i++)
if(*pos < cap) out[(*pos)++] = bits[i];
}
@@ -263,10 +260,10 @@ size_t tagtinker_rle_compress(
if(run_count > 1) record_run(out, &pos, out_cap, run_count);
if(pos < count) {
*comp_type = 2; /* RLE */
*comp_type = 2;
return pos;
}
/* Compression didn't help — use raw */
memcpy(out, pixels, count < out_cap ? count : out_cap);
*comp_type = 0;
return count < out_cap ? count : out_cap;
@@ -411,6 +408,7 @@ bool tagtinker_encode_planes_payload(
if(mode == TagTinkerCompressionRle) {
use_compressed = true;
} else if(mode == TagTinkerCompressionAuto) {
/* Auto mode picks RLE only when it is smaller than the raw bitstream. */
use_compressed = (comp_len > 0U) && (comp_len < total_pixels);
}
size_t src_len = use_compressed ? comp_len : total_pixels;
@@ -474,6 +472,7 @@ size_t tagtinker_make_image_param_frame(
uint16_t height,
uint16_t pos_x,
uint16_t pos_y) {
/* Command 0x05 tells the tag how many bytes are coming and where to place them. */
size_t p = mcu_frame(buf, plid, 0x05);
append_word(buf, &p, byte_count);
buf[p++] = 0x00;
@@ -495,6 +494,7 @@ size_t tagtinker_make_image_data_frame(
const uint8_t plid[4],
uint16_t frame_index,
const uint8_t data_bytes[20]) {
/* Command 0x20 carries one fixed 20-byte image block. */
size_t p = mcu_frame(buf, plid, 0x20);
append_word(buf, &p, frame_index);
memcpy(&buf[p], data_bytes, DATA_BYTES_PER_FRAME);
@@ -502,8 +502,6 @@ size_t tagtinker_make_image_data_frame(
return terminate(buf, p);
}
/* ── Image upload sequence builder ──────────────────────────── */
void tagtinker_build_image_sequence(
TagTinkerApp* app,
const uint8_t plid[4],
@@ -517,6 +515,8 @@ void tagtinker_build_image_sequence(
if(!tagtinker_encode_image_payload(
pixels, width, height, app->color_clear, app->compression_mode, &payload))
return;
/* The tag expects 20 data bytes per frame, so pad the payload to that boundary. */
size_t frame_count = payload.byte_count / DATA_BYTES_PER_FRAME;
FURI_LOG_I("TagTinker", "IMG %ux%u pg=%u comp=%u %zu->%zu frames=%zu",
@@ -528,7 +528,6 @@ void tagtinker_build_image_sequence(
payload.byte_count,
frame_count);
/* Total: ping + params + N data + refresh */
size_t total = 2 + frame_count + 1;
app->frame_seq_count = total;
@@ -544,13 +543,13 @@ void tagtinker_build_image_sequence(
size_t idx = 0;
/* 1. Ping */
/* Wake the tag before sending the upload. */
app->frame_sequence[idx] = malloc(TAGTINKER_MAX_FRAME_SIZE);
app->frame_lengths[idx] = tagtinker_make_ping_frame(app->frame_sequence[idx], plid);
app->frame_repeats[idx] = wake_repeats;
idx++;
/* 2. Parameters (cmd 0x05) */
/* The parameter frame describes size, page, compression mode, and placement. */
app->frame_sequence[idx] = malloc(TAGTINKER_MAX_FRAME_SIZE);
app->frame_lengths[idx] = tagtinker_make_image_param_frame(
app->frame_sequence[idx],
@@ -565,22 +564,21 @@ void tagtinker_build_image_sequence(
app->frame_repeats[idx] = 1;
idx++;
/* 3..N+2. Data frames (cmd 0x20) */
/* Data frames follow in order and carry the packed bitmap bytes. */
for(size_t fi = 0; fi < frame_count; fi++) {
app->frame_sequence[idx] = malloc(TAGTINKER_MAX_FRAME_SIZE);
size_t start = fi * DATA_BYTES_PER_FRAME;
app->frame_lengths[idx] = tagtinker_make_image_data_frame(
app->frame_sequence[idx], plid, (uint16_t)fi, &payload.data[start]);
app->frame_repeats[idx] = 3; /* 3 repeats per data frame for reliability */
app->frame_repeats[idx] = 3;
idx++;
}
/* N+3. Refresh */
/* Refresh asks the tag to display the uploaded image. */
app->frame_sequence[idx] = malloc(TAGTINKER_MAX_FRAME_SIZE);
app->frame_lengths[idx] = tagtinker_make_refresh_frame(app->frame_sequence[idx], plid);
app->frame_repeats[idx] = 1;
/* Copy first data frame for display in TX scene */
if(app->frame_seq_count > 1) {
memcpy(app->frame_buf, app->frame_sequence[1],
app->frame_lengths[1] < TAGTINKER_MAX_FRAME_SIZE
+12 -24
View File
@@ -1,8 +1,8 @@
/*
* TagTinker - ESL protocol helpers
* ESL protocol helpers.
*
* Frame construction, CRC, and encoding for the infrared
* ESL protocol used by this project. Ported from furrtek/TagTinker.
* This layer turns barcodes, pixels, and payload bytes into the frames
* that the Flipper sends over IR to the tag.
*
* SPDX-License-Identifier: MIT
*/
@@ -13,19 +13,15 @@
#include <stddef.h>
#include <stdbool.h>
#define TAGTINKER_PROTO_DM 0x85 /* Dot-matrix / graphic ESLs */
#define TAGTINKER_PROTO_SEG 0x84 /* 7-segment ESLs */
#define TAGTINKER_PROTO_DM 0x85
#define TAGTINKER_PROTO_SEG 0x84
#define TAGTINKER_MAX_FRAME_SIZE 96
/* Forward declaration for TagTinkerApp (avoids circular include) */
typedef struct TagTinkerApp TagTinkerApp;
/* ── CRC ────────────────────────────────────────────────────── */
/* CRC used by the ESL wire format. */
uint16_t tagtinker_crc16(const uint8_t* data, size_t len);
/* ── Barcode / PLID ─────────────────────────────────────────── */
typedef enum {
TagTinkerTagKindUnknown = 0,
TagTinkerTagKindDotMatrix,
@@ -93,40 +89,32 @@ size_t tagtinker_make_image_data_frame(
uint16_t frame_index,
const uint8_t data_bytes[20]);
/* ── Frame builders ─────────────────────────────────────────── */
/* Broadcast page-change (no barcode needed). */
/* Broadcast frames address every listening tag. */
size_t tagtinker_build_broadcast_page_frame(
uint8_t* buf, uint8_t page, bool forever, uint16_t duration);
/* Broadcast diagnostic page. */
size_t tagtinker_build_broadcast_debug_frame(uint8_t* buf);
/* Addressed DM frame: wraps raw payload with protocol + PLID + CRC. */
/* Addressed frames use the PLID decoded from the barcode. */
size_t tagtinker_make_addressed_frame(
uint8_t* buf, const uint8_t plid[4],
const uint8_t* payload, size_t payload_len);
/* Wake-up ping (must be sent before most addressed commands). */
/* Tags need a wake ping before most addressed commands. */
size_t tagtinker_make_ping_frame(uint8_t* buf, const uint8_t plid[4]);
/* Display refresh request. */
size_t tagtinker_make_refresh_frame(uint8_t* buf, const uint8_t plid[4]);
/* MCU-level frame (used for image upload protocol). */
/* Image upload uses MCU frames: one parameter frame followed by data frames. */
size_t tagtinker_make_mcu_frame(
uint8_t* buf, const uint8_t plid[4], uint8_t cmd);
/* ── Image upload helpers ───────────────────────────────────── */
/* RLE-compress a pixel array (0/1 values).
* Returns compressed bitstream length. comp_type is set to 0 (raw) or 2 (RLE). */
/* RLE is the tag's compact bitmap format. Raw mode keeps one bit per pixel. */
size_t tagtinker_rle_compress(
const uint8_t* pixels, size_t count,
uint8_t* out, size_t out_cap, uint8_t* comp_type);
/* Build a complete image-upload frame sequence and store it in app state.
* Allocates memory that the transmit scene frees on exit. */
/* Builds the full IR sequence: wake, image params, data chunks, refresh. */
void tagtinker_build_image_sequence(
TagTinkerApp* app,
const uint8_t plid[4],
+3 -3
View File
@@ -15,8 +15,8 @@ void(*const tagtinker_scene_on_enter_handlers[])(void*) = {
tagtinker_scene_barcode_input_on_enter,
tagtinker_scene_text_input_on_enter,
tagtinker_scene_preset_list_on_enter,
tagtinker_scene_synced_image_list_on_enter,
tagtinker_scene_size_picker_on_enter,
tagtinker_scene_image_upload_on_enter,
tagtinker_scene_image_options_on_enter,
tagtinker_scene_transmit_on_enter,
tagtinker_scene_about_on_enter,
@@ -33,8 +33,8 @@ bool(*const tagtinker_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
tagtinker_scene_barcode_input_on_event,
tagtinker_scene_text_input_on_event,
tagtinker_scene_preset_list_on_event,
tagtinker_scene_synced_image_list_on_event,
tagtinker_scene_size_picker_on_event,
tagtinker_scene_image_upload_on_event,
tagtinker_scene_image_options_on_event,
tagtinker_scene_transmit_on_event,
tagtinker_scene_about_on_event,
@@ -51,8 +51,8 @@ void(*const tagtinker_scene_on_exit_handlers[])(void*) = {
tagtinker_scene_barcode_input_on_exit,
tagtinker_scene_text_input_on_exit,
tagtinker_scene_preset_list_on_exit,
tagtinker_scene_synced_image_list_on_exit,
tagtinker_scene_size_picker_on_exit,
tagtinker_scene_image_upload_on_exit,
tagtinker_scene_image_options_on_exit,
tagtinker_scene_transmit_on_exit,
tagtinker_scene_about_on_exit,
+5 -6
View File
@@ -1,5 +1,5 @@
/*
* TagTinker — Scene Definitions
* Scene definitions.
*/
#pragma once
@@ -17,15 +17,14 @@ typedef enum {
TagTinkerSceneBarcodeInput,
TagTinkerSceneTextInput,
TagTinkerScenePresetList,
TagTinkerSceneSyncedImageList,
TagTinkerSceneSizePicker,
TagTinkerSceneImageUpload,
TagTinkerSceneImageOptions,
TagTinkerSceneTransmit,
TagTinkerSceneAbout,
TagTinkerSceneCount,
} TagTinkerScene;
/* Scene handler declarations */
void tagtinker_scene_warning_on_enter(void* ctx);
bool tagtinker_scene_warning_on_event(void* ctx, SceneManagerEvent event);
void tagtinker_scene_warning_on_exit(void* ctx);
@@ -70,9 +69,9 @@ void tagtinker_scene_preset_list_on_enter(void* ctx);
bool tagtinker_scene_preset_list_on_event(void* ctx, SceneManagerEvent event);
void tagtinker_scene_preset_list_on_exit(void* ctx);
void tagtinker_scene_image_upload_on_enter(void* ctx);
bool tagtinker_scene_image_upload_on_event(void* ctx, SceneManagerEvent event);
void tagtinker_scene_image_upload_on_exit(void* ctx);
void tagtinker_scene_synced_image_list_on_enter(void* ctx);
bool tagtinker_scene_synced_image_list_on_event(void* ctx, SceneManagerEvent event);
void tagtinker_scene_synced_image_list_on_exit(void* ctx);
void tagtinker_scene_image_options_on_enter(void* ctx);
bool tagtinker_scene_image_options_on_event(void* ctx, SceneManagerEvent event);
+756 -6
View File
@@ -1,25 +1,714 @@
/*
* About — credits (state=0) or Android teaser (state=1)
* About and phone sync scene.
*/
#include "../tagtinker_app.h"
#include <gui/elements.h>
#define TAGTINKER_SYNC_DIR APP_DATA_PATH("sync")
#define TAGTINKER_SYNC_INDEX_PATH APP_DATA_PATH("synced_images.txt")
#define TAGTINKER_BLE_FLOW_WINDOW 8192U
#define TAGTINKER_SYNC_MAX_CHUNK_BYTES 384U
enum {
AboutEventSendLatestPhone = 1,
};
typedef struct {
uint32_t mode;
uint32_t tick;
char status_text[32];
bool can_send_latest;
char target_name[TAGTINKER_TARGET_NAME_LEN + 1];
} AboutViewModel;
static void ble_set_status(TagTinkerApp* app, const char* text) {
if(!app || !text) return;
snprintf(app->ble_status_text, sizeof(app->ble_status_text), "%s", text);
}
static void ble_send_line(TagTinkerApp* app, const char* line) {
if(!app || !app->ble_serial || !line) return;
uint8_t buf[256];
size_t n = strlen(line);
if(n > sizeof(buf) - 2U) n = sizeof(buf) - 2U;
memcpy(buf, line, n);
buf[n++] = '\n';
ble_profile_serial_tx(app->ble_serial, buf, (uint16_t)n);
}
static void ble_set_rx_status(TagTinkerApp* app, const char* line) {
if(!app || !line) return;
snprintf(app->ble_status_text, sizeof(app->ble_status_text), "RX %.24s", line);
}
static char* sync_next_token(char** cursor) {
if(!cursor || !*cursor) return NULL;
char* token = *cursor;
char* sep = strchr(token, '|');
if(sep) {
*sep = '\0';
*cursor = sep + 1;
} else {
*cursor = NULL;
}
return token;
}
static bool sync_safe_token(const char* value, size_t max_len) {
if(!value || !*value) return false;
size_t len = strlen(value);
if(len == 0U || len > max_len) return false;
for(size_t i = 0; i < len; i++) {
char c = value[i];
if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
c == '_' || c == '-') {
continue;
}
return false;
}
return true;
}
static void sync_send_targets(TagTinkerApp* app) {
if(!app) return;
char line[96];
snprintf(line, sizeof(line), "TT_TARGETS_BEGIN|%u", app->target_count);
ble_send_line(app, line);
for(uint8_t i = 0; i < app->target_count; i++) {
const TagTinkerTarget* target = &app->targets[i];
snprintf(
line,
sizeof(line),
"TT_TARGET|%s|%s|%u|%u",
target->barcode,
target->name,
target->profile.width,
target->profile.height);
ble_send_line(app, line);
}
ble_send_line(app, "TT_TARGETS_END");
}
static void sync_clear_active_job(TagTinkerApp* app) {
if(!app) return;
app->ble_sync_job_active = false;
app->ble_sync_compact_protocol = false;
app->ble_sync_job_id[0] = '\0';
app->ble_sync_barcode[0] = '\0';
app->ble_sync_temp_path[0] = '\0';
app->ble_sync_final_path[0] = '\0';
app->ble_sync_expected_bytes = 0;
app->ble_sync_received_bytes = 0;
app->ble_sync_last_chunk = 0;
}
static void sync_abort_active_job(TagTinkerApp* app) {
if(!app) return;
if(app->ble_sync_temp_path[0] != '\0') {
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_common_remove(storage, app->ble_sync_temp_path);
furi_record_close(RECORD_STORAGE);
}
sync_clear_active_job(app);
}
static bool sync_append_index_record(
const char* job_id,
const char* barcode,
uint16_t width,
uint16_t height,
uint8_t page,
const char* image_path) {
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_common_mkdir(storage, APP_DATA_PATH(""));
File* file = storage_file_alloc(storage);
bool ok = storage_file_open(file, TAGTINKER_SYNC_INDEX_PATH, FSAM_WRITE, FSOM_OPEN_APPEND);
if(!ok) {
ok = storage_file_open(file, TAGTINKER_SYNC_INDEX_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS);
}
if(ok) {
char line[384];
int len = snprintf(
line,
sizeof(line),
"%s|%s|%u|%u|%u|%s\n",
job_id,
barcode,
width,
height,
page,
image_path);
ok = (len > 0) && ((size_t)len < sizeof(line)) &&
(storage_file_write(file, line, (uint16_t)len) == (uint16_t)len);
storage_file_close(file);
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
return ok;
}
static int8_t sync_base64_value(char c) {
if(c >= 'A' && c <= 'Z') return (int8_t)(c - 'A');
if(c >= 'a' && c <= 'z') return (int8_t)(c - 'a' + 26);
if(c >= '0' && c <= '9') return (int8_t)(c - '0' + 52);
if(c == '+' || c == '-') return 62;
if(c == '/' || c == '_') return 63;
if(c == '=') return -2;
return -1;
}
static bool sync_decode_base64(
const char* input,
uint8_t* output,
size_t output_size,
size_t* output_len) {
if(!input || !output || !output_len) return false;
size_t out_len = 0;
uint8_t quartet[4];
uint8_t quartet_len = 0;
uint8_t padding = 0;
for(const char* p = input; *p; p++) {
int8_t value = sync_base64_value(*p);
if(value == -1) return false;
if(value == -2) {
value = 0;
padding++;
}
quartet[quartet_len++] = (uint8_t)value;
if(quartet_len != 4U) continue;
if(out_len + 3U > output_size) return false;
output[out_len++] = (uint8_t)((quartet[0] << 2U) | (quartet[1] >> 4U));
if(padding < 2U) output[out_len++] = (uint8_t)((quartet[1] << 4U) | (quartet[2] >> 2U));
if(padding == 0U) output[out_len++] = (uint8_t)((quartet[2] << 6U) | quartet[3]);
quartet_len = 0;
padding = 0;
}
if(quartet_len != 0U) return false;
*output_len = out_len;
return true;
}
static bool sync_begin_job(
TagTinkerApp* app,
const char* job_id,
const char* barcode,
uint16_t width,
uint16_t height,
uint8_t page,
uint32_t byte_count,
bool compact_protocol) {
if(!app || !sync_safe_token(job_id, TAGTINKER_SYNC_JOB_ID_LEN) ||
(barcode && *barcode && !sync_safe_token(barcode, TAGTINKER_BC_LEN)) || width == 0U || height == 0U ||
page > 7U || byte_count == 0U) {
return false;
}
sync_abort_active_job(app);
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_common_mkdir(storage, APP_DATA_PATH(""));
storage_common_mkdir(storage, TAGTINKER_SYNC_DIR);
snprintf(
app->ble_sync_temp_path,
sizeof(app->ble_sync_temp_path),
"%s/%s.part",
TAGTINKER_SYNC_DIR,
job_id);
snprintf(
app->ble_sync_final_path,
sizeof(app->ble_sync_final_path),
"%s/%s.bmp",
TAGTINKER_SYNC_DIR,
job_id);
storage_common_remove(storage, app->ble_sync_temp_path);
storage_common_remove(storage, app->ble_sync_final_path);
File* file = storage_file_alloc(storage);
bool ok = storage_file_open(file, app->ble_sync_temp_path, FSAM_WRITE, FSOM_CREATE_ALWAYS);
if(ok) {
storage_file_close(file);
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
if(!ok) return false;
strncpy(app->ble_sync_job_id, job_id, TAGTINKER_SYNC_JOB_ID_LEN);
app->ble_sync_job_id[TAGTINKER_SYNC_JOB_ID_LEN] = '\0';
if(barcode && *barcode) {
strncpy(app->ble_sync_barcode, barcode, TAGTINKER_BC_LEN);
app->ble_sync_barcode[TAGTINKER_BC_LEN] = '\0';
} else {
app->ble_sync_barcode[0] = '\0';
}
app->ble_sync_expected_bytes = byte_count;
app->ble_sync_received_bytes = 0;
app->ble_sync_last_chunk = 0;
app->ble_synced_lines = 0;
app->img_page = page;
app->esl_width = width;
app->esl_height = height;
app->ble_sync_job_active = true;
app->ble_sync_compact_protocol = compact_protocol;
app->ble_sync_ready_target = -1;
app->ble_status_text[0] = '\0';
ble_set_status(app, "Upload started");
ble_send_line(app, compact_protocol ? "AB" : "TT_ACK|BEGIN");
return true;
}
static bool sync_set_job_barcode(TagTinkerApp* app, const char* barcode) {
if(!app || !app->ble_sync_job_active || !barcode || !sync_safe_token(barcode, TAGTINKER_BC_LEN)) {
return false;
}
if(app->ble_sync_barcode[0] != '\0' && strcmp(app->ble_sync_barcode, barcode) != 0) {
return false;
}
strncpy(app->ble_sync_barcode, barcode, TAGTINKER_BC_LEN);
app->ble_sync_barcode[TAGTINKER_BC_LEN] = '\0';
ble_send_line(app, app->ble_sync_compact_protocol ? "AT" : "TT_ACK|TARGET");
return true;
}
static bool sync_append_chunk(TagTinkerApp* app, uint16_t sequence, const char* payload) {
if(!app || !app->ble_sync_job_active || !payload || sequence == 0U) return false;
if(sequence == app->ble_sync_last_chunk) {
char ack[32];
if(app->ble_sync_compact_protocol) {
snprintf(ack, sizeof(ack), "A%04X", sequence);
} else {
snprintf(ack, sizeof(ack), "TT_ACK|%u", sequence);
}
ble_send_line(app, ack);
return true;
}
if(sequence != (uint16_t)(app->ble_sync_last_chunk + 1U)) return false;
uint8_t decoded[TAGTINKER_SYNC_MAX_CHUNK_BYTES];
size_t decoded_len = 0;
if(!sync_decode_base64(payload, decoded, sizeof(decoded), &decoded_len)) return false;
if((app->ble_sync_received_bytes + decoded_len) > app->ble_sync_expected_bytes) return false;
Storage* storage = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(storage);
bool ok = storage_file_open(file, app->ble_sync_temp_path, FSAM_WRITE, FSOM_OPEN_APPEND);
if(ok) {
ok = storage_file_write(file, decoded, decoded_len) == decoded_len;
storage_file_close(file);
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
if(!ok) return false;
app->ble_sync_received_bytes += decoded_len;
app->ble_sync_last_chunk = sequence;
app->ble_synced_lines = sequence;
snprintf(app->ble_status_text, sizeof(app->ble_status_text), "RX %u chunks", sequence);
char ack[32];
if(app->ble_sync_compact_protocol) {
snprintf(ack, sizeof(ack), "A%04X", sequence);
} else {
snprintf(ack, sizeof(ack), "TT_ACK|%u", sequence);
}
ble_send_line(app, ack);
return true;
}
static bool sync_finish_job(TagTinkerApp* app, const char* job_id) {
if(!app || !sync_safe_token(job_id, TAGTINKER_SYNC_JOB_ID_LEN)) return false;
if(!app->ble_sync_job_active && strcmp(job_id, app->ble_sync_last_job_id) == 0) {
char ack[32];
if(app->ble_sync_last_compact_protocol) {
snprintf(ack, sizeof(ack), "AE");
} else {
snprintf(ack, sizeof(ack), "TT_ACK|END|%u", app->ble_sync_last_completed_chunks);
}
ble_send_line(app, ack);
return true;
}
if(!app->ble_sync_job_active || strcmp(job_id, app->ble_sync_job_id) != 0) return false;
if(app->ble_sync_barcode[0] == '\0') {
ble_set_status(app, "No target");
return false;
}
if(app->ble_sync_received_bytes != app->ble_sync_expected_bytes) {
ble_set_status(app, "Size mismatch");
return false;
}
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_common_remove(storage, app->ble_sync_final_path);
bool ok =
storage_common_rename(storage, app->ble_sync_temp_path, app->ble_sync_final_path) ==
FSE_OK;
furi_record_close(RECORD_STORAGE);
if(!ok) {
ble_set_status(app, "Save failed");
return false;
}
ok = sync_append_index_record(
app->ble_sync_job_id,
app->ble_sync_barcode,
app->esl_width,
app->esl_height,
app->img_page,
app->ble_sync_final_path);
if(!ok) {
ble_set_status(app, "Index failed");
return false;
}
strncpy(app->ble_sync_last_job_id, app->ble_sync_job_id, TAGTINKER_SYNC_JOB_ID_LEN);
app->ble_sync_last_job_id[TAGTINKER_SYNC_JOB_ID_LEN] = '\0';
app->ble_sync_last_completed_chunks = app->ble_sync_last_chunk;
app->ble_sync_last_compact_protocol = app->ble_sync_compact_protocol;
char ack[32];
if(app->ble_sync_compact_protocol) {
snprintf(ack, sizeof(ack), "AE");
} else {
snprintf(ack, sizeof(ack), "TT_ACK|END|%u", app->ble_sync_last_chunk);
}
int8_t target_index = tagtinker_ensure_target(app, app->ble_sync_barcode);
if(target_index >= 0) {
tagtinker_select_target(app, (uint8_t)target_index);
app->ble_sync_ready_target = target_index;
snprintf(
app->ble_status_text,
sizeof(app->ble_status_text),
"Saved for %.20s",
app->targets[target_index].name);
} else {
app->ble_sync_ready_target = -1;
ble_set_status(app, "Saved on Flipper");
}
ble_send_line(app, ack);
sync_clear_active_job(app);
return true;
}
static void sync_apply_line(TagTinkerApp* app, const char* line) {
if(!app || !line) return;
/*
* Compact upload protocol:
* B<job><w><h><page><size> begin upload
* C<barcode> bind upload to a target
* D<seq><base64> append one chunk
* E<job> finish upload
*
* Acks are AB, AT, A<seq>, and AE.
*/
if(strcmp(line, "TT_PING") == 0) {
ble_set_status(app, "RX ping");
ble_send_line(app, "TT_PONG");
return;
}
if(strcmp(line, "TT_LIST_TARGETS") == 0) {
sync_send_targets(app);
return;
}
if(strncmp(line, "TT_BEGIN|", 9) == 0) {
char temp[160];
strncpy(temp, line, sizeof(temp) - 1U);
temp[sizeof(temp) - 1U] = '\0';
char* cursor = temp;
sync_next_token(&cursor);
char* job_id = sync_next_token(&cursor);
char* barcode = sync_next_token(&cursor);
char* width = sync_next_token(&cursor);
char* height = sync_next_token(&cursor);
char* page = sync_next_token(&cursor);
char* bytes = sync_next_token(&cursor);
if(job_id && barcode && width && height && page && bytes &&
sync_begin_job(
app,
job_id,
barcode,
(uint16_t)atoi(width),
(uint16_t)atoi(height),
(uint8_t)atoi(page),
(uint32_t)strtoul(bytes, NULL, 10),
false)) {
return;
}
ble_set_status(app, "BEGIN failed");
return;
}
if(strncmp(line, "TT_DATA|", 8) == 0) {
char temp[1024];
strncpy(temp, line, sizeof(temp) - 1U);
temp[sizeof(temp) - 1U] = '\0';
char* cursor = temp;
sync_next_token(&cursor);
char* seq = sync_next_token(&cursor);
char* payload = sync_next_token(&cursor);
if(seq && payload && sync_append_chunk(app, (uint16_t)atoi(seq), payload)) {
return;
}
ble_set_status(app, "DATA failed");
return;
}
size_t compact_len = strlen(line);
if(line[0] == 'B' && compact_len >= 18U && compact_len <= 20U) {
char job_id[7];
char width_hex[4];
char height_hex[4];
char page_hex[2];
char size_hex[7];
memcpy(job_id, line + 1, 6);
job_id[6] = '\0';
memcpy(width_hex, line + 7, 3);
width_hex[3] = '\0';
memcpy(height_hex, line + 10, 3);
height_hex[3] = '\0';
memcpy(page_hex, line + 13, 1);
page_hex[1] = '\0';
size_t size_hex_len = compact_len - 14U;
memcpy(size_hex, line + 14, size_hex_len);
size_hex[size_hex_len] = '\0';
if(sync_begin_job(
app,
job_id,
NULL,
(uint16_t)strtoul(width_hex, NULL, 16),
(uint16_t)strtoul(height_hex, NULL, 16),
(uint8_t)strtoul(page_hex, NULL, 16),
(uint32_t)strtoul(size_hex, NULL, 16),
true)) {
return;
}
ble_set_status(app, "BEGIN failed");
return;
}
if(line[0] == 'C' && strlen(line) == 18U) {
if(sync_set_job_barcode(app, line + 1)) {
return;
}
ble_set_status(app, "TARGET failed");
return;
}
if(line[0] == 'D' && strlen(line) > 5U) {
char seq_hex[5];
memcpy(seq_hex, line + 1, 4);
seq_hex[4] = '\0';
if(sync_append_chunk(app, (uint16_t)strtoul(seq_hex, NULL, 16), line + 5)) {
return;
}
ble_set_status(app, "DATA failed");
return;
}
if(strncmp(line, "TT_END|", 7) == 0) {
const char* job_id = line + 7;
if(sync_finish_job(app, job_id)) {
return;
}
ble_set_status(app, "END failed");
return;
}
if(line[0] == 'E' && strlen(line) == 7U) {
if(sync_finish_job(app, line + 1)) {
return;
}
ble_set_status(app, "END failed");
return;
}
ble_set_rx_status(app, line);
}
static uint16_t ble_rx_callback(SerialServiceEvent event, void* context) {
TagTinkerApp* app = context;
if(event.event == SerialServiceEventTypeDataReceived) {
for(uint16_t i = 0; i < event.data.size; i++) {
char c = (char)event.data.buffer[i];
if(c == '\n' || c == '\r') {
if(app->ble_rx_len > 0U && !app->ble_rx_pending_ready) {
app->ble_rx_line[app->ble_rx_len] = '\0';
strncpy(
app->ble_rx_pending_line,
app->ble_rx_line,
sizeof(app->ble_rx_pending_line) - 1U);
app->ble_rx_pending_line[sizeof(app->ble_rx_pending_line) - 1U] = '\0';
app->ble_rx_pending_ready = true;
app->ble_rx_len = 0;
}
} else if(!app->ble_rx_pending_ready && app->ble_rx_len < (sizeof(app->ble_rx_line) - 1U)) {
app->ble_rx_line[app->ble_rx_len++] = c;
}
}
}
if(app->ble_rx_pending_ready) return 0U;
return (uint16_t)((sizeof(app->ble_rx_line) - 1U) - app->ble_rx_len);
}
static void bt_status_cb(BtStatus status, void* context) {
TagTinkerApp* app = context;
app->ble_status = status;
switch(status) {
case BtStatusConnected:
if(app->ble_serial) {
ble_profile_serial_set_event_callback(
app->ble_serial, TAGTINKER_BLE_FLOW_WINDOW, ble_rx_callback, app);
ble_profile_serial_set_rpc_active(app->ble_serial, false);
}
ble_set_status(app, "Connected");
ble_send_line(app, "TT_HELLO");
break;
case BtStatusAdvertising:
ble_set_status(app, "Waiting phone");
break;
case BtStatusOff:
ble_set_status(app, "Bluetooth off");
break;
default:
ble_set_status(app, "BLE idle");
break;
}
}
static void ble_sync_start(TagTinkerApp* app) {
if(!app || !app->bt || app->ble_sync_active) return;
bt_disconnect(app->bt);
bt_set_status_changed_callback(app->bt, bt_status_cb, app);
app->ble_serial = bt_profile_start(app->bt, ble_profile_serial, NULL);
if(!app->ble_serial) {
ble_set_status(app, "BLE start fail");
return;
}
app->ble_synced_lines = 0;
app->ble_rx_len = 0;
app->ble_rx_line[0] = '\0';
app->ble_rx_pending_line[0] = '\0';
app->ble_rx_pending_ready = false;
sync_clear_active_job(app);
app->ble_sync_last_job_id[0] = '\0';
app->ble_sync_last_completed_chunks = 0;
app->ble_sync_last_compact_protocol = false;
app->ble_sync_ready_target = -1;
ble_profile_serial_set_event_callback(app->ble_serial, TAGTINKER_BLE_FLOW_WINDOW, ble_rx_callback, app);
ble_profile_serial_set_rpc_active(app->ble_serial, false);
ble_set_status(app, "Waiting phone");
app->ble_sync_active = true;
}
static void ble_sync_stop(TagTinkerApp* app) {
if(!app || !app->ble_sync_active) return;
if(app->ble_sync_job_active) {
sync_abort_active_job(app);
}
bt_profile_restore_default(app->bt);
app->ble_serial = NULL;
app->ble_sync_active = false;
app->ble_sync_ready_target = -1;
}
static bool about_input_cb(InputEvent* event, void* context) {
TagTinkerApp* app = context;
if(event->type != InputTypeShort || event->key != InputKeyOk) {
return false;
}
if(app->ble_sync_ready_target < 0 || app->ble_sync_ready_target >= app->target_count) {
return false;
}
view_dispatcher_send_custom_event(app->view_dispatcher, AboutEventSendLatestPhone);
return true;
}
static void about_draw_cb(Canvas* canvas, void* _model) {
AboutViewModel* model = _model;
canvas_set_font(canvas, FontPrimary);
if(model->mode == 1) {
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignCenter, "In Progress");
if(model->mode == 1U) {
canvas_draw_str_aligned(canvas, 64, 8, AlignCenter, AlignTop, "Phone Sync");
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(
canvas,
64,
20,
AlignCenter,
AlignTop,
model->can_send_latest ? model->target_name : "Pick target on phone");
canvas_draw_str_aligned(
canvas,
64,
30,
AlignCenter,
AlignTop,
model->can_send_latest ? "Press OK to send latest" : "Upload image to Flipper");
canvas_draw_str_aligned(canvas, 64, 44, AlignCenter, AlignTop, "Status:");
canvas_draw_str_aligned(canvas, 64, 54, AlignCenter, AlignTop, model->status_text);
if(model->can_send_latest) {
elements_button_center(canvas, "Send");
}
} else {
canvas_draw_str_aligned(
canvas, 64, 10, AlignCenter, AlignTop, TAGTINKER_DISPLAY_NAME " v" TAGTINKER_VERSION);
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignTop, "Ported by I12BP8");
canvas_draw_str_aligned(canvas, 64, 34, AlignCenter, AlignTop, "Research by furrtek");
@@ -34,28 +723,89 @@ void tagtinker_scene_about_on_enter(void* ctx) {
view_allocate_model(app->about_view, ViewModelTypeLockFree, sizeof(AboutViewModel));
view_set_context(app->about_view, app);
view_set_draw_callback(app->about_view, about_draw_cb);
view_set_input_callback(app->about_view, about_input_cb);
app->about_view_allocated = true;
}
AboutViewModel* model = view_get_model(app->about_view);
if(app->ble_status_text[0] == '\0') {
ble_set_status(app, mode == 1U ? "Waiting phone" : "Idle");
}
model->mode = mode;
model->tick = 0;
model->can_send_latest = false;
model->target_name[0] = '\0';
strncpy(model->status_text, app->ble_status_text, sizeof(model->status_text) - 1U);
model->status_text[sizeof(model->status_text) - 1U] = '\0';
view_commit_model(app->about_view, true);
if(mode == 1U) {
ble_sync_start(app);
}
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewAbout);
}
bool tagtinker_scene_about_on_event(void* ctx, SceneManagerEvent event) {
TagTinkerApp* app = ctx;
if(event.type == SceneManagerEventTypeCustom && event.event == AboutEventSendLatestPhone) {
if(app->ble_sync_ready_target >= 0 && app->ble_sync_ready_target < app->target_count) {
TagTinkerTarget* target = &app->targets[app->ble_sync_ready_target];
TagTinkerSyncedImage image;
if(tagtinker_find_latest_synced_image(app, target->barcode, &image)) {
tagtinker_select_target(app, (uint8_t)app->ble_sync_ready_target);
app->img_page = image.page;
app->draw_x = 0;
app->draw_y = 0;
app->color_clear = false;
tagtinker_prepare_bmp_tx(
app,
target->plid,
image.image_path,
image.width,
image.height,
image.page);
app->tx_spam = false;
app->ble_sync_ready_target = -1;
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit);
return true;
}
}
return true;
}
if(event.type == SceneManagerEventTypeTick) {
AboutViewModel* model = view_get_model(app->about_view);
model->tick++;
if(app->ble_rx_pending_ready) {
sync_apply_line(app, app->ble_rx_pending_line);
app->ble_rx_pending_line[0] = '\0';
app->ble_rx_pending_ready = false;
if(app->ble_serial) {
ble_profile_serial_notify_buffer_is_empty(app->ble_serial);
}
}
model->can_send_latest =
(app->ble_sync_ready_target >= 0 && app->ble_sync_ready_target < app->target_count);
if(model->can_send_latest) {
strncpy(
model->target_name,
app->targets[app->ble_sync_ready_target].name,
sizeof(model->target_name) - 1U);
model->target_name[sizeof(model->target_name) - 1U] = '\0';
} else {
model->target_name[0] = '\0';
}
strncpy(model->status_text, app->ble_status_text, sizeof(model->status_text) - 1U);
model->status_text[sizeof(model->status_text) - 1U] = '\0';
view_commit_model(app->about_view, true);
return true;
}
return false;
}
void tagtinker_scene_about_on_exit(void* ctx) {
UNUSED(ctx);
TagTinkerApp* app = ctx;
ble_sync_stop(app);
}
+2 -28
View File
@@ -1,13 +1,11 @@
/*
* Barcode Input — custom barcode selector
* User enters 1 letter + 16 digits with arrow keys.
* Barcode input scene.
*/
#include "../tagtinker_app.h"
static void numlock_done(void* ctx, const char* barcode) {
TagTinkerApp* app = ctx;
/* Copy result to app barcode buffer */
strncpy(app->barcode, barcode, TAGTINKER_BC_LEN);
app->barcode[TAGTINKER_BC_LEN] = '\0';
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
@@ -45,31 +43,7 @@ bool tagtinker_scene_barcode_input_on_event(void* ctx, SceneManagerEvent event)
FURI_LOG_I(TAGTINKER_TAG, "Barcode: %s -> PLID %02X%02X%02X%02X",
app->barcode, app->plid[3], app->plid[2], app->plid[1], app->plid[0]);
/* Auto-save target */
bool exists = false;
for(uint8_t i = 0; i < app->target_count; i++) {
if(strcmp(app->targets[i].barcode, app->barcode) == 0) {
exists = true;
app->selected_target = i;
break;
}
}
if(!exists && app->target_count < TAGTINKER_MAX_TARGETS) {
TagTinkerTarget* t = &app->targets[app->target_count];
memcpy(t->barcode, app->barcode, TAGTINKER_BC_LEN + 1);
memcpy(t->plid, app->plid, 4);
char suffix[7];
memcpy(suffix, app->barcode + TAGTINKER_BC_LEN - 6, 6);
suffix[6] = '\0';
snprintf(t->name, TAGTINKER_TARGET_NAME_LEN, "Tag ...%s", suffix);
tagtinker_target_refresh_profile(t);
app->selected_target = app->target_count;
app->target_count++;
if(!tagtinker_targets_save(app)) {
FURI_LOG_W(TAGTINKER_TAG, "Failed to save targets");
}
}
app->selected_target = tagtinker_ensure_target(app, app->barcode);
if(app->selected_target >= 0) {
tagtinker_select_target(app, (uint8_t)app->selected_target);
+3 -3
View File
@@ -13,10 +13,10 @@ void tagtinker_scene_broadcast_menu_on_enter(void* ctx) {
TagTinkerApp* app = ctx;
submenu_reset(app->submenu);
submenu_set_header(app->submenu, "Broadcast Tag");
submenu_set_header(app->submenu, "Broadcast Payloads");
submenu_add_item(app->submenu, "Change Page", TagTinkerBroadcastFlipPage, broadcast_menu_cb, app);
submenu_add_item(app->submenu, "Show Debug Page", TagTinkerBroadcastDebugScreen, broadcast_menu_cb, app);
submenu_add_item(app->submenu, "Change Page", TagTinkerBroadcastFlipPage, broadcast_menu_cb, app);
submenu_add_item(app->submenu, "Diagnostic Page", TagTinkerBroadcastDebugScreen, broadcast_menu_cb, app);
submenu_set_selected_item(
app->submenu,
-104
View File
@@ -1,104 +0,0 @@
#include "../tagtinker_app.h"
#include <dialogs/dialogs.h>
#include <storage/storage.h>
#include <gui/canvas.h>
static void show_error_dialog(TagTinkerApp* app, const char* text) {
DialogMessage* message = dialog_message_alloc();
dialog_message_set_header(message, "Load Error", 64, 0, AlignCenter, AlignTop);
dialog_message_set_text(message, text, 64, 26, AlignCenter, AlignCenter);
dialog_message_set_buttons(message, "OK", NULL, NULL);
dialog_message_show(app->dialogs, message);
dialog_message_free(message);
}
void tagtinker_scene_image_upload_on_enter(void* ctx) {
TagTinkerApp* app = ctx;
TagTinkerTarget* target = &app->targets[app->selected_target];
FuriString* file_path = furi_string_alloc();
furi_string_set(file_path, "/ext/apps_data/tagtinker");
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_simply_mkdir(storage, furi_string_get_cstr(file_path));
DialogsFileBrowserOptions browser_options;
memset(&browser_options, 0, sizeof(browser_options));
dialog_file_browser_set_basic_options(&browser_options, ".bmp", NULL);
browser_options.hide_dot_files = true;
if(dialog_file_browser_show(app->dialogs, file_path, file_path, &browser_options)) {
File* file = storage_file_alloc(storage);
if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
uint8_t header[54];
if(storage_file_read(file, header, sizeof(header)) == sizeof(header)) {
if(header[0] == 'B' && header[1] == 'M') {
uint32_t data_offset = header[10] | (header[11] << 8) | (header[12] << 16) | (header[13] << 24);
int32_t bmp_w = header[18] | (header[19] << 8) | (header[20] << 16) | (header[21] << 24);
int32_t bmp_h = header[22] | (header[23] << 8) | (header[24] << 16) | (header[25] << 24);
uint16_t bpp = header[28] | (header[29] << 8);
bool top_down = false;
if(bmp_h < 0) {
top_down = true;
bmp_h = -bmp_h;
}
if(bpp == 1) {
size_t w = (size_t)bmp_w;
size_t h = (size_t)bmp_h;
UNUSED(data_offset);
UNUSED(top_down);
tagtinker_prepare_bmp_tx(
app,
target->plid,
furi_string_get_cstr(file_path),
(uint16_t)w,
(uint16_t)h,
app->img_page);
scene_manager_next_scene(app->scene_manager, TagTinkerSceneImageOptions);
} else if(bpp == 24 || bpp == 32) {
size_t w = (size_t)bmp_w;
size_t h = (size_t)bmp_h;
UNUSED(data_offset);
UNUSED(top_down);
tagtinker_prepare_bmp_tx(
app,
target->plid,
furi_string_get_cstr(file_path),
(uint16_t)w,
(uint16_t)h,
app->img_page);
scene_manager_next_scene(app->scene_manager, TagTinkerSceneImageOptions);
} else {
show_error_dialog(app, "Use 1/24/32-bit BMP");
}
} else {
show_error_dialog(app, "Invalid BMP magic");
}
} else {
show_error_dialog(app, "File too small");
}
storage_file_close(file);
} else {
show_error_dialog(app, "Could not open file");
}
storage_file_free(file);
} else {
/* User cancelled browser */
scene_manager_previous_scene(app->scene_manager);
}
furi_record_close(RECORD_STORAGE);
furi_string_free(file_path);
}
bool tagtinker_scene_image_upload_on_event(void* ctx, SceneManagerEvent event) {
UNUSED(ctx);
UNUSED(event);
return false;
}
void tagtinker_scene_image_upload_on_exit(void* ctx) {
UNUSED(ctx);
}
+3 -3
View File
@@ -23,10 +23,10 @@ void tagtinker_scene_main_menu_on_enter(void* ctx) {
submenu_reset(app->submenu);
submenu_set_header(app->submenu, TAGTINKER_DISPLAY_NAME " v" TAGTINKER_VERSION);
submenu_add_item(app->submenu, "Broadcast Tag", MainMenuBroadcast, main_menu_cb, app);
submenu_add_item(app->submenu, "Target Tag", MainMenuTargetESL, main_menu_cb, app);
submenu_add_item(app->submenu, "Broadcast Payloads", MainMenuBroadcast, main_menu_cb, app);
submenu_add_item(app->submenu, "Targeted Payloads", MainMenuTargetESL, main_menu_cb, app);
submenu_add_item(app->submenu, "Phone Sync", MainMenuAndroid, main_menu_cb, app);
submenu_add_item(app->submenu, "Settings", MainMenuSettings, main_menu_cb, app);
submenu_add_item(app->submenu, "Android App", MainMenuAndroid, main_menu_cb, app);
submenu_add_item(app->submenu, "About", MainMenuAbout, main_menu_cb, app);
submenu_set_selected_item(
+1 -11
View File
@@ -1,15 +1,11 @@
/*
* Preset List — first screen after "Push Text".
*
* Shows saved presets (click = instant transmit with saved text + settings).
* "[+] Add New Preset" goes to text input → size picker → save.
* Text preset list.
*/
#include "../tagtinker_app.h"
#define EVT_ADD_NEW 200
#define EVT_PRESET 0
/* Load presets from SD card */
static void presets_load(TagTinkerApp* app) {
app->preset_count = 0;
@@ -57,7 +53,6 @@ static void preset_list_cb(void* ctx, uint32_t index) {
view_dispatcher_send_custom_event(app->view_dispatcher, index);
}
/* Static label storage */
static char preset_labels[TAGTINKER_MAX_PRESETS][48];
void tagtinker_scene_preset_list_on_enter(void* ctx) {
@@ -68,11 +63,9 @@ void tagtinker_scene_preset_list_on_enter(void* ctx) {
submenu_reset(app->submenu);
submenu_set_header(app->submenu, "Text Presets");
/* Add New Preset option first */
submenu_add_item(app->submenu, "[+] New Preset",
EVT_ADD_NEW, preset_list_cb, app);
/* Saved presets */
for(uint8_t i = 0; i < app->preset_count; i++) {
snprintf(preset_labels[i], sizeof(preset_labels[i]),
"%ux%u \"%s\"",
@@ -91,17 +84,14 @@ bool tagtinker_scene_preset_list_on_event(void* ctx, SceneManagerEvent event) {
if(event.type != SceneManagerEventTypeCustom) return false;
if(event.event == EVT_ADD_NEW) {
/* Clear text buffer and go to text input */
memset(app->text_input_buf, 0, sizeof(app->text_input_buf));
scene_manager_set_scene_state(app->scene_manager, TagTinkerSceneTextInput, 0);
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTextInput);
return true;
}
/* Preset selected — load text + settings and transmit */
uint32_t idx = event.event - EVT_PRESET;
if(idx < app->preset_count) {
/* Load preset into app state */
app->esl_width = app->presets[idx].width;
app->esl_height = app->presets[idx].height;
app->img_page = app->presets[idx].page;
+1 -6
View File
@@ -1,5 +1,5 @@
/*
* Size Picker — exact native sizes plus a useful custom range.
* Size picker scene.
*/
#include "../tagtinker_app.h"
@@ -31,7 +31,6 @@ static const char* compression_labels[] = {"Auto", "Raw", "RLE"};
#define H_COUNT COUNT_OF(height_values)
#define COORD_COUNT COUNT_OF(coord_values)
/* Setting indices */
enum {
SettingWidth,
SettingHeight,
@@ -46,8 +45,6 @@ enum {
SettingTransmit,
};
/* ── Callbacks ── */
static void clamp_current_offsets(TagTinkerApp* app);
static void width_changed(VariableItem* item) {
@@ -236,8 +233,6 @@ static void setting_cb(void* ctx, uint32_t index) {
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit);
}
/* ── Scene handlers ── */
void tagtinker_scene_size_picker_on_enter(void* ctx) {
TagTinkerApp* app = ctx;
+155
View File
@@ -0,0 +1,155 @@
#include "../tagtinker_app.h"
#define EVT_SYNCED_IMAGE_BASE 300
static uint8_t synced_image_menu_map[TAGTINKER_MAX_SYNCED_IMAGES];
static char synced_image_labels[TAGTINKER_MAX_SYNCED_IMAGES][48];
static void synced_image_list_cb(void* ctx, uint32_t index) {
TagTinkerApp* app = ctx;
view_dispatcher_send_custom_event(app->view_dispatcher, index);
}
static char* synced_image_next_token(char** cursor) {
if(!cursor || !*cursor) return NULL;
char* token = *cursor;
char* sep = strchr(token, '|');
if(sep) {
*sep = '\0';
*cursor = sep + 1;
} else {
*cursor = NULL;
}
return token;
}
static void synced_images_load(TagTinkerApp* app) {
app->synced_image_count = 0;
if(app->selected_target < 0 || app->selected_target >= app->target_count) return;
Storage* storage = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(storage);
if(storage_file_open(file, APP_DATA_PATH("synced_images.txt"), FSAM_READ, FSOM_OPEN_EXISTING)) {
uint64_t size = storage_file_size(file);
if(size > 0 && size < 8192U) {
char* buf = malloc((size_t)size + 1U);
if(buf) {
uint16_t read = storage_file_read(file, buf, (uint16_t)size);
buf[read] = '\0';
char* line = buf;
while(line && *line && app->synced_image_count < TAGTINKER_MAX_SYNCED_IMAGES) {
char* nl = strchr(line, '\n');
if(nl) *nl = '\0';
if(*line) {
char* cursor = line;
char* job_id = synced_image_next_token(&cursor);
char* barcode = synced_image_next_token(&cursor);
char* width = synced_image_next_token(&cursor);
char* height = synced_image_next_token(&cursor);
char* page = synced_image_next_token(&cursor);
char* path = synced_image_next_token(&cursor);
if(job_id && barcode && width && height && page && path &&
strcmp(barcode, app->targets[app->selected_target].barcode) == 0 &&
storage_common_exists(storage, path)) {
TagTinkerSyncedImage* image =
&app->synced_images[app->synced_image_count++];
strncpy(image->job_id, job_id, TAGTINKER_SYNC_JOB_ID_LEN);
image->job_id[TAGTINKER_SYNC_JOB_ID_LEN] = '\0';
strncpy(image->barcode, barcode, TAGTINKER_BC_LEN);
image->barcode[TAGTINKER_BC_LEN] = '\0';
image->width = (uint16_t)atoi(width);
image->height = (uint16_t)atoi(height);
image->page = (uint8_t)atoi(page);
strncpy(image->image_path, path, TAGTINKER_IMAGE_PATH_LEN);
image->image_path[TAGTINKER_IMAGE_PATH_LEN] = '\0';
}
}
line = nl ? (nl + 1) : NULL;
}
free(buf);
}
}
storage_file_close(file);
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
}
void tagtinker_scene_synced_image_list_on_enter(void* ctx) {
TagTinkerApp* app = ctx;
synced_images_load(app);
submenu_reset(app->submenu);
submenu_set_header(app->submenu, "Uploads");
if(app->synced_image_count == 0) {
submenu_add_item(app->submenu, "No synced images", 0, synced_image_list_cb, app);
} else {
uint8_t menu_idx = 0;
for(int16_t i = (int16_t)app->synced_image_count - 1; i >= 0; i--) {
const TagTinkerSyncedImage* image = &app->synced_images[i];
const char* suffix = image->job_id;
size_t suffix_len = strlen(image->job_id);
if(suffix_len > 6U) suffix += suffix_len - 6U;
snprintf(
synced_image_labels[menu_idx],
sizeof(synced_image_labels[menu_idx]),
"P%u %ux%u #%s",
image->page,
image->width,
image->height,
suffix);
synced_image_menu_map[menu_idx] = (uint8_t)i;
submenu_add_item(
app->submenu,
synced_image_labels[menu_idx],
EVT_SYNCED_IMAGE_BASE + menu_idx,
synced_image_list_cb,
app);
menu_idx++;
}
}
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewSubmenu);
}
bool tagtinker_scene_synced_image_list_on_event(void* ctx, SceneManagerEvent event) {
TagTinkerApp* app = ctx;
if(event.type != SceneManagerEventTypeCustom) return false;
if(event.event < EVT_SYNCED_IMAGE_BASE) return true;
uint32_t menu_idx = event.event - EVT_SYNCED_IMAGE_BASE;
if(menu_idx >= app->synced_image_count) return true;
if(app->selected_target < 0 || app->selected_target >= app->target_count) return true;
TagTinkerSyncedImage* image = &app->synced_images[synced_image_menu_map[menu_idx]];
TagTinkerTarget* target = &app->targets[app->selected_target];
app->img_page = image->page;
app->draw_x = 0;
app->draw_y = 0;
app->color_clear = false;
tagtinker_prepare_bmp_tx(
app, target->plid, image->image_path, image->width, image->height, image->page);
scene_manager_next_scene(app->scene_manager, TagTinkerSceneImageOptions);
return true;
}
void tagtinker_scene_synced_image_list_on_exit(void* ctx) {
TagTinkerApp* app = ctx;
submenu_reset(app->submenu);
}
+79 -21
View File
@@ -1,5 +1,5 @@
/*
* Target Actions what to do with a selected ESL
* Target actions scene.
*/
#include "../tagtinker_app.h"
@@ -10,6 +10,29 @@ static void target_actions_cb(void* ctx, uint32_t index) {
view_dispatcher_send_custom_event(app->view_dispatcher, index);
}
static bool confirm_target_action(TagTinkerApp* app, const char* header, const char* body, const char* action) {
if(!app || !header || !body || !action) return false;
DialogMessage* message = dialog_message_alloc();
dialog_message_set_header(message, header, 64, 2, AlignCenter, AlignTop);
dialog_message_set_text(message, body, 64, 18, AlignCenter, AlignTop);
dialog_message_set_buttons(message, "Back", NULL, action);
DialogMessageButton button = dialog_message_show(app->dialogs, message);
dialog_message_free(message);
return button == DialogMessageButtonRight;
}
static void show_target_action_result(TagTinkerApp* app, const char* header, const char* body) {
if(!app || !header || !body) return;
DialogMessage* message = dialog_message_alloc();
dialog_message_set_header(message, header, 64, 2, AlignCenter, AlignTop);
dialog_message_set_text(message, body, 64, 18, AlignCenter, AlignTop);
dialog_message_set_buttons(message, "OK", NULL, NULL);
dialog_message_show(app->dialogs, message);
dialog_message_free(message);
}
static void show_target_details(TagTinkerApp* app, const TagTinkerTarget* target) {
if(!target) return;
@@ -37,7 +60,7 @@ static void show_target_details(TagTinkerApp* app, const TagTinkerTarget* target
}
DialogMessage* message = dialog_message_alloc();
dialog_message_set_header(message, "Tag Details", 64, 2, AlignCenter, AlignTop);
dialog_message_set_header(message, "Tag Info", 64, 2, AlignCenter, AlignTop);
dialog_message_set_text(message, body, 64, 18, AlignCenter, AlignTop);
dialog_message_set_buttons(message, "OK", NULL, NULL);
dialog_message_show(app->dialogs, message);
@@ -52,16 +75,24 @@ void tagtinker_scene_target_actions_on_enter(void* ctx) {
submenu_reset(app->submenu);
char header[24];
snprintf(header, sizeof(header), "Tag: %.8s...", app->barcode);
snprintf(
header,
sizeof(header),
"%s",
(target && target->name[0]) ? target->name : "Target");
submenu_set_header(app->submenu, header);
submenu_add_item(app->submenu, "Tag Details", TagTinkerTargetDetails, target_actions_cb, app);
submenu_add_item(app->submenu, "Show Tag Info", TagTinkerTargetDetails, target_actions_cb, app);
submenu_add_item(app->submenu, "Rename Tag", TagTinkerTargetRename, target_actions_cb, app);
if(allow_graphics) {
submenu_add_item(app->submenu, "Show Text Preset", TagTinkerTargetPushText, target_actions_cb, app);
submenu_add_item(app->submenu, "Show Custom Image", TagTinkerTargetPushImage, target_actions_cb, app);
submenu_add_item(app->submenu, "Set Text", TagTinkerTargetPushText, target_actions_cb, app);
submenu_add_item(app->submenu, "Set Image", TagTinkerTargetPushSyncedImage, target_actions_cb, app);
}
submenu_add_item(app->submenu, "LED Response Check", TagTinkerTargetPingFlash, target_actions_cb, app);
submenu_add_item(app->submenu, "LED Test", TagTinkerTargetPingFlash, target_actions_cb, app);
submenu_add_item(
app->submenu, "Delete Saved Images", TagTinkerTargetDeleteSyncedImages, target_actions_cb, app);
submenu_add_item(app->submenu, "Delete Tag", TagTinkerTargetDeleteTag, target_actions_cb, app);
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewSubmenu);
}
@@ -74,47 +105,74 @@ bool tagtinker_scene_target_actions_on_event(void* ctx, SceneManagerEvent event)
case TagTinkerTargetDetails:
show_target_details(app, &app->targets[app->selected_target]);
return true;
case TagTinkerTargetRename:
scene_manager_set_scene_state(
app->scene_manager, TagTinkerSceneTextInput, TagTinkerTextInputRenameTarget);
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTextInput);
return true;
case TagTinkerTargetPushText:
if(!tagtinker_target_supports_graphics(&app->targets[app->selected_target])) return true;
scene_manager_next_scene(app->scene_manager, TagTinkerScenePresetList);
return true;
case TagTinkerTargetPushImage:
case TagTinkerTargetPushSyncedImage:
if(!tagtinker_target_supports_graphics(&app->targets[app->selected_target])) return true;
scene_manager_next_scene(app->scene_manager, TagTinkerSceneImageUpload);
scene_manager_next_scene(app->scene_manager, TagTinkerSceneSyncedImageList);
return true;
case TagTinkerTargetDeleteSyncedImages:
{
TagTinkerTarget* target = &app->targets[app->selected_target];
char body[96];
snprintf(body, sizeof(body), "Remove saved images for\n%s?", target->name);
if(!confirm_target_action(app, "Delete Images", body, "Delete")) {
return true;
}
size_t removed = tagtinker_delete_synced_images_for_barcode(app, target->barcode);
char result[96];
snprintf(result, sizeof(result), "Removed %u saved image%s", (unsigned)removed, removed == 1U ? "" : "s");
show_target_action_result(app, "Delete Images", result);
}
return true;
case TagTinkerTargetPingFlash:
{
TagTinkerTarget* target = &app->targets[app->selected_target];
/* The blink command actually requires two frames:
* 1. A wake ping (lots of repeats)
* 2. The green LED command payload (0x06 0xC9 0x00 0x00 0x00 0x00)
*/
app->frame_seq_count = 2;
app->frame_sequence = malloc(sizeof(uint8_t*) * 2);
app->frame_lengths = malloc(sizeof(size_t) * 2);
app->frame_repeats = malloc(sizeof(uint16_t) * 2);
/* 1. Wake ping */
app->frame_sequence[0] = malloc(TAGTINKER_MAX_FRAME_SIZE);
app->frame_lengths[0] = tagtinker_make_ping_frame(app->frame_sequence[0], target->plid);
app->frame_repeats[0] = 500;
/* 2. LED command */
app->frame_sequence[1] = malloc(TAGTINKER_MAX_FRAME_SIZE);
const uint8_t blink_payload[6] = {0x06, 0xC9, 0x00, 0x00, 0x00, 0x00};
app->frame_lengths[1] = tagtinker_make_addressed_frame(
app->frame_sequence[1], target->plid, blink_payload, 6);
app->frame_repeats[1] = 100;
/* Put the first frame in the preview buffer */
memcpy(app->frame_buf, app->frame_sequence[0], app->frame_lengths[0]);
app->frame_len = app->frame_lengths[0];
app->tx_spam = false;
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit);
}
return true;
case TagTinkerTargetDeleteTag:
{
TagTinkerTarget* target = &app->targets[app->selected_target];
char body[96];
snprintf(body, sizeof(body), "Delete %s and its\nsaved images?", target->name);
if(!confirm_target_action(app, "Delete Tag", body, "Delete")) {
return true;
}
tagtinker_delete_target(app, (uint8_t)app->selected_target);
scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, TagTinkerSceneTargetMenu);
}
return true;
}
return false;
}
+2 -2
View File
@@ -17,10 +17,10 @@ void tagtinker_scene_target_menu_on_enter(void* ctx) {
TagTinkerApp* app = ctx;
submenu_reset(app->submenu);
submenu_set_header(app->submenu, "Target Tag");
submenu_set_header(app->submenu, "Targeted Payloads");
/* Add new target */
submenu_add_item(app->submenu, "+ Add Tag", TargetMenuAddNew, target_menu_cb, app);
submenu_add_item(app->submenu, "+ New Target", TargetMenuAddNew, target_menu_cb, app);
/* List saved targets */
for(uint8_t i = 0; i < app->target_count; i++) {
+49 -9
View File
@@ -1,7 +1,5 @@
/*
* TagTinker Text Input Scene
*
* Prompts user for a text string, then goes to Size Picker.
* Text input scene.
*/
#include "../tagtinker_app.h"
@@ -11,24 +9,47 @@ static void text_input_done_cb(void* ctx) {
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
}
static void text_input_sanitize_name(char* value) {
if(!value) return;
for(char* p = value; *p; p++) {
if(*p == '|' || *p == '\r' || *p == '\n') {
*p = ' ';
}
}
}
void tagtinker_scene_text_input_on_enter(void* ctx) {
TagTinkerApp* app = ctx;
bool clear = scene_manager_get_scene_state(app->scene_manager, TagTinkerSceneTextInput) == 0;
if(clear) {
uint32_t mode = scene_manager_get_scene_state(app->scene_manager, TagTinkerSceneTextInput);
bool rename_target = mode == TagTinkerTextInputRenameTarget;
bool clear = mode == TagTinkerTextInputNewText;
if(rename_target) {
if(app->selected_target >= 0 && app->selected_target < app->target_count) {
strncpy(
app->text_input_buf,
app->targets[app->selected_target].name,
sizeof(app->text_input_buf) - 1U);
app->text_input_buf[sizeof(app->text_input_buf) - 1U] = '\0';
} else {
memset(app->text_input_buf, 0, sizeof(app->text_input_buf));
}
} else if(clear) {
memset(app->text_input_buf, 0, sizeof(app->text_input_buf));
scene_manager_set_scene_state(app->scene_manager, TagTinkerSceneTextInput, 1);
scene_manager_set_scene_state(
app->scene_manager, TagTinkerSceneTextInput, TagTinkerTextInputKeepText);
}
text_input_reset(app->text_input);
text_input_set_header_text(app->text_input, "Text to display:");
text_input_set_header_text(app->text_input, rename_target ? "Target name:" : "Text to display:");
text_input_set_result_callback(
app->text_input,
text_input_done_cb,
app,
app->text_input_buf,
sizeof(app->text_input_buf),
clear);
clear && !rename_target);
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewTextInput);
}
@@ -37,6 +58,25 @@ bool tagtinker_scene_text_input_on_event(void* ctx, SceneManagerEvent event) {
TagTinkerApp* app = ctx;
if(event.type != SceneManagerEventTypeCustom) return false;
uint32_t mode = scene_manager_get_scene_state(app->scene_manager, TagTinkerSceneTextInput);
if(mode == TagTinkerTextInputRenameTarget) {
if(app->selected_target >= 0 && app->selected_target < app->target_count) {
TagTinkerTarget* target = &app->targets[app->selected_target];
text_input_sanitize_name(app->text_input_buf);
if(strlen(app->text_input_buf) == 0U) {
tagtinker_target_set_default_name(target);
} else {
strncpy(target->name, app->text_input_buf, TAGTINKER_TARGET_NAME_LEN);
target->name[TAGTINKER_TARGET_NAME_LEN] = '\0';
}
tagtinker_targets_save(app);
}
scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, TagTinkerSceneTargetActions);
return true;
}
if(strlen(app->text_input_buf) == 0) {
scene_manager_search_and_switch_to_previous_scene(
app->scene_manager, TagTinkerSceneTargetActions);
+231 -4
View File
@@ -59,6 +59,231 @@ void tagtinker_target_refresh_profile(TagTinkerTarget* target) {
tagtinker_barcode_to_profile(target->barcode, &target->profile);
}
void tagtinker_target_set_default_name(TagTinkerTarget* target) {
if(!target) return;
if(strlen(target->barcode) < 6U) {
snprintf(target->name, sizeof(target->name), "Tag");
return;
}
char suffix[7];
memcpy(suffix, target->barcode + TAGTINKER_BC_LEN - 6, 6);
suffix[6] = '\0';
snprintf(target->name, sizeof(target->name), "Tag ...%s", suffix);
}
int8_t tagtinker_find_target_by_barcode(const TagTinkerApp* app, const char* barcode) {
if(!app || !barcode || !*barcode) return -1;
for(uint8_t i = 0; i < app->target_count; i++) {
if(strcmp(app->targets[i].barcode, barcode) == 0) {
return (int8_t)i;
}
}
return -1;
}
int8_t tagtinker_ensure_target(TagTinkerApp* app, const char* barcode) {
if(!app || !barcode) return -1;
int8_t existing = tagtinker_find_target_by_barcode(app, barcode);
if(existing >= 0) return existing;
if(app->target_count >= TAGTINKER_MAX_TARGETS) return -1;
TagTinkerTarget* target = &app->targets[app->target_count];
memset(target, 0, sizeof(*target));
strncpy(target->barcode, barcode, TAGTINKER_BC_LEN);
target->barcode[TAGTINKER_BC_LEN] = '\0';
if(!tagtinker_barcode_to_plid(target->barcode, target->plid)) {
memset(target, 0, sizeof(*target));
return -1;
}
tagtinker_target_set_default_name(target);
tagtinker_target_refresh_profile(target);
app->target_count++;
tagtinker_targets_save(app);
return (int8_t)(app->target_count - 1U);
}
bool tagtinker_find_latest_synced_image(
const TagTinkerApp* app,
const char* barcode,
TagTinkerSyncedImage* image) {
if(!app || !barcode || !*barcode || !image) return false;
bool found = false;
Storage* storage = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(storage);
if(storage_file_open(file, APP_DATA_PATH("synced_images.txt"), FSAM_READ, FSOM_OPEN_EXISTING)) {
uint64_t size = storage_file_size(file);
if(size > 0U && size < 8192U) {
char* buf = malloc((size_t)size + 1U);
if(buf) {
uint16_t read = storage_file_read(file, buf, (uint16_t)size);
buf[read] = '\0';
char* line = buf;
while(line && *line) {
char* nl = strchr(line, '\n');
if(nl) *nl = '\0';
if(*line) {
char* cursor = line;
char* job_id = cursor;
char* current_barcode = strchr(cursor, '|');
if(current_barcode) *current_barcode++ = '\0';
char* width = current_barcode ? strchr(current_barcode, '|') : NULL;
if(width) *width++ = '\0';
char* height = width ? strchr(width, '|') : NULL;
if(height) *height++ = '\0';
char* page = height ? strchr(height, '|') : NULL;
if(page) *page++ = '\0';
char* path = page ? strchr(page, '|') : NULL;
if(path) *path++ = '\0';
if(job_id && current_barcode && width && height && page && path &&
strcmp(current_barcode, barcode) == 0 && storage_common_exists(storage, path)) {
strncpy(image->job_id, job_id, TAGTINKER_SYNC_JOB_ID_LEN);
image->job_id[TAGTINKER_SYNC_JOB_ID_LEN] = '\0';
strncpy(image->barcode, current_barcode, TAGTINKER_BC_LEN);
image->barcode[TAGTINKER_BC_LEN] = '\0';
image->width = (uint16_t)atoi(width);
image->height = (uint16_t)atoi(height);
image->page = (uint8_t)atoi(page);
strncpy(image->image_path, path, TAGTINKER_IMAGE_PATH_LEN);
image->image_path[TAGTINKER_IMAGE_PATH_LEN] = '\0';
found = true;
}
}
line = nl ? (nl + 1) : NULL;
}
free(buf);
}
}
storage_file_close(file);
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
return found;
}
size_t tagtinker_delete_synced_images_for_barcode(TagTinkerApp* app, const char* barcode) {
UNUSED(app);
if(!barcode || !*barcode) return 0U;
Storage* storage = furi_record_open(RECORD_STORAGE);
File* file = storage_file_alloc(storage);
size_t removed_count = 0U;
if(storage_file_open(file, APP_DATA_PATH("synced_images.txt"), FSAM_READ, FSOM_OPEN_EXISTING)) {
uint64_t size = storage_file_size(file);
if(size > 0U && size < 8192U) {
char* input = malloc((size_t)size + 1U);
char* output = malloc((size_t)size + 1U);
if(input && output) {
uint16_t read = storage_file_read(file, input, (uint16_t)size);
input[read] = '\0';
output[0] = '\0';
size_t output_len = 0U;
char* line = input;
while(line && *line) {
char* nl = strchr(line, '\n');
if(nl) *nl = '\0';
if(*line) {
char line_copy[384];
snprintf(line_copy, sizeof(line_copy), "%s", line);
char* cursor = line;
char* job_id = cursor;
char* current_barcode = strchr(cursor, '|');
if(current_barcode) *current_barcode++ = '\0';
char* width = current_barcode ? strchr(current_barcode, '|') : NULL;
if(width) *width++ = '\0';
char* height = width ? strchr(width, '|') : NULL;
if(height) *height++ = '\0';
char* page = height ? strchr(height, '|') : NULL;
if(page) *page++ = '\0';
char* path = page ? strchr(page, '|') : NULL;
if(path) *path++ = '\0';
bool matches =
job_id && current_barcode && width && height && page && path &&
strcmp(current_barcode, barcode) == 0;
if(matches) {
storage_common_remove(storage, path);
removed_count++;
} else {
size_t line_len = strlen(line_copy);
if((output_len + line_len + 1U) < ((size_t)size + 1U)) {
memcpy(output + output_len, line_copy, line_len);
output_len += line_len;
output[output_len++] = '\n';
output[output_len] = '\0';
}
}
}
line = nl ? (nl + 1) : NULL;
}
storage_file_close(file);
if(output_len == 0U) {
storage_common_remove(storage, APP_DATA_PATH("synced_images.txt"));
} else if(
storage_file_open(
file, APP_DATA_PATH("synced_images.txt"), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
storage_file_write(file, output, (uint16_t)output_len);
storage_file_close(file);
}
} else {
storage_file_close(file);
}
free(output);
free(input);
} else {
storage_file_close(file);
}
}
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
return removed_count;
}
bool tagtinker_delete_target(TagTinkerApp* app, uint8_t index) {
if(!app || index >= app->target_count) return false;
tagtinker_delete_synced_images_for_barcode(app, app->targets[index].barcode);
if(index + 1U < app->target_count) {
memmove(
&app->targets[index],
&app->targets[index + 1U],
sizeof(TagTinkerTarget) * (size_t)(app->target_count - index - 1U));
}
memset(&app->targets[app->target_count - 1U], 0, sizeof(TagTinkerTarget));
app->target_count--;
app->selected_target = -1;
app->barcode[0] = '\0';
memset(app->plid, 0, sizeof(app->plid));
app->barcode_valid = false;
return tagtinker_targets_save(app);
}
bool tagtinker_target_supports_graphics(const TagTinkerTarget* target) {
if(!target) return false;
@@ -299,10 +524,7 @@ void tagtinker_targets_load(TagTinkerApp* app) {
strncpy(target->name, sep + 1, TAGTINKER_TARGET_NAME_LEN);
target->name[TAGTINKER_TARGET_NAME_LEN] = '\0';
} else {
char suffix[7];
memcpy(suffix, target->barcode + TAGTINKER_BC_LEN - 6, 6);
suffix[6] = '\0';
snprintf(target->name, TAGTINKER_TARGET_NAME_LEN + 1, "Tag ...%s", suffix);
tagtinker_target_set_default_name(target);
}
tagtinker_target_refresh_profile(target);
@@ -367,6 +589,7 @@ static TagTinkerApp* app_alloc(void) {
app->invert_text = false;
strcpy(app->text_input_buf, "TagTinker");
app->selected_target = -1;
app->ble_sync_ready_target = -1;
tagtinker_settings_load(app);
tagtinker_targets_load(app);
@@ -386,6 +609,7 @@ static TagTinkerApp* app_alloc(void) {
/* Notifications */
app->notifications = furi_record_open(RECORD_NOTIFICATION);
app->bt = furi_record_open(RECORD_BT);
/* Views */
app->submenu = submenu_alloc();
@@ -465,6 +689,9 @@ static void app_free(TagTinkerApp* app) {
furi_record_close(RECORD_GUI);
furi_record_close(RECORD_NOTIFICATION);
furi_record_close(RECORD_DIALOGS);
if(app->bt) {
furi_record_close(RECORD_BT);
}
view_free(app->warning_view);
view_free(app->transmit_view);
+100 -42
View File
@@ -1,5 +1,5 @@
/*
* TagTinker App State
* App state.
*/
#pragma once
@@ -17,6 +17,8 @@
#include <dialogs/dialogs.h>
#include <notification/notification_messages.h>
#include <storage/storage.h>
#include <bt/bt_service/bt.h>
#include <targets/f7/ble_glue/profiles/serial_profile.h>
#include "views/numlock_input.h"
@@ -26,39 +28,19 @@
#define TAGTINKER_TAG "TagTinker"
#define TAGTINKER_DISPLAY_NAME "TagTinker"
#define TAGTINKER_VERSION "1.1"
#define TAGTINKER_VERSION "1.3"
#define TAGTINKER_BC_LEN 17
#define TAGTINKER_HEX_LEN 64
#define TAGTINKER_MAX_TARGETS 16
#define TAGTINKER_TARGET_NAME_LEN 16
#define TAGTINKER_MAX_PRESETS 6
#define TAGTINKER_MAX_SYNCED_IMAGES 24
#define TAGTINKER_PRESET_TEXT_LEN 32
#define TAGTINKER_IMAGE_PATH_LEN 255
/* Views */
typedef enum {
TagTinkerViewWarning,
TagTinkerViewSubmenu,
TagTinkerViewVarItemList,
TagTinkerViewTextInput,
TagTinkerViewPopup,
TagTinkerViewWidget,
TagTinkerViewNumlock,
TagTinkerViewTargetActions,
TagTinkerViewTransmit,
TagTinkerViewAbout,
} TagTinkerView;
/* Saved ESL target */
typedef struct {
char name[TAGTINKER_TARGET_NAME_LEN + 1];
char barcode[TAGTINKER_BC_LEN + 1];
uint8_t plid[4];
TagTinkerTagProfile profile;
} TagTinkerTarget;
#define TAGTINKER_SYNC_JOB_ID_LEN 32
typedef enum {
TagTinkerTxModeDirect = 0,
TagTinkerTxModeNone = 0,
TagTinkerTxModeTextImage,
TagTinkerTxModeBmpImage,
} TagTinkerTxMode;
@@ -79,6 +61,43 @@ typedef struct {
char image_path[TAGTINKER_IMAGE_PATH_LEN + 1];
} TagTinkerImageTxJob;
typedef struct {
char job_id[TAGTINKER_SYNC_JOB_ID_LEN + 1];
char barcode[TAGTINKER_BC_LEN + 1];
uint16_t width;
uint16_t height;
uint8_t page;
char image_path[TAGTINKER_IMAGE_PATH_LEN + 1];
} TagTinkerSyncedImage;
typedef enum {
TagTinkerTextInputNewText = 0,
TagTinkerTextInputKeepText = 1,
TagTinkerTextInputRenameTarget = 2,
} TagTinkerTextInputMode;
/* Views */
typedef enum {
TagTinkerViewSubmenu,
TagTinkerViewVarItemList,
TagTinkerViewTextInput,
TagTinkerViewPopup,
TagTinkerViewWidget,
TagTinkerViewNumlock,
TagTinkerViewTargetActions,
TagTinkerViewWarning,
TagTinkerViewTransmit,
TagTinkerViewAbout,
} TagTinkerView;
/* Saved ESL target */
typedef struct {
char name[TAGTINKER_TARGET_NAME_LEN + 1];
char barcode[TAGTINKER_BC_LEN + 1];
uint8_t plid[4];
TagTinkerTagProfile profile;
} TagTinkerTarget;
struct TagTinkerApp {
/* GUI */
Gui* gui;
@@ -94,8 +113,8 @@ struct TagTinkerApp {
Popup* popup;
Widget* widget;
NumlockInput* numlock;
View* warning_view;
View* target_actions_view;
View* warning_view;
View* transmit_view;
View* about_view;
bool warning_view_allocated;
@@ -113,10 +132,6 @@ struct TagTinkerApp {
uint16_t repeats;
bool forever;
bool tx_spam;
bool show_startup_warning;
TagTinkerSignalMode signal_mode;
TagTinkerCompressionMode compression_mode;
uint8_t data_frame_repeats;
/* Current target */
char barcode[TAGTINKER_BC_LEN + 1];
@@ -158,24 +173,54 @@ struct TagTinkerApp {
} presets[TAGTINKER_MAX_PRESETS];
uint8_t preset_count;
TagTinkerSyncedImage synced_images[TAGTINKER_MAX_SYNCED_IMAGES];
uint8_t synced_image_count;
/* Image settings */
uint8_t img_page;
uint16_t draw_x;
uint16_t draw_y;
uint16_t draw_width;
uint16_t draw_height;
TagTinkerImageTxJob image_tx_job;
TagTinkerCompressionMode compression_mode;
uint8_t data_frame_repeats;
TagTinkerSignalMode signal_mode;
bool show_startup_warning;
/* Indicates which mode triggered raw cmd (0=broadcast, 1=targeted) */
uint8_t raw_mode;
/* Chunked image/text TX */
TagTinkerImageTxJob image_tx_job;
/* Android BLE sync state */
Bt* bt;
FuriHalBleProfileBase* ble_serial;
BtStatus ble_status;
bool ble_sync_active;
uint16_t ble_synced_lines;
char ble_status_text[32];
char ble_rx_line[1024];
char ble_rx_pending_line[1024];
uint16_t ble_rx_len;
bool ble_rx_pending_ready;
bool ble_sync_job_active;
char ble_sync_job_id[TAGTINKER_SYNC_JOB_ID_LEN + 1];
char ble_sync_barcode[TAGTINKER_BC_LEN + 1];
char ble_sync_temp_path[TAGTINKER_IMAGE_PATH_LEN + 1];
char ble_sync_final_path[TAGTINKER_IMAGE_PATH_LEN + 1];
char ble_sync_last_job_id[TAGTINKER_SYNC_JOB_ID_LEN + 1];
uint32_t ble_sync_expected_bytes;
uint32_t ble_sync_received_bytes;
uint16_t ble_sync_last_chunk;
uint16_t ble_sync_last_completed_chunks;
bool ble_sync_compact_protocol;
bool ble_sync_last_compact_protocol;
int8_t ble_sync_ready_target;
};
/* Main menu items */
typedef enum {
TagTinkerMenuBroadcast,
TagTinkerMenuTargetESL,
TagTinkerMenuSettings,
TagTinkerMenuAndroid,
TagTinkerMenuAbout,
} TagTinkerMainMenuItem;
@@ -188,19 +233,29 @@ typedef enum {
/* Target action items */
typedef enum {
TagTinkerTargetDetails,
TagTinkerTargetRename,
TagTinkerTargetPushText,
TagTinkerTargetPushImage,
TagTinkerTargetPushSyncedImage,
TagTinkerTargetDeleteSyncedImages,
TagTinkerTargetPingFlash,
TagTinkerTargetDeleteTag,
} TagTinkerTargetActionItem;
void tagtinker_settings_load(TagTinkerApp* app);
bool tagtinker_settings_save(const TagTinkerApp* app);
void tagtinker_targets_load(TagTinkerApp* app);
bool tagtinker_targets_save(const TagTinkerApp* app);
void tagtinker_target_refresh_profile(TagTinkerTarget* target);
void tagtinker_select_target(TagTinkerApp* app, uint8_t index);
void tagtinker_target_set_default_name(TagTinkerTarget* target);
bool tagtinker_target_supports_graphics(const TagTinkerTarget* target);
bool tagtinker_target_supports_accent(const TagTinkerTarget* target);
const char* tagtinker_profile_kind_label(TagTinkerTagKind kind);
const char* tagtinker_profile_color_label(TagTinkerTagColor color);
int8_t tagtinker_find_target_by_barcode(const TagTinkerApp* app, const char* barcode);
int8_t tagtinker_ensure_target(TagTinkerApp* app, const char* barcode);
bool tagtinker_find_latest_synced_image(
const TagTinkerApp* app,
const char* barcode,
TagTinkerSyncedImage* image);
size_t tagtinker_delete_synced_images_for_barcode(TagTinkerApp* app, const char* barcode);
bool tagtinker_delete_target(TagTinkerApp* app, uint8_t index);
void tagtinker_free_frame_sequence(TagTinkerApp* app);
uint16_t tagtinker_pick_chunk_height(uint16_t width, bool color_clear);
void tagtinker_prepare_text_tx(TagTinkerApp* app, const uint8_t plid[4]);
@@ -211,5 +266,8 @@ void tagtinker_prepare_bmp_tx(
uint16_t width,
uint16_t height,
uint8_t page);
const char* tagtinker_profile_kind_label(TagTinkerTagKind kind);
const char* tagtinker_profile_color_label(TagTinkerTagColor color);
void tagtinker_select_target(TagTinkerApp* app, uint8_t index);
void tagtinker_settings_load(TagTinkerApp* app);
bool tagtinker_settings_save(const TagTinkerApp* app);
void tagtinker_targets_load(TagTinkerApp* app);
bool tagtinker_targets_save(const TagTinkerApp* app);
+9 -27
View File
@@ -1,8 +1,5 @@
/*
* TagTinker Number Lock Barcode Input
*
* Clean centered barcode entry: 1 letter + 16 digits in groups of 4.
* UP/DOWN cycle, LEFT/RIGHT move, OK confirm, BACK cancel.
* Barcode input view.
*/
#include "numlock_input.h"
@@ -28,7 +25,7 @@ static uint8_t prefix_x(void) {
static uint8_t digit_x(uint8_t i) {
uint8_t groups = i / GROUP_SIZE;
/* Spread across horizontal: Prefix(8) + 16 chars(6) + 3 gaps(5) = 119. Left margin = 4. */
/* Keep the full code centered across the 128 px canvas. */
return 4 + 8 + i * CHAR_W + groups * GROUP_GAP;
}
@@ -36,7 +33,6 @@ static void numlock_draw(Canvas* canvas, void* model_v) {
NumlockModel* m = model_v;
canvas_clear(canvas);
/* 1. Header Bar — Inverted Tech Banner */
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(canvas, 0, 0, 128, 12);
canvas_set_color(canvas, ColorWhite);
@@ -44,17 +40,14 @@ static void numlock_draw(Canvas* canvas, void* model_v) {
canvas_draw_str_aligned(canvas, 64, 6, AlignCenter, AlignCenter, "SET BARCODE");
canvas_set_color(canvas, ColorBlack);
/* 2. Main Input Frame — Heavy industrial border */
int frame_y = 19;
int frame_h = 24;
canvas_draw_rframe(canvas, 1, frame_y, 126, frame_h, 2);
/* Inner shadow/border detail */
canvas_draw_line(canvas, 2, frame_y+1, 125, frame_y+1);
const uint8_t baseline = 36;
canvas_set_font(canvas, FontPrimary);
/* Editable letter prefix */
char prefix[2] = {m->prefix, '\0'};
if(m->cursor == 0) {
uint8_t x = prefix_x();
@@ -75,45 +68,34 @@ static void numlock_draw(Canvas* canvas, void* model_v) {
canvas_draw_str(canvas, prefix_x(), baseline, prefix);
}
/* Iterating Digits */
for(uint8_t i = 0; i < NUM_DIGITS; i++) {
uint8_t x = digit_x(i);
char ch[2] = {'0' + m->digits[i], '\0'};
if((i + 1) == m->cursor) {
/* Selected digit bounding block */
canvas_draw_box(canvas, x - 1, frame_y + 3, CHAR_W + 2, frame_h - 6);
canvas_set_color(canvas, ColorWhite);
canvas_draw_str(canvas, x, baseline, ch);
canvas_set_color(canvas, ColorBlack);
/* Sharp Navigator Arrows pointing at selection */
uint8_t cx = x + CHAR_W / 2 - 1; /* Center of char */
/* Up pointer */
uint8_t cx = x + CHAR_W / 2 - 1;
canvas_draw_line(canvas, cx, frame_y - 4, cx - 2, frame_y - 2);
canvas_draw_line(canvas, cx, frame_y - 4, cx + 2, frame_y - 2);
canvas_draw_line(canvas, cx, frame_y - 4, cx, frame_y - 1);
/* Down pointer */
canvas_draw_line(canvas, cx, frame_y - 4, cx, frame_y - 1);
canvas_draw_line(canvas, cx, frame_y + frame_h + 3, cx - 2, frame_y + frame_h + 1);
canvas_draw_line(canvas, cx, frame_y + frame_h + 3, cx + 2, frame_y + frame_h + 1);
canvas_draw_line(canvas, cx, frame_y + frame_h + 3, cx, frame_y + frame_h);
canvas_draw_line(canvas, cx, frame_y + frame_h + 3, cx, frame_y + frame_h);
} else {
/* Standard unselected digit */
canvas_draw_str(canvas, x, baseline, ch);
}
}
/* 3. Footer UI with Clean Button Callouts */
canvas_set_font(canvas, FontSecondary);
/* D-Pad Hint Icons */
canvas_draw_str(canvas, 2, 59, "<\x12\x13> Sel");
canvas_draw_str(canvas, 2, 59, "<\x12\x13> Sel");
canvas_draw_str(canvas, 45, 59, "^\x18\x19v Set");
/* Thick OK Button */
canvas_draw_rbox(canvas, 92, 48, 34, 14, 2);
canvas_set_color(canvas, ColorWhite);
canvas_draw_str_aligned(canvas, 109, 55, AlignCenter, AlignCenter, "Hold OK");
+1 -10
View File
@@ -1,14 +1,5 @@
/*
* TagTinker - number lock barcode input view
*
* Custom view for entering a 17-character ESL barcode.
* Format: Letter + 16 digits.
*
* Controls:
* UP/DOWN - cycle digit 0-9
* LEFT/RIGHT - move cursor position
* OK - confirm barcode
* BACK - cancel
* Barcode input view.
*/
#pragma once
+1 -1
View File
@@ -134,7 +134,7 @@ static inline void set_region_pixel(
* bg_val/fg_val control polarity:
* Normal: bg=1 (white), fg=0 (black)
* Inverted: bg=0 (black), fg=1 (white)
* Zero margin text fills edge to edge.
* No outer margin is added.
*/
static inline void render_text_ex(uint8_t* buf, uint16_t w, uint16_t h,
const char* text, uint8_t bg_val, uint8_t fg_val) {