mirror of
https://github.com/i12bp8/TagTinker.git
synced 2026-07-02 12:21:42 +00:00
TagTinker V1.3 Android Companion, Extra Settings, ESL Type Detection, Bug Fixes
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.gradle/
|
||||
build/
|
||||
app/build/
|
||||
local.properties
|
||||
@@ -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
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,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,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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user