Merge pull request #53 from sosnek/add-smarttag-color-1626
Deploy Image Prep to GitHub Pages / deploy (push) Canceled after 0s

Add SmartTAG Color 2.6 (type 1626) support
This commit is contained in:
I12BP8
2026-09-02 18:00:38 +02:00
committed by GitHub
9 changed files with 470 additions and 34 deletions
+56 -24
View File
@@ -53,6 +53,8 @@ static const TagTinkerProfileEntry profile_table[] = {
{1370, 296, 128, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed, "SmartTag HD L Red (2021)", 0},
{1371, 648, 480, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed, "SmartTag HD150 Red (2021)", 0},
{1510, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono, "SmartTag E5 M", 1},
{TAGTINKER_TYPE_SMARTAG_COLOR_26, TAGTINKER_COLOR26_WIRE_W, TAGTINKER_COLOR26_WIRE_H,
TagTinkerTagKindDotMatrix, TagTinkerTagColorRed, "SmartTAG Color 2.6", 0},
{1627, 296, 128, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed, "SmartTag HD L Red", 0},
{1628, 296, 128, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed, "SmartTag HD L Red", 0},
{1639, 152, 152, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed, "SmartTag HD S Red", 0},
@@ -150,6 +152,17 @@ size_t tagtinker_make_ping_frame(uint8_t* buf, const uint8_t plid[4]) {
return terminate(buf, p);
}
size_t tagtinker_make_wake_frame(uint8_t* buf, const uint8_t plid[4]) {
/* PrecIR / PriceHax wake: cmd 0x17 and 22 trailing 0x01 bytes. */
size_t p = raw_frame(buf, TAGTINKER_PROTO_DM, plid, 0x17);
buf[p++] = 0x01;
buf[p++] = 0x00;
buf[p++] = 0x00;
buf[p++] = 0x00;
for(int i = 0; i < 22; i++) buf[p++] = 0x01;
return terminate(buf, p);
}
size_t tagtinker_make_refresh_frame(uint8_t* buf, const uint8_t plid[4]) {
size_t p = mcu_frame(buf, plid, 0x01);
for(int i = 0; i < 18; i++) buf[p++] = 0x00;
@@ -212,15 +225,15 @@ static inline uint8_t plane_pixel_at(const uint8_t* p1, const uint8_t* p2, size_
return (idx < count) ? p1[idx] : p2[idx - count];
}
static size_t tagtinker_rle_planes_bit_length(const uint8_t* p1, const uint8_t* p2, size_t count) {
if(!p1) return 0;
size_t total = p2 ? (count * 2U) : count;
if(total == 0) return 0;
#define DATA_BITS_PER_FRAME (TAGTINKER_IMAGE_DATA_BYTES_PER_FRAME * 8U)
static size_t tagtinker_rle_fn_bit_length(TagTinkerPixelAtFn pixel_at, void* ctx, size_t total) {
if(!pixel_at || total == 0) return 0;
size_t bit_len = 1U;
uint8_t run_pixel = plane_pixel_at(p1, p2, count, 0);
uint8_t run_pixel = pixel_at(0, ctx);
uint32_t run_count = 1;
for(size_t i = 1; i < total; i++) {
uint8_t pix = plane_pixel_at(p1, p2, count, i);
uint8_t pix = pixel_at(i, ctx);
if(pix == run_pixel) run_count++;
else { bit_len += record_run_bit_length(run_count); run_pixel = pix; run_count = 1; }
}
@@ -228,48 +241,67 @@ static size_t tagtinker_rle_planes_bit_length(const uint8_t* p1, const uint8_t*
return bit_len;
}
static void tagtinker_pack_planes_raw(const uint8_t* p1, const uint8_t* p2, size_t count, uint8_t* out) {
size_t total = p2 ? (count * 2U) : count;
static void tagtinker_pack_fn_raw(TagTinkerPixelAtFn pixel_at, void* ctx, size_t total, uint8_t* out) {
TagTinkerBitWriter writer = {.data = out, .bit_pos = 0};
for(size_t i = 0; i < total; i++) bit_writer_append(&writer, plane_pixel_at(p1, p2, count, i));
for(size_t i = 0; i < total; i++) bit_writer_append(&writer, pixel_at(i, ctx));
}
static void tagtinker_pack_planes_rle(const uint8_t* p1, const uint8_t* p2, size_t count, uint8_t* out) {
size_t total = p2 ? (count * 2U) : count;
static void tagtinker_pack_fn_rle(TagTinkerPixelAtFn pixel_at, void* ctx, size_t total, uint8_t* out) {
if(total == 0) return;
TagTinkerBitWriter writer = {.data = out, .bit_pos = 0};
uint8_t run_pixel = plane_pixel_at(p1, p2, count, 0);
uint8_t run_pixel = pixel_at(0, ctx);
uint32_t run_count = 1;
bit_writer_append(&writer, run_pixel);
for(size_t i = 1; i < total; i++) {
uint8_t pix = plane_pixel_at(p1, p2, count, i);
uint8_t pix = pixel_at(i, ctx);
if(pix == run_pixel) run_count++;
else { bit_writer_append_run(&writer, run_count); run_pixel = pix; run_count = 1; }
}
if(run_count > 0U) bit_writer_append_run(&writer, run_count);
}
#define DATA_BITS_PER_FRAME (TAGTINKER_IMAGE_DATA_BYTES_PER_FRAME * 8U)
bool tagtinker_encode_planes_payload(
const uint8_t* p1, const uint8_t* p2, size_t count, TagTinkerCompressionMode mode, TagTinkerImagePayload* payload) {
if(!p1 || !payload) return false;
bool tagtinker_encode_fn_payload(
TagTinkerPixelAtFn pixel_at,
void* ctx,
size_t total,
TagTinkerCompressionMode mode,
TagTinkerImagePayload* payload) {
if(!pixel_at || !payload || total == 0) return false;
memset(payload, 0, sizeof(*payload));
size_t total = p2 ? (count * 2U) : count;
size_t comp_len = tagtinker_rle_planes_bit_length(p1, p2, count);
bool use_compressed = (mode == TagTinkerCompressionRle) ||
size_t comp_len = tagtinker_rle_fn_bit_length(pixel_at, ctx, total);
bool use_compressed = (mode == TagTinkerCompressionRle) ||
(mode == TagTinkerCompressionAuto && comp_len > 0U && comp_len < total);
size_t src_len = use_compressed ? comp_len : total;
size_t padded_bits = src_len + ((DATA_BITS_PER_FRAME - (src_len % DATA_BITS_PER_FRAME)) % DATA_BITS_PER_FRAME);
uint8_t* data = calloc(padded_bits / 8U, 1);
if(!data) return false;
if(use_compressed) tagtinker_pack_planes_rle(p1, p2, count, data);
else tagtinker_pack_planes_raw(p1, p2, count, data);
payload->data = data; payload->byte_count = padded_bits / 8U;
if(use_compressed) tagtinker_pack_fn_rle(pixel_at, ctx, total, data);
else tagtinker_pack_fn_raw(pixel_at, ctx, total, data);
payload->data = data;
payload->byte_count = padded_bits / 8U;
payload->comp_type = use_compressed ? 2U : 0U;
return true;
}
typedef struct {
const uint8_t* p1;
const uint8_t* p2;
size_t count;
} TagTinkerPlaneCtx;
static uint8_t tagtinker_plane_pixel_cb(size_t idx, void* ctx) {
const TagTinkerPlaneCtx* c = ctx;
return plane_pixel_at(c->p1, c->p2, c->count, idx);
}
bool tagtinker_encode_planes_payload(
const uint8_t* p1, const uint8_t* p2, size_t count, TagTinkerCompressionMode mode, TagTinkerImagePayload* payload) {
if(!p1 || !payload) return false;
TagTinkerPlaneCtx ctx = {.p1 = p1, .p2 = p2, .count = count};
size_t total = p2 ? (count * 2U) : count;
return tagtinker_encode_fn_payload(tagtinker_plane_pixel_cb, &ctx, total, mode, payload);
}
bool tagtinker_encode_image_payload(
const uint8_t* pixels, uint16_t width, uint16_t height, bool color_clear,
TagTinkerCompressionMode mode, TagTinkerImagePayload* payload) {
+68
View File
@@ -18,6 +18,40 @@
#define TAGTINKER_MAX_FRAME_SIZE 96
#define TAGTINKER_IMAGE_DATA_BYTES_PER_FRAME 20U
/* Color 2.6 glass is landscape 296x152. Image header is 152x296 (short x long). */
#define TAGTINKER_TYPE_SMARTAG_COLOR_26 1626
#define TAGTINKER_COLOR26_WIRE_W 152U
#define TAGTINKER_COLOR26_WIRE_H 296U
#define TAGTINKER_COLOR26_GLASS_W 296U
#define TAGTINKER_COLOR26_GLASS_H 152U
static inline bool tagtinker_type_needs_wh_swap(uint16_t type_code) {
return type_code == TAGTINKER_TYPE_SMARTAG_COLOR_26;
}
static inline bool tagtinker_type_uses_ui_page(uint16_t type_code) {
return tagtinker_type_needs_wh_swap(type_code);
}
/* Store-used Color 2.6 tags keep the barcode on page 1. Page 2 is the image
* slot. Unspecified pages (0 or 1) map there. Explicit 2-7 are left alone. */
static inline uint8_t tagtinker_color26_resolve_page(uint8_t page) {
if(page <= 1U) return 2U;
if(page > 7U) return 7U;
return page;
}
/* Wire (px, py) on 152x296 -> glass (bx, by) 296x152. */
static inline void tagtinker_color26_proto_to_glass(
uint16_t proto_w,
uint16_t px,
uint16_t py,
uint16_t* bx,
uint16_t* by) {
*bx = py;
*by = (uint16_t)(proto_w - 1U - px);
}
typedef struct TagTinkerApp TagTinkerApp;
/* CRC used by the ESL wire format. */
@@ -46,6 +80,28 @@ typedef struct {
bool known;
} TagTinkerTagProfile;
static inline bool tagtinker_profile_needs_wh_swap(const TagTinkerTagProfile* profile) {
return profile && tagtinker_type_needs_wh_swap(profile->type_code);
}
static inline bool tagtinker_profile_uses_ui_page(const TagTinkerTagProfile* profile) {
return profile && tagtinker_type_uses_ui_page(profile->type_code);
}
static inline void tagtinker_profile_glass_size(
const TagTinkerTagProfile* profile,
uint16_t* width,
uint16_t* height) {
if(!profile || !width || !height) return;
if(tagtinker_profile_needs_wh_swap(profile)) {
*width = TAGTINKER_COLOR26_GLASS_W;
*height = TAGTINKER_COLOR26_GLASS_H;
return;
}
*width = profile->width;
*height = profile->height;
}
bool tagtinker_is_barcode_valid(const char* barcode);
bool tagtinker_barcode_to_plid(const char* barcode, uint8_t plid[4]);
bool tagtinker_barcode_to_type(const char* barcode, uint16_t* type_code);
@@ -70,6 +126,16 @@ bool tagtinker_encode_image_payload(
bool color_clear,
TagTinkerCompressionMode mode,
TagTinkerImagePayload* payload);
typedef uint8_t (*TagTinkerPixelAtFn)(size_t idx, void* ctx);
bool tagtinker_encode_fn_payload(
TagTinkerPixelAtFn pixel_at,
void* ctx,
size_t total_pixels,
TagTinkerCompressionMode mode,
TagTinkerImagePayload* payload);
bool tagtinker_encode_planes_payload(
const uint8_t* primary_pixels,
const uint8_t* secondary_pixels,
@@ -107,6 +173,8 @@ size_t tagtinker_make_addressed_frame(
/* Tags need a wake ping before most addressed commands. */
size_t tagtinker_make_ping_frame(uint8_t* buf, const uint8_t plid[4]);
size_t tagtinker_make_wake_frame(uint8_t* buf, const uint8_t plid[4]);
size_t tagtinker_make_refresh_frame(uint8_t* buf, const uint8_t plid[4]);
/* Image upload uses MCU frames: one parameter frame followed by data frames. */
+6 -1
View File
@@ -76,7 +76,12 @@ static void setting_cb(void* ctx, uint32_t index) {
void tagtinker_scene_size_picker_on_enter(void* ctx) {
TagTinkerApp* app = ctx;
app->signal_mode = TagTinkerSignalPP4;
if(app->img_page == 0U) app->img_page = 1U;
if(app->selected_target >= 0 && app->selected_target < app->target_count &&
tagtinker_profile_uses_ui_page(&app->targets[app->selected_target].profile)) {
app->img_page = tagtinker_color26_resolve_page(app->img_page);
} else if(app->img_page == 0U) {
app->img_page = 1U;
}
if(app->img_page > 8U) app->img_page = 8U;
variable_item_list_reset(app->var_item_list);
+5 -2
View File
@@ -90,11 +90,14 @@ static void dropped_images_load(TagTinkerApp* app) {
if(file_info_is_dir(&info)) continue;
TagTinkerSyncedImage entry;
uint16_t tw = target->profile.width;
uint16_t th = target->profile.height;
tagtinker_profile_glass_size(&target->profile, &tw, &th);
if(!tagtinker_parse_dropped_filename(
name,
target->barcode,
target->profile.width,
target->profile.height,
tw,
th,
&entry)) {
continue;
}
+5 -2
View File
@@ -29,6 +29,9 @@ static void show_target_details(TagTinkerApp* app, const TagTinkerTarget* target
text_box_set_focus(app->text_box, TextBoxFocusStart);
static char details_buf[256];
uint16_t size_w = target->profile.width;
uint16_t size_h = target->profile.height;
tagtinker_profile_glass_size(&target->profile, &size_w, &size_h);
snprintf(
details_buf,
sizeof(details_buf),
@@ -41,8 +44,8 @@ static void show_target_details(TagTinkerApp* app, const TagTinkerTarget* target
target->profile.model_name ? target->profile.model_name : "Unknown",
target->profile.type_code,
tagtinker_profile_kind_label(target->profile.kind),
target->profile.width,
target->profile.height,
size_w,
size_h,
tagtinker_profile_color_label(target->profile.color),
target->barcode);
+310
View File
@@ -7,6 +7,8 @@
#include "../views/tagtinker_font.h"
#include <furi_hal.h>
#include <storage/storage.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
TagTinkerApp* app;
@@ -28,6 +30,11 @@ typedef struct {
* shot instead of streaming in chunks. 49152 covers 208×112×2 = 46592
* pixels (the most common color DM images). */
#define TX_FULL_JOB_PIXEL_LIMIT 49152U
#define TX_COLOR26_WAKE_REPEATS 400U
#define TX_COLOR26_BMP_MAX 24576U
#define TX_COLOR26_TEXT_ROWS 16U
#define TX_COLOR26_PACKED_BYTES \
(((size_t)TAGTINKER_COLOR26_GLASS_W * (size_t)TAGTINKER_COLOR26_GLASS_H + 7U) / 8U)
static uint16_t tx_pick_chunk_height(uint16_t width, uint16_t height, bool second_plane);
@@ -49,6 +56,17 @@ static TagTinkerTagColor tx_target_color(const TagTinkerApp* app) {
return app->targets[app->selected_target].profile.color;
}
static const TagTinkerTagProfile* tx_target_profile(const TagTinkerApp* app) {
if(app->selected_target < 0 || app->selected_target >= app->target_count) {
return NULL;
}
return &app->targets[app->selected_target].profile;
}
static bool tx_is_color26(const TagTinkerApp* app) {
return tagtinker_profile_needs_wh_swap(tx_target_profile(app));
}
static bool tx_send_frame(TagTinkerApp* app, const uint8_t* frame, size_t len, uint16_t repeats) {
if(!app->tx_active) return false;
return tagtinker_ir_transmit(frame, len, tx_apply_signal_mode(app, repeats), 1);
@@ -60,12 +78,59 @@ static bool tx_send_ping(TagTinkerApp* app, const uint8_t plid[4]) {
return tx_send_frame(app, frame, len, 80);
}
static bool tx_send_wake(TagTinkerApp* app, const uint8_t plid[4], uint16_t repeats) {
uint8_t frame[TAGTINKER_MAX_FRAME_SIZE];
size_t len = tagtinker_make_wake_frame(frame, plid);
return tx_send_frame(app, frame, len, repeats);
}
static bool tx_send_refresh(TagTinkerApp* app, const uint8_t plid[4]) {
uint8_t frame[TAGTINKER_MAX_FRAME_SIZE];
size_t len = tagtinker_make_refresh_frame(frame, plid);
return tx_send_frame(app, frame, len, 20);
}
static bool tx_send_color26_payload(TagTinkerApp* app, const TagTinkerImagePayload* payload) {
const TagTinkerImageTxJob* job = &app->image_tx_job;
uint8_t frame[TAGTINKER_MAX_FRAME_SIZE];
bool ok = tx_send_wake(app, job->plid, TX_COLOR26_WAKE_REPEATS);
if(ok) furi_delay_ms(50);
if(ok) {
size_t len = tagtinker_make_image_param_frame(
frame,
job->plid,
(uint16_t)payload->byte_count,
payload->comp_type,
job->page,
TAGTINKER_COLOR26_WIRE_W,
TAGTINKER_COLOR26_WIRE_H,
0,
0);
ok = tx_send_frame(app, frame, len, 1);
}
if(ok) furi_delay_ms(50);
size_t frame_count = payload->byte_count / TAGTINKER_IMAGE_DATA_BYTES_PER_FRAME;
for(size_t i = 0; ok && i < frame_count; i++) {
size_t len = tagtinker_make_image_data_frame(
frame,
job->plid,
(uint16_t)i,
&payload->data[i * TAGTINKER_IMAGE_DATA_BYTES_PER_FRAME]);
ok = tx_send_frame(app, frame, len, 1);
if(ok && (i + 1U) < frame_count) {
furi_delay_ms(50);
}
}
if(ok) furi_delay_ms(50);
if(ok) {
size_t len = tagtinker_make_refresh_frame(frame, job->plid);
ok = tx_send_frame(app, frame, len, 1);
}
return ok;
}
static bool tx_should_send_full_job(uint16_t width, uint16_t height, bool second_plane) {
size_t pixel_count = (size_t)width * height;
if(second_plane) pixel_count *= 2U;
@@ -268,7 +333,133 @@ static bool tx_send_full_text_image(TagTinkerApp* app) {
return ok;
}
typedef struct {
const uint8_t* p1;
const uint8_t* p2;
} TxColor26TextCtx;
static uint8_t tx_packed_bit(const uint8_t* packed, size_t idx) {
return (packed[idx / 8U] >> (7U - (idx % 8U))) & 1U;
}
static void tx_packed_set(uint8_t* packed, size_t idx, uint8_t val) {
size_t byte = idx / 8U;
uint8_t mask = (uint8_t)(1U << (7U - (idx % 8U)));
if(val) packed[byte] |= mask;
else packed[byte] &= (uint8_t)~mask;
}
static void tx_pack_region(
uint8_t* packed,
const uint8_t* region,
uint16_t w,
uint16_t y0,
uint16_t h) {
for(uint16_t y = 0; y < h; y++) {
for(uint16_t x = 0; x < w; x++) {
tx_packed_set(packed, (size_t)(y0 + y) * w + x, region[(size_t)y * w + x]);
}
}
}
static uint8_t tx_color26_text_pixel(size_t idx, void* ctx) {
const TxColor26TextCtx* c = ctx;
size_t count = (size_t)TAGTINKER_COLOR26_WIRE_W * (size_t)TAGTINKER_COLOR26_WIRE_H;
uint8_t plane = 0;
if(idx >= count) {
plane = 1;
idx -= count;
}
uint16_t px = (uint16_t)(idx % TAGTINKER_COLOR26_WIRE_W);
uint16_t py = (uint16_t)(idx / TAGTINKER_COLOR26_WIRE_W);
uint16_t bx = 0;
uint16_t by = 0;
tagtinker_color26_proto_to_glass(TAGTINKER_COLOR26_WIRE_W, px, py, &bx, &by);
size_t si = (size_t)by * TAGTINKER_COLOR26_GLASS_W + bx;
if(plane == 0) return tx_packed_bit(c->p1, si);
if(!c->p2) return 1U;
return tx_packed_bit(c->p2, si);
}
static bool tx_render_color26_text_plane(
uint8_t* packed,
const char* text,
uint8_t bg,
uint8_t fg,
uint8_t padding_pct) {
const uint16_t w = TAGTINKER_COLOR26_GLASS_W;
const uint16_t h = TAGTINKER_COLOR26_GLASS_H;
memset(packed, bg ? 0xFF : 0x00, TX_COLOR26_PACKED_BYTES);
uint8_t* row = malloc((size_t)w * TX_COLOR26_TEXT_ROWS);
if(!row) return false;
for(uint16_t y = 0; y < h; y = (uint16_t)(y + TX_COLOR26_TEXT_ROWS)) {
uint16_t rh = (uint16_t)(h - y);
if(rh > TX_COLOR26_TEXT_ROWS) rh = TX_COLOR26_TEXT_ROWS;
render_text_region_ex(row, w, h, y, rh, text, bg, fg, padding_pct);
tx_pack_region(packed, row, w, y, rh);
}
free(row);
return true;
}
static bool tx_send_color26_text(TagTinkerApp* app) {
const TagTinkerTarget* target =
(app->selected_target >= 0) ? &app->targets[app->selected_target] : NULL;
bool accent_capable = tagtinker_target_supports_accent(target);
bool accent_text = accent_capable && app->color_clear;
TagTinkerTagColor accent_color = tx_target_color(app);
uint8_t* primary = malloc(TX_COLOR26_PACKED_BYTES);
uint8_t* secondary = NULL;
if(!primary) return false;
if(accent_text) {
secondary = malloc(TX_COLOR26_PACKED_BYTES);
if(!secondary) accent_text = false;
}
uint8_t bg = app->invert_text ? 0 : 1;
uint8_t fg = app->invert_text ? 1 : 0;
bool ok;
if(accent_text) {
uint8_t fg_primary = (accent_color == TagTinkerTagColorYellow) ? 0 : 1;
ok = tx_render_color26_text_plane(
primary, app->text_input_buf, bg, fg_primary, app->text_padding_pct);
if(ok) {
ok = tx_render_color26_text_plane(
secondary, app->text_input_buf, 1, 0, app->text_padding_pct);
}
} else {
ok = tx_render_color26_text_plane(
primary, app->text_input_buf, bg, fg, app->text_padding_pct);
}
if(!ok) {
free(primary);
free(secondary);
return false;
}
TxColor26TextCtx ctx = {.p1 = primary, .p2 = secondary};
TagTinkerImagePayload payload;
ok = tagtinker_encode_fn_payload(
tx_color26_text_pixel,
&ctx,
(size_t)TAGTINKER_COLOR26_WIRE_W * (size_t)TAGTINKER_COLOR26_WIRE_H * 2U,
app->compression_mode,
&payload);
free(primary);
free(secondary);
if(!ok) return false;
ok = tx_send_color26_payload(app, &payload);
tagtinker_free_image_payload(&payload);
return ok;
}
static bool tx_stream_text_image(TagTinkerApp* app) {
if(tx_is_color26(app)) {
return tx_send_color26_text(app);
}
if(tx_send_full_text_image(app)) {
return true;
}
@@ -454,6 +645,117 @@ static inline bool bmp_read_row_at(
} \
} while(0)
typedef struct {
const uint8_t* file;
size_t file_len;
const TxBmpInfo* info;
} TxColor26BmpCtx;
static void tx_color26_src_xy(
const TxBmpInfo* info,
uint16_t px,
uint16_t py,
uint16_t* sx,
uint16_t* sy) {
if(info->width == TAGTINKER_COLOR26_WIRE_W &&
info->height == TAGTINKER_COLOR26_WIRE_H) {
*sx = px;
*sy = py;
return;
}
uint16_t gx = 0;
uint16_t gy = 0;
tagtinker_color26_proto_to_glass(TAGTINKER_COLOR26_WIRE_W, px, py, &gx, &gy);
if(info->width == TAGTINKER_COLOR26_GLASS_W &&
info->height == TAGTINKER_COLOR26_GLASS_H) {
*sx = gx;
*sy = gy;
return;
}
*sx = bmp_map_x(gx, TAGTINKER_COLOR26_GLASS_W, info->width);
*sy = bmp_map_y(gy, TAGTINKER_COLOR26_GLASS_H, info->height);
}
static uint8_t tx_color26_bmp_bit(
const TxColor26BmpCtx* c,
uint16_t bx,
uint16_t by,
uint8_t plane) {
if(c->info->width == 0U || c->info->height == 0U) return 1U;
if(bx >= c->info->width) bx = (uint16_t)(c->info->width - 1U);
if(by >= c->info->height) by = (uint16_t)(c->info->height - 1U);
uint16_t actual_row = c->info->top_down ? by : (uint16_t)(c->info->height - 1U - by);
uint32_t off = c->info->data_offset +
((uint32_t)actual_row + (uint32_t)plane * (uint32_t)c->info->height) * c->info->row_stride;
off += bx / 8U;
if(off >= c->file_len) return 1U;
uint8_t bit = (c->file[off] >> (7U - (bx % 8U))) & 1U;
return bit ? 0U : 1U;
}
static uint8_t tx_color26_bmp_pixel(size_t idx, void* ctx) {
const TxColor26BmpCtx* c = ctx;
size_t count = (size_t)TAGTINKER_COLOR26_WIRE_W * (size_t)TAGTINKER_COLOR26_WIRE_H;
uint8_t plane = 0;
if(idx >= count) {
plane = 1;
idx -= count;
}
uint16_t px = (uint16_t)(idx % TAGTINKER_COLOR26_WIRE_W);
uint16_t py = (uint16_t)(idx / TAGTINKER_COLOR26_WIRE_W);
uint16_t bx = 0;
uint16_t by = 0;
tx_color26_src_xy(c->info, px, py, &bx, &by);
if(plane && c->info->bpp != 2) return 1U;
return tx_color26_bmp_bit(c, bx, by, plane);
}
static bool tx_send_color26_bmp(TagTinkerApp* app, File* file, const TxBmpInfo* info) {
if(info->bpp != 1 && info->bpp != 2) {
FURI_LOG_W(
TAGTINKER_TAG,
"Color 2.6 BMP bpp=%u not supported (need 1 or 2)",
info->bpp);
return false;
}
uint64_t sz = storage_file_size(file);
if(sz < 54U || sz > TX_COLOR26_BMP_MAX) {
FURI_LOG_W(
TAGTINKER_TAG,
"Color 2.6 BMP size %lu not supported",
(unsigned long)sz);
return false;
}
uint8_t* mem = malloc((size_t)sz);
if(!mem) return false;
storage_file_seek(file, 0, true);
bool loaded = storage_file_read(file, mem, (size_t)sz) == (size_t)sz;
if(!loaded) {
free(mem);
return false;
}
TxColor26BmpCtx ctx = {
.file = mem,
.file_len = (size_t)sz,
.info = info,
};
TagTinkerImagePayload payload;
bool ok = tagtinker_encode_fn_payload(
tx_color26_bmp_pixel,
&ctx,
(size_t)TAGTINKER_COLOR26_WIRE_W * (size_t)TAGTINKER_COLOR26_WIRE_H * 2U,
app->compression_mode,
&payload);
free(mem);
if(!ok) return false;
ok = tx_send_color26_payload(app, &payload);
tagtinker_free_image_payload(&payload);
return ok;
}
static bool tx_stream_bmp_image(TagTinkerApp* app) {
const TagTinkerImageTxJob* job = &app->image_tx_job;
tx_debug_log("BMP TX: path=%s w=%u h=%u page=%u",
@@ -473,6 +775,14 @@ static bool tx_stream_bmp_image(TagTinkerApp* app) {
return false;
}
if(tx_is_color26(app)) {
bool sent = tx_send_color26_bmp(app, file, &info);
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
return sent;
}
/* Output dims come from the target's profile; source dims come from the
* BMP file. The streaming pipeline below rescales source -> target with
* nearest-neighbour as it reads, so any BMP can drive any tag. */
+1 -4
View File
@@ -289,13 +289,10 @@ static void start_run(TagTinkerApp* app) {
* else fallback to a reasonable sane size. */
uint16_t tw = app->esl_width ? app->esl_width : 296;
uint16_t th = app->esl_height ? app->esl_height : 128;
/* Honour the tag's accent capability: red/yellow profiles get the
* accent plane, mono profiles stay mono. The BMP writer + the IR TX
* pipeline already understand 2-plane BMPs (same convention as the
* web image prep tool), so plugins can use the accent freely. */
uint8_t accent = TT_ACCENT_NONE;
if(app->selected_target >= 0 && app->selected_target < app->target_count) {
const TagTinkerTarget* t = &app->targets[app->selected_target];
tagtinker_profile_glass_size(&t->profile, &tw, &th);
if(tagtinker_target_supports_accent(t)) {
accent = (t->profile.color == TagTinkerTagColorYellow)
? TT_ACCENT_YELLOW
+17 -1
View File
@@ -354,7 +354,19 @@ void tagtinker_prepare_text_tx(TagTinkerApp* app, const uint8_t plid[4]) {
memset(&app->image_tx_job, 0, sizeof(app->image_tx_job));
app->image_tx_job.mode = TagTinkerTxModeTextImage;
memcpy(app->image_tx_job.plid, plid, sizeof(app->image_tx_job.plid));
app->image_tx_job.page = (app->img_page > 0U) ? (uint8_t)(app->img_page - 1U) : 0U;
/* HD tags map UI page 1-8 to protocol 0-7. Color 2.6 uses the UI page
* number on the wire. Unspecified 0/1 become page 2 (image slot). */
{
const TagTinkerTagProfile* profile = NULL;
if(app->selected_target >= 0 && app->selected_target < app->target_count) {
profile = &app->targets[app->selected_target].profile;
}
if(tagtinker_profile_uses_ui_page(profile)) {
app->image_tx_job.page = tagtinker_color26_resolve_page(app->img_page);
} else {
app->image_tx_job.page = (app->img_page > 0U) ? (uint8_t)(app->img_page - 1U) : 0U;
}
}
if(app->image_tx_job.page > 7U) app->image_tx_job.page = 7U;
app->image_tx_job.width = app->esl_width;
app->image_tx_job.height = app->esl_height;
@@ -382,6 +394,10 @@ void tagtinker_prepare_bmp_tx(
app->image_tx_job.mode = TagTinkerTxModeBmpImage;
memcpy(app->image_tx_job.plid, plid, sizeof(app->image_tx_job.plid));
app->image_tx_job.page = page;
if(app->selected_target >= 0 && app->selected_target < app->target_count &&
tagtinker_profile_uses_ui_page(&app->targets[app->selected_target].profile)) {
app->image_tx_job.page = tagtinker_color26_resolve_page(page);
}
app->image_tx_job.width = width;
app->image_tx_job.height = height;
app->image_tx_job.pos_x = app->draw_x;
+2
View File
@@ -379,6 +379,8 @@ const PROFILES = [
{ code:1354, w:648, h:480, color:"red", name:"SmartTag HD150 Red" },
{ code:1370, w:296, h:128, color:"red", name:"SmartTag HD L Red (2021)" },
{ code:1371, w:648, h:480, color:"red", name:"SmartTag HD150 Red (2021)" },
/* Glass is landscape 296x152. The FAP transposes to 152x296 for the firmware. */
{ code:1626, w:296, h:152, color:"red", name:"SmartTAG Color 2.6" },
{ code:1627, w:296, h:128, color:"red", name:"SmartTag HD L Red" },
{ code:1628, w:296, h:128, color:"red", name:"SmartTag HD L Red" },
{ code:1639, w:152, h:152, color:"red", name:"SmartTag HD S Red" },