From b328511d02bd7111040fbbc39edfe806fccf7c2e Mon Sep 17 00:00:00 2001 From: sosnek Date: Mon, 31 Aug 2026 00:07:03 -0400 Subject: [PATCH 1/4] Add SmartTAG Color 2.6 (type 1626) support These tags look like 296x152 but the firmware only takes 152x296, so landscape BMPs get transposed. Tested on a real 2.6 color tag. Co-authored-by: Cursor --- protocol/tagtinker_proto.c | 68 ++++++++ protocol/tagtinker_proto.h | 12 ++ scenes/tagtinker_scene_size_picker.c | 8 +- scenes/tagtinker_scene_transmit.c | 247 +++++++++++++++++++++++++++ tagtinker_app.c | 17 +- web-image-prep/index.html | 2 + 6 files changed, 352 insertions(+), 2 deletions(-) diff --git a/protocol/tagtinker_proto.c b/protocol/tagtinker_proto.c index b9e4410..8a68a6b 100644 --- a/protocol/tagtinker_proto.c +++ b/protocol/tagtinker_proto.c @@ -53,6 +53,7 @@ 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}, + {1626, 152, 296, 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 +151,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; @@ -251,6 +263,62 @@ static void tagtinker_pack_planes_rle(const uint8_t* p1, const uint8_t* p2, size #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 = pixel_at(0, ctx); + uint32_t run_count = 1; + for(size_t i = 1; i < total; 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; } + } + if(run_count > 0U) bit_len += record_run_bit_length(run_count); + return bit_len; +} + +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, pixel_at(i, ctx)); +} + +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 = 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 = 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); +} + +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 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_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; +} + 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; diff --git a/protocol/tagtinker_proto.h b/protocol/tagtinker_proto.h index 903af98..751579b 100644 --- a/protocol/tagtinker_proto.h +++ b/protocol/tagtinker_proto.h @@ -70,6 +70,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 +117,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. */ diff --git a/scenes/tagtinker_scene_size_picker.c b/scenes/tagtinker_scene_size_picker.c index 9fa406e..a81b285 100644 --- a/scenes/tagtinker_scene_size_picker.c +++ b/scenes/tagtinker_scene_size_picker.c @@ -76,7 +76,13 @@ 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 && + app->targets[app->selected_target].profile.type_code == 1626) { + if(app->img_page == 0U || app->img_page == 1U) app->img_page = 2U; + } 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); diff --git a/scenes/tagtinker_scene_transmit.c b/scenes/tagtinker_scene_transmit.c index 4a23a28..7f939a1 100644 --- a/scenes/tagtinker_scene_transmit.c +++ b/scenes/tagtinker_scene_transmit.c @@ -7,6 +7,7 @@ #include "../views/tagtinker_font.h" #include #include +#include typedef struct { TagTinkerApp* app; @@ -29,6 +30,15 @@ typedef struct { * pixels (the most common color DM images). */ #define TX_FULL_JOB_PIXEL_LIMIT 49152U +/* Color 2.6 glass is landscape 296x152. Firmware image header is 152x296 + * (short x long). Same pixel count, axes swapped. */ +#define TX_COLOR26_PROTO_W 152U +#define TX_COLOR26_PROTO_H 296U +#define TX_COLOR26_GLASS_W 296U +#define TX_COLOR26_GLASS_H 152U +#define TX_COLOR26_BMP_MAX 24576U +#define TX_COLOR26_WAKE_REPEATS 400U + static uint16_t tx_pick_chunk_height(uint16_t width, uint16_t height, bool second_plane); static void tx_debug_log(const char* fmt, ...) { @@ -49,6 +59,13 @@ static TagTinkerTagColor tx_target_color(const TagTinkerApp* app) { return app->targets[app->selected_target].profile.color; } +static bool tx_is_color26(const TagTinkerApp* app) { + if(app->selected_target < 0 || app->selected_target >= app->target_count) { + return false; + } + return app->targets[app->selected_target].profile.type_code == 1626; +} + 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 +77,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, + TX_COLOR26_PROTO_W, + TX_COLOR26_PROTO_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 +332,95 @@ 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_color26_text_pixel(size_t idx, void* ctx) { + const TxColor26TextCtx* c = ctx; + size_t count = (size_t)TX_COLOR26_PROTO_W * (size_t)TX_COLOR26_PROTO_H; + uint8_t plane = 0; + if(idx >= count) { + plane = 1; + idx -= count; + } + uint16_t px = (uint16_t)(idx % TX_COLOR26_PROTO_W); + uint16_t py = (uint16_t)(idx / TX_COLOR26_PROTO_W); + uint16_t bx = py; + uint16_t by = (uint16_t)(TX_COLOR26_PROTO_W - 1U - px); + size_t si = (size_t)by * TX_COLOR26_GLASS_W + bx; + if(plane == 0) return c->p1[si]; + if(!c->p2) return 1U; + return c->p2[si]; +} + +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); + size_t count = (size_t)TX_COLOR26_GLASS_W * (size_t)TX_COLOR26_GLASS_H; + uint8_t* primary = malloc(count); + uint8_t* secondary = accent_text ? malloc(count) : NULL; + if(!primary || (accent_text && !secondary)) { + free(primary); + free(secondary); + return false; + } + + if(accent_text) { + uint8_t bg_primary = app->invert_text ? 0 : 1; + uint8_t fg_primary = (accent_color == TagTinkerTagColorYellow) ? 0 : 1; + render_text_ex( + primary, + TX_COLOR26_GLASS_W, + TX_COLOR26_GLASS_H, + app->text_input_buf, + bg_primary, + fg_primary, + app->text_padding_pct); + render_text_ex( + secondary, + TX_COLOR26_GLASS_W, + TX_COLOR26_GLASS_H, + app->text_input_buf, + 1, + 0, + app->text_padding_pct); + } else { + render_text_ex( + primary, + TX_COLOR26_GLASS_W, + TX_COLOR26_GLASS_H, + app->text_input_buf, + app->invert_text ? 0 : 1, + app->invert_text ? 1 : 0, + app->text_padding_pct); + } + + TxColor26TextCtx ctx = {.p1 = primary, .p2 = secondary}; + TagTinkerImagePayload payload; + bool ok = tagtinker_encode_fn_payload( + tx_color26_text_pixel, + &ctx, + count * 2U, + TagTinkerCompressionAuto, + &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 +606,93 @@ static inline bool bmp_read_row_at( } \ } while(0) +typedef struct { + const uint8_t* file; + size_t file_len; + const TxBmpInfo* info; + bool transpose; +} TxColor26BmpCtx; + +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)TX_COLOR26_PROTO_W * (size_t)TX_COLOR26_PROTO_H; + uint8_t plane = 0; + if(idx >= count) { + plane = 1; + idx -= count; + } + uint16_t px = (uint16_t)(idx % TX_COLOR26_PROTO_W); + uint16_t py = (uint16_t)(idx / TX_COLOR26_PROTO_W); + uint16_t bx = px; + uint16_t by = py; + if(c->transpose) { + bx = py; + by = (uint16_t)(TX_COLOR26_PROTO_W - 1U - px); + } + 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) return false; + uint64_t sz = storage_file_size(file); + if(sz < 54U || sz > TX_COLOR26_BMP_MAX) 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, + .transpose = (info->width == TX_COLOR26_GLASS_W && info->height == TX_COLOR26_GLASS_H), + }; + TagTinkerImagePayload payload; + bool ok = tagtinker_encode_fn_payload( + tx_color26_bmp_pixel, + &ctx, + (size_t)TX_COLOR26_PROTO_W * (size_t)TX_COLOR26_PROTO_H * 2U, + TagTinkerCompressionAuto, + &payload); + free(mem); + if(!ok) return false; + + tx_debug_log( + "Color26 BMP %ux%u bpp=%u transpose=%u bytes=%u pg=%u", + info->width, + info->height, + info->bpp, + ctx.transpose ? 1U : 0U, + (unsigned)payload.byte_count, + app->image_tx_job.page); + 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 +712,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. */ diff --git a/tagtinker_app.c b/tagtinker_app.c index bd81b9a..bfa2dc4 100644 --- a/tagtinker_app.c +++ b/tagtinker_app.c @@ -354,7 +354,16 @@ 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 as the protocol page (store image slot is page 2). */ + if(app->selected_target >= 0 && + app->selected_target < app->target_count && + app->targets[app->selected_target].profile.type_code == 1626) { + app->image_tx_job.page = app->img_page; + if(app->image_tx_job.page < 1U) app->image_tx_job.page = 1U; + } 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 +391,12 @@ 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 && + app->targets[app->selected_target].profile.type_code == 1626 && + app->image_tx_job.page == 0U) { + app->image_tx_job.page = 2U; + } app->image_tx_job.width = width; app->image_tx_job.height = height; app->image_tx_job.pos_x = app->draw_x; diff --git a/web-image-prep/index.html b/web-image-prep/index.html index c78ce00..269c278 100644 --- a/web-image-prep/index.html +++ b/web-image-prep/index.html @@ -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" }, From 3b8ad97c34aae31d3151c100f8d1b4ce1d7ac80a Mon Sep 17 00:00:00 2001 From: sosnek Date: Mon, 31 Aug 2026 00:40:50 -0400 Subject: [PATCH 2/4] Fix Color 2.6 page default, Set Text heap, and WiFi canvas Default image page is 2 unless the user picked 2-7. Set Text packs planes instead of 45KB buffers. WiFi and odd-size BMPs use glass 296x152 then transpose. Co-authored-by: Cursor --- protocol/tagtinker_color26.h | 42 ++++ protocol/tagtinker_proto.c | 66 ++---- protocol/tagtinker_proto.h | 23 +++ scenes/tagtinker_scene_size_picker.c | 7 +- scenes/tagtinker_scene_synced_image_list.c | 7 +- scenes/tagtinker_scene_target_actions.c | 7 +- scenes/tagtinker_scene_transmit.c | 225 +++++++++++++-------- scenes/tagtinker_scene_wifi_run.c | 5 +- tagtinker_app.c | 29 +-- tests/test_color26.c | 68 +++++++ 10 files changed, 321 insertions(+), 158 deletions(-) create mode 100644 protocol/tagtinker_color26.h create mode 100644 tests/test_color26.c diff --git a/protocol/tagtinker_color26.h b/protocol/tagtinker_color26.h new file mode 100644 index 0000000..8552440 --- /dev/null +++ b/protocol/tagtinker_color26.h @@ -0,0 +1,42 @@ +/* + * SmartTAG Color 2.6 (type 1626) geometry and page policy. + * Glass is landscape 296x152. The image header is 152x296 (short x long). + */ + +#pragma once + +#include +#include + +#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; +} + +/* proto (px, py) on the 152x296 wire canvas -> 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); +} diff --git a/protocol/tagtinker_proto.c b/protocol/tagtinker_proto.c index 8a68a6b..066f23f 100644 --- a/protocol/tagtinker_proto.c +++ b/protocol/tagtinker_proto.c @@ -53,7 +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}, - {1626, 152, 296, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed, "SmartTAG Color 2.6", 0}, + {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}, @@ -224,43 +225,6 @@ 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; - size_t bit_len = 1U; - uint8_t run_pixel = plane_pixel_at(p1, p2, count, 0); - uint32_t run_count = 1; - for(size_t i = 1; i < total; i++) { - uint8_t pix = plane_pixel_at(p1, p2, count, i); - if(pix == run_pixel) run_count++; - else { bit_len += record_run_bit_length(run_count); run_pixel = pix; run_count = 1; } - } - if(run_count > 0U) bit_len += record_run_bit_length(run_count); - 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; - 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)); -} - -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; - if(total == 0) return; - TagTinkerBitWriter writer = {.data = out, .bit_pos = 0}; - uint8_t run_pixel = plane_pixel_at(p1, p2, count, 0); - 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); - 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) static size_t tagtinker_rle_fn_bit_length(TagTinkerPixelAtFn pixel_at, void* ctx, size_t total) { @@ -319,23 +283,23 @@ bool tagtinker_encode_fn_payload( 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; - memset(payload, 0, sizeof(*payload)); + TagTinkerPlaneCtx ctx = {.p1 = p1, .p2 = p2, .count = count}; size_t total = p2 ? (count * 2U) : count; - size_t comp_len = tagtinker_rle_planes_bit_length(p1, p2, count); - 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; - payload->comp_type = use_compressed ? 2U : 0U; - return true; + return tagtinker_encode_fn_payload(tagtinker_plane_pixel_cb, &ctx, total, mode, payload); } bool tagtinker_encode_image_payload( diff --git a/protocol/tagtinker_proto.h b/protocol/tagtinker_proto.h index 751579b..db96527 100644 --- a/protocol/tagtinker_proto.h +++ b/protocol/tagtinker_proto.h @@ -12,6 +12,7 @@ #include #include #include +#include "tagtinker_color26.h" #define TAGTINKER_PROTO_DM 0x85 #define TAGTINKER_PROTO_SEG 0x84 @@ -46,6 +47,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); diff --git a/scenes/tagtinker_scene_size_picker.c b/scenes/tagtinker_scene_size_picker.c index a81b285..660fc43 100644 --- a/scenes/tagtinker_scene_size_picker.c +++ b/scenes/tagtinker_scene_size_picker.c @@ -76,10 +76,9 @@ 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->selected_target >= 0 && - app->selected_target < app->target_count && - app->targets[app->selected_target].profile.type_code == 1626) { - if(app->img_page == 0U || app->img_page == 1U) app->img_page = 2U; + 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; } diff --git a/scenes/tagtinker_scene_synced_image_list.c b/scenes/tagtinker_scene_synced_image_list.c index 5e4bdeb..93c3dc8 100644 --- a/scenes/tagtinker_scene_synced_image_list.c +++ b/scenes/tagtinker_scene_synced_image_list.c @@ -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; } diff --git a/scenes/tagtinker_scene_target_actions.c b/scenes/tagtinker_scene_target_actions.c index 0beb0d7..979802b 100644 --- a/scenes/tagtinker_scene_target_actions.c +++ b/scenes/tagtinker_scene_target_actions.c @@ -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); diff --git a/scenes/tagtinker_scene_transmit.c b/scenes/tagtinker_scene_transmit.c index 7f939a1..f5ab8fd 100644 --- a/scenes/tagtinker_scene_transmit.c +++ b/scenes/tagtinker_scene_transmit.c @@ -8,6 +8,7 @@ #include #include #include +#include typedef struct { TagTinkerApp* app; @@ -29,15 +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 - -/* Color 2.6 glass is landscape 296x152. Firmware image header is 152x296 - * (short x long). Same pixel count, axes swapped. */ -#define TX_COLOR26_PROTO_W 152U -#define TX_COLOR26_PROTO_H 296U -#define TX_COLOR26_GLASS_W 296U -#define TX_COLOR26_GLASS_H 152U -#define TX_COLOR26_BMP_MAX 24576U #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); @@ -59,11 +56,15 @@ static TagTinkerTagColor tx_target_color(const TagTinkerApp* app) { return app->targets[app->selected_target].profile.color; } -static bool tx_is_color26(const TagTinkerApp* app) { +static const TagTinkerTagProfile* tx_target_profile(const TagTinkerApp* app) { if(app->selected_target < 0 || app->selected_target >= app->target_count) { - return false; + return NULL; } - return app->targets[app->selected_target].profile.type_code == 1626; + 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) { @@ -101,8 +102,8 @@ static bool tx_send_color26_payload(TagTinkerApp* app, const TagTinkerImagePaylo (uint16_t)payload->byte_count, payload->comp_type, job->page, - TX_COLOR26_PROTO_W, - TX_COLOR26_PROTO_H, + TAGTINKER_COLOR26_WIRE_W, + TAGTINKER_COLOR26_WIRE_H, 0, 0); ok = tx_send_frame(app, frame, len, 1); @@ -337,22 +338,70 @@ typedef struct { 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)TX_COLOR26_PROTO_W * (size_t)TX_COLOR26_PROTO_H; + 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 % TX_COLOR26_PROTO_W); - uint16_t py = (uint16_t)(idx / TX_COLOR26_PROTO_W); - uint16_t bx = py; - uint16_t by = (uint16_t)(TX_COLOR26_PROTO_W - 1U - px); - size_t si = (size_t)by * TX_COLOR26_GLASS_W + bx; - if(plane == 0) return c->p1[si]; + 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 c->p2[si]; + 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) { @@ -361,52 +410,42 @@ static bool tx_send_color26_text(TagTinkerApp* app) { bool accent_capable = tagtinker_target_supports_accent(target); bool accent_text = accent_capable && app->color_clear; TagTinkerTagColor accent_color = tx_target_color(app); - size_t count = (size_t)TX_COLOR26_GLASS_W * (size_t)TX_COLOR26_GLASS_H; - uint8_t* primary = malloc(count); - uint8_t* secondary = accent_text ? malloc(count) : NULL; - if(!primary || (accent_text && !secondary)) { + 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; } - if(accent_text) { - uint8_t bg_primary = app->invert_text ? 0 : 1; - uint8_t fg_primary = (accent_color == TagTinkerTagColorYellow) ? 0 : 1; - render_text_ex( - primary, - TX_COLOR26_GLASS_W, - TX_COLOR26_GLASS_H, - app->text_input_buf, - bg_primary, - fg_primary, - app->text_padding_pct); - render_text_ex( - secondary, - TX_COLOR26_GLASS_W, - TX_COLOR26_GLASS_H, - app->text_input_buf, - 1, - 0, - app->text_padding_pct); - } else { - render_text_ex( - primary, - TX_COLOR26_GLASS_W, - TX_COLOR26_GLASS_H, - app->text_input_buf, - app->invert_text ? 0 : 1, - app->invert_text ? 1 : 0, - app->text_padding_pct); - } - TxColor26TextCtx ctx = {.p1 = primary, .p2 = secondary}; TagTinkerImagePayload payload; - bool ok = tagtinker_encode_fn_payload( + ok = tagtinker_encode_fn_payload( tx_color26_text_pixel, &ctx, - count * 2U, - TagTinkerCompressionAuto, + (size_t)TAGTINKER_COLOR26_WIRE_W * (size_t)TAGTINKER_COLOR26_WIRE_H * 2U, + app->compression_mode, &payload); free(primary); free(secondary); @@ -610,9 +649,33 @@ typedef struct { const uint8_t* file; size_t file_len; const TxBmpInfo* info; - bool transpose; } 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, @@ -632,28 +695,37 @@ static uint8_t tx_color26_bmp_bit( static uint8_t tx_color26_bmp_pixel(size_t idx, void* ctx) { const TxColor26BmpCtx* c = ctx; - size_t count = (size_t)TX_COLOR26_PROTO_W * (size_t)TX_COLOR26_PROTO_H; + 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 % TX_COLOR26_PROTO_W); - uint16_t py = (uint16_t)(idx / TX_COLOR26_PROTO_W); - uint16_t bx = px; - uint16_t by = py; - if(c->transpose) { - bx = py; - by = (uint16_t)(TX_COLOR26_PROTO_W - 1U - px); - } + 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) return false; + 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) return false; + 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; @@ -668,26 +740,17 @@ static bool tx_send_color26_bmp(TagTinkerApp* app, File* file, const TxBmpInfo* .file = mem, .file_len = (size_t)sz, .info = info, - .transpose = (info->width == TX_COLOR26_GLASS_W && info->height == TX_COLOR26_GLASS_H), }; TagTinkerImagePayload payload; bool ok = tagtinker_encode_fn_payload( tx_color26_bmp_pixel, &ctx, - (size_t)TX_COLOR26_PROTO_W * (size_t)TX_COLOR26_PROTO_H * 2U, - TagTinkerCompressionAuto, + (size_t)TAGTINKER_COLOR26_WIRE_W * (size_t)TAGTINKER_COLOR26_WIRE_H * 2U, + app->compression_mode, &payload); free(mem); if(!ok) return false; - tx_debug_log( - "Color26 BMP %ux%u bpp=%u transpose=%u bytes=%u pg=%u", - info->width, - info->height, - info->bpp, - ctx.transpose ? 1U : 0U, - (unsigned)payload.byte_count, - app->image_tx_job.page); ok = tx_send_color26_payload(app, &payload); tagtinker_free_image_payload(&payload); return ok; diff --git a/scenes/tagtinker_scene_wifi_run.c b/scenes/tagtinker_scene_wifi_run.c index c14b57b..7d23f60 100644 --- a/scenes/tagtinker_scene_wifi_run.c +++ b/scenes/tagtinker_scene_wifi_run.c @@ -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 diff --git a/tagtinker_app.c b/tagtinker_app.c index bfa2dc4..f6c5292 100644 --- a/tagtinker_app.c +++ b/tagtinker_app.c @@ -354,15 +354,18 @@ 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)); - /* HD tags map UI page 1-8 to protocol 0-7. Color 2.6 uses the UI - * page as the protocol page (store image slot is page 2). */ - if(app->selected_target >= 0 && - app->selected_target < app->target_count && - app->targets[app->selected_target].profile.type_code == 1626) { - app->image_tx_job.page = app->img_page; - if(app->image_tx_job.page < 1U) app->image_tx_job.page = 1U; - } else { - 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; @@ -391,11 +394,9 @@ 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 && - app->targets[app->selected_target].profile.type_code == 1626 && - app->image_tx_job.page == 0U) { - app->image_tx_job.page = 2U; + 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; diff --git a/tests/test_color26.c b/tests/test_color26.c new file mode 100644 index 0000000..168b302 --- /dev/null +++ b/tests/test_color26.c @@ -0,0 +1,68 @@ +#include "../protocol/tagtinker_color26.h" +#include +#include + +static int failures = 0; + +static void expect_u8(const char* name, uint8_t got, uint8_t want) { + if(got == want) return; + fprintf(stderr, "FAIL %s: got %u want %u\n", name, got, want); + failures++; +} + +static void expect_xy( + const char* name, + uint16_t gx, + uint16_t gy, + uint16_t want_x, + uint16_t want_y) { + if(gx == want_x && gy == want_y) return; + fprintf(stderr, "FAIL %s: got %u,%u want %u,%u\n", name, gx, gy, want_x, want_y); + failures++; +} + +int main(void) { + expect_u8("page 0", tagtinker_color26_resolve_page(0), 2); + expect_u8("page 1", tagtinker_color26_resolve_page(1), 2); + expect_u8("page 2", tagtinker_color26_resolve_page(2), 2); + expect_u8("page 3", tagtinker_color26_resolve_page(3), 3); + expect_u8("page 7", tagtinker_color26_resolve_page(7), 7); + expect_u8("page 8", tagtinker_color26_resolve_page(8), 7); + expect_u8("page 9", tagtinker_color26_resolve_page(9), 7); + + /* 2x3 wire, 3x2 glass. bx = py, by = proto_w - 1 - px. */ + const uint16_t proto_w = 2; + struct { + uint16_t px, py, bx, by; + } cases[] = { + {0, 0, 0, 1}, + {1, 0, 0, 0}, + {0, 1, 1, 1}, + {1, 1, 1, 0}, + {0, 2, 2, 1}, + {1, 2, 2, 0}, + }; + for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + uint16_t bx = 99, by = 99; + tagtinker_color26_proto_to_glass(proto_w, cases[i].px, cases[i].py, &bx, &by); + char name[32]; + snprintf(name, sizeof(name), "map %u,%u", cases[i].px, cases[i].py); + expect_xy(name, bx, by, cases[i].bx, cases[i].by); + } + + if(!tagtinker_type_needs_wh_swap(TAGTINKER_TYPE_SMARTAG_COLOR_26)) { + fprintf(stderr, "FAIL type 1626 should swap W/H\n"); + failures++; + } + if(tagtinker_type_needs_wh_swap(1627)) { + fprintf(stderr, "FAIL type 1627 should not swap W/H\n"); + failures++; + } + + if(failures) { + fprintf(stderr, "%d failure(s)\n", failures); + return 1; + } + printf("ok\n"); + return 0; +} From 3ebae10adaa1b9daef919dddfc5bcae5efcb1062 Mon Sep 17 00:00:00 2001 From: sosnek Date: Mon, 31 Aug 2026 00:44:27 -0400 Subject: [PATCH 3/4] Fold Color 2.6 helpers into proto.h One extra header for a single type code did not match how the rest of the protocol layer is laid out. Co-authored-by: Cursor --- protocol/tagtinker_color26.h | 42 ------------------------------------ protocol/tagtinker_proto.h | 35 +++++++++++++++++++++++++++++- tests/test_color26.c | 2 +- 3 files changed, 35 insertions(+), 44 deletions(-) delete mode 100644 protocol/tagtinker_color26.h diff --git a/protocol/tagtinker_color26.h b/protocol/tagtinker_color26.h deleted file mode 100644 index 8552440..0000000 --- a/protocol/tagtinker_color26.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SmartTAG Color 2.6 (type 1626) geometry and page policy. - * Glass is landscape 296x152. The image header is 152x296 (short x long). - */ - -#pragma once - -#include -#include - -#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; -} - -/* proto (px, py) on the 152x296 wire canvas -> 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); -} diff --git a/protocol/tagtinker_proto.h b/protocol/tagtinker_proto.h index db96527..1861cb7 100644 --- a/protocol/tagtinker_proto.h +++ b/protocol/tagtinker_proto.h @@ -12,13 +12,46 @@ #include #include #include -#include "tagtinker_color26.h" #define TAGTINKER_PROTO_DM 0x85 #define TAGTINKER_PROTO_SEG 0x84 #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. */ diff --git a/tests/test_color26.c b/tests/test_color26.c index 168b302..52fe119 100644 --- a/tests/test_color26.c +++ b/tests/test_color26.c @@ -1,4 +1,4 @@ -#include "../protocol/tagtinker_color26.h" +#include "../protocol/tagtinker_proto.h" #include #include From f1447306fadf0e72b935f681376c17761651f673 Mon Sep 17 00:00:00 2001 From: sosnek Date: Mon, 31 Aug 2026 00:52:39 -0400 Subject: [PATCH 4/4] Drop the Color 2.6 host unit test It was a one-off, unwired pattern this repo does not use. Co-authored-by: Cursor --- tests/test_color26.c | 68 -------------------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 tests/test_color26.c diff --git a/tests/test_color26.c b/tests/test_color26.c deleted file mode 100644 index 52fe119..0000000 --- a/tests/test_color26.c +++ /dev/null @@ -1,68 +0,0 @@ -#include "../protocol/tagtinker_proto.h" -#include -#include - -static int failures = 0; - -static void expect_u8(const char* name, uint8_t got, uint8_t want) { - if(got == want) return; - fprintf(stderr, "FAIL %s: got %u want %u\n", name, got, want); - failures++; -} - -static void expect_xy( - const char* name, - uint16_t gx, - uint16_t gy, - uint16_t want_x, - uint16_t want_y) { - if(gx == want_x && gy == want_y) return; - fprintf(stderr, "FAIL %s: got %u,%u want %u,%u\n", name, gx, gy, want_x, want_y); - failures++; -} - -int main(void) { - expect_u8("page 0", tagtinker_color26_resolve_page(0), 2); - expect_u8("page 1", tagtinker_color26_resolve_page(1), 2); - expect_u8("page 2", tagtinker_color26_resolve_page(2), 2); - expect_u8("page 3", tagtinker_color26_resolve_page(3), 3); - expect_u8("page 7", tagtinker_color26_resolve_page(7), 7); - expect_u8("page 8", tagtinker_color26_resolve_page(8), 7); - expect_u8("page 9", tagtinker_color26_resolve_page(9), 7); - - /* 2x3 wire, 3x2 glass. bx = py, by = proto_w - 1 - px. */ - const uint16_t proto_w = 2; - struct { - uint16_t px, py, bx, by; - } cases[] = { - {0, 0, 0, 1}, - {1, 0, 0, 0}, - {0, 1, 1, 1}, - {1, 1, 1, 0}, - {0, 2, 2, 1}, - {1, 2, 2, 0}, - }; - for(size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { - uint16_t bx = 99, by = 99; - tagtinker_color26_proto_to_glass(proto_w, cases[i].px, cases[i].py, &bx, &by); - char name[32]; - snprintf(name, sizeof(name), "map %u,%u", cases[i].px, cases[i].py); - expect_xy(name, bx, by, cases[i].bx, cases[i].by); - } - - if(!tagtinker_type_needs_wh_swap(TAGTINKER_TYPE_SMARTAG_COLOR_26)) { - fprintf(stderr, "FAIL type 1626 should swap W/H\n"); - failures++; - } - if(tagtinker_type_needs_wh_swap(1627)) { - fprintf(stderr, "FAIL type 1627 should not swap W/H\n"); - failures++; - } - - if(failures) { - fprintf(stderr, "%d failure(s)\n", failures); - return 1; - } - printf("ok\n"); - return 0; -}