From f3095f389f24fa07718aa2bfc9584890a413a15a Mon Sep 17 00:00:00 2001 From: i12bp8 Date: Tue, 7 Apr 2026 00:18:41 +0200 Subject: [PATCH] Add profile-aware transmit controls and image options --- application.fam | 1 + protocol/tagtinker_proto.c | 422 ++++++++++++++---- protocol/tagtinker_proto.h | 65 +++ scenes/tagtinker_scene.c | 3 + scenes/tagtinker_scene.h | 5 + scenes/tagtinker_scene_barcode_input.c | 11 +- scenes/tagtinker_scene_image_options.c | 196 +++++++++ scenes/tagtinker_scene_image_upload.c | 60 ++- scenes/tagtinker_scene_preset_list.c | 23 +- scenes/tagtinker_scene_settings.c | 136 ++++++ scenes/tagtinker_scene_size_picker.c | 217 +++++++--- scenes/tagtinker_scene_target_actions.c | 51 ++- scenes/tagtinker_scene_target_menu.c | 5 +- scenes/tagtinker_scene_transmit.c | 549 +++++++++++++++++++++++- tagtinker_app.c | 228 +++++++++- tagtinker_app.h | 51 ++- views/numlock_input.c | 53 ++- views/numlock_input.h | 4 +- views/tagtinker_font.h | 61 +++ 19 files changed, 1907 insertions(+), 234 deletions(-) create mode 100644 scenes/tagtinker_scene_image_options.c diff --git a/application.fam b/application.fam index 81bc63b..5710438 100644 --- a/application.fam +++ b/application.fam @@ -20,6 +20,7 @@ App( "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_settings.c", diff --git a/protocol/tagtinker_proto.c b/protocol/tagtinker_proto.c index c812179..6298737 100644 --- a/protocol/tagtinker_proto.c +++ b/protocol/tagtinker_proto.c @@ -13,6 +13,65 @@ /* ── Helpers ────────────────────────────────────────────────── */ +typedef struct { + uint16_t type_code; + uint16_t width; + uint16_t height; + TagTinkerTagKind kind; + TagTinkerTagColor color; +} TagTinkerProfileEntry; + +static const TagTinkerProfileEntry profile_table[] = { + {1206, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1207, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1217, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1219, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1240, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1241, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1242, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1243, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1265, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1275, 320, 192, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1276, 320, 140, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1291, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1300, 172, 72, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1314, 400, 300, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1315, 296, 128, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1317, 152, 152, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1318, 208, 112, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1319, 800, 480, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1322, 152, 152, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1324, 208, 112, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1327, 208, 112, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1328, 296, 128, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1336, 400, 300, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1339, 152, 152, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1340, 800, 480, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1344, 296, 128, TagTinkerTagKindDotMatrix, TagTinkerTagColorYellow}, + {1346, 800, 480, TagTinkerTagKindDotMatrix, TagTinkerTagColorYellow}, + {1348, 264, 176, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1349, 264, 176, TagTinkerTagKindDotMatrix, TagTinkerTagColorYellow}, + {1351, 648, 480, TagTinkerTagKindDotMatrix, TagTinkerTagColorMono}, + {1353, 648, 480, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1354, 648, 480, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1370, 296, 128, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1371, 648, 480, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1510, 0, 0, TagTinkerTagKindSegment, TagTinkerTagColorMono}, + {1627, 296, 128, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1628, 296, 128, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, + {1639, 152, 152, TagTinkerTagKindDotMatrix, TagTinkerTagColorRed}, +}; + +static const TagTinkerProfileEntry* find_profile_entry(uint16_t type_code) { + for(size_t i = 0; i < COUNT_OF(profile_table); i++) { + if(profile_table[i].type_code == type_code) { + return &profile_table[i]; + } + } + + return NULL; +} + static void append_word(uint8_t* buf, size_t* pos, uint16_t value) { buf[(*pos)++] = (value >> 8) & 0xFF; buf[(*pos)++] = value & 0xFF; @@ -74,6 +133,39 @@ bool tagtinker_barcode_to_plid(const char* barcode, uint8_t plid[4]) { return true; } +bool tagtinker_barcode_to_type(const char* barcode, uint16_t* type_code) { + if(!barcode || strlen(barcode) != 17 || !type_code) return false; + + uint16_t type = 0; + for(int i = 12; i < 16; i++) { + if(barcode[i] < '0' || barcode[i] > '9') return false; + type = (uint16_t)(type * 10 + (barcode[i] - '0')); + } + + *type_code = type; + return true; +} + +bool tagtinker_barcode_to_profile(const char* barcode, TagTinkerTagProfile* profile) { + if(!profile) return false; + + memset(profile, 0, sizeof(*profile)); + + uint16_t type_code = 0; + if(!tagtinker_barcode_to_type(barcode, &type_code)) return false; + + profile->type_code = type_code; + const TagTinkerProfileEntry* entry = find_profile_entry(type_code); + if(!entry) return true; + + profile->width = entry->width; + profile->height = entry->height; + profile->kind = entry->kind; + profile->color = entry->color; + profile->known = true; + return true; +} + /* ── Frame builders ─────────────────────────────────────────── */ size_t tagtinker_build_broadcast_page_frame( @@ -180,11 +272,238 @@ size_t tagtinker_rle_compress( return count < out_cap ? count : out_cap; } -/* ── Image upload sequence builder ──────────────────────────── */ +static inline uint8_t plane_pixel_at( + const uint8_t* primary_pixels, + const uint8_t* secondary_pixels, + size_t pixel_count, + size_t index) { + if(index < pixel_count) return primary_pixels[index]; + return secondary_pixels[index - pixel_count]; +} + +typedef struct { + uint8_t* data; + size_t bit_pos; +} TagTinkerBitWriter; + +static inline void bit_writer_append(TagTinkerBitWriter* writer, uint8_t bit) { + size_t byte_idx = writer->bit_pos / 8U; + size_t bit_idx = 7U - (writer->bit_pos % 8U); + if(bit) writer->data[byte_idx] |= (uint8_t)(1U << bit_idx); + writer->bit_pos++; +} + +static size_t record_run_bit_length(uint32_t run_count) { + size_t bit_count = 0; + do { + bit_count++; + run_count >>= 1; + } while(run_count); + + return (bit_count * 2U) - 1U; +} + +static void bit_writer_append_run(TagTinkerBitWriter* writer, uint32_t run_count) { + uint8_t bits[32]; + int n = 0; + uint32_t v = run_count; + while(v) { + bits[n++] = v & 1U; + v >>= 1; + } + + for(int i = 0; i < n / 2; i++) { + uint8_t t = bits[i]; + bits[i] = bits[n - 1 - i]; + bits[n - 1 - i] = t; + } + + for(int i = 1; i < n; i++) bit_writer_append(writer, 0U); + for(int i = 0; i < n; i++) bit_writer_append(writer, bits[i]); +} + +static size_t tagtinker_rle_planes_bit_length( + const uint8_t* primary_pixels, + const uint8_t* secondary_pixels, + size_t pixel_count) { + if(!primary_pixels) return 0; + + size_t total_count = secondary_pixels ? (pixel_count * 2U) : pixel_count; + if(total_count == 0) return 0; + + size_t bit_len = 1U; + uint8_t run_pixel = plane_pixel_at(primary_pixels, secondary_pixels, pixel_count, 0); + uint32_t run_count = 1; + + for(size_t i = 1; i < total_count; i++) { + uint8_t pixel = plane_pixel_at(primary_pixels, secondary_pixels, pixel_count, i); + if(pixel == run_pixel) { + run_count++; + } else { + bit_len += record_run_bit_length(run_count); + run_pixel = pixel; + run_count = 1; + } + } + + if(run_count > 1U) bit_len += record_run_bit_length(run_count); + return bit_len; +} + +static void tagtinker_pack_planes_raw( + const uint8_t* primary_pixels, + const uint8_t* secondary_pixels, + size_t pixel_count, + uint8_t* out) { + size_t total_count = secondary_pixels ? (pixel_count * 2U) : pixel_count; + TagTinkerBitWriter writer = {.data = out, .bit_pos = 0}; + + for(size_t i = 0; i < total_count; i++) { + bit_writer_append( + &writer, plane_pixel_at(primary_pixels, secondary_pixels, pixel_count, i)); + } +} + +static void tagtinker_pack_planes_rle( + const uint8_t* primary_pixels, + const uint8_t* secondary_pixels, + size_t pixel_count, + uint8_t* out) { + size_t total_count = secondary_pixels ? (pixel_count * 2U) : pixel_count; + if(total_count == 0) return; + + TagTinkerBitWriter writer = {.data = out, .bit_pos = 0}; + uint8_t run_pixel = plane_pixel_at(primary_pixels, secondary_pixels, pixel_count, 0); + uint32_t run_count = 1; + + bit_writer_append(&writer, run_pixel); + + for(size_t i = 1; i < total_count; i++) { + uint8_t pixel = plane_pixel_at(primary_pixels, secondary_pixels, pixel_count, i); + if(pixel == run_pixel) { + run_count++; + } else { + bit_writer_append_run(&writer, run_count); + run_pixel = pixel; + run_count = 1; + } + } + + if(run_count > 1U) bit_writer_append_run(&writer, run_count); +} #define DATA_BYTES_PER_FRAME 20 #define DATA_BITS_PER_FRAME (DATA_BYTES_PER_FRAME * 8) +bool tagtinker_encode_planes_payload( + const uint8_t* primary_pixels, + const uint8_t* secondary_pixels, + size_t pixel_count, + TagTinkerCompressionMode mode, + TagTinkerImagePayload* payload) { + if(!primary_pixels || !payload) return false; + + memset(payload, 0, sizeof(*payload)); + + size_t total_pixels = secondary_pixels ? (pixel_count * 2U) : pixel_count; + size_t comp_len = tagtinker_rle_planes_bit_length(primary_pixels, secondary_pixels, pixel_count); + bool use_compressed = false; + if(mode == TagTinkerCompressionRle) { + use_compressed = true; + } else if(mode == TagTinkerCompressionAuto) { + use_compressed = (comp_len > 0U) && (comp_len < total_pixels); + } + size_t src_len = use_compressed ? comp_len : total_pixels; + + size_t padding = (DATA_BITS_PER_FRAME - (src_len % DATA_BITS_PER_FRAME)) % DATA_BITS_PER_FRAME; + size_t padded_bits = src_len + padding; + size_t padded_bytes = padded_bits / 8U; + + uint8_t* data_bytes = calloc(padded_bytes, 1); + if(!data_bytes) return false; + + if(use_compressed) { + tagtinker_pack_planes_rle(primary_pixels, secondary_pixels, pixel_count, data_bytes); + } else { + tagtinker_pack_planes_raw(primary_pixels, secondary_pixels, pixel_count, data_bytes); + } + + payload->data = data_bytes; + payload->byte_count = padded_bytes; + payload->comp_type = use_compressed ? 2U : 0U; + return true; +} + +bool tagtinker_encode_image_payload( + const uint8_t* pixels, + uint16_t width, + uint16_t height, + bool color_clear, + TagTinkerCompressionMode mode, + TagTinkerImagePayload* payload) { + size_t pixel_count = (size_t)width * height; + uint8_t* second_plane = NULL; + + if(color_clear) { + second_plane = malloc(pixel_count); + if(!second_plane) return false; + memset(second_plane, 1, pixel_count); + } + + bool ok = tagtinker_encode_planes_payload(pixels, second_plane, pixel_count, mode, payload); + free(second_plane); + return ok; +} + +void tagtinker_free_image_payload(TagTinkerImagePayload* payload) { + if(!payload) return; + + free(payload->data); + payload->data = NULL; + payload->byte_count = 0; + payload->comp_type = 0; +} + +size_t tagtinker_make_image_param_frame( + uint8_t* buf, + const uint8_t plid[4], + uint16_t byte_count, + uint8_t comp_type, + uint8_t page, + uint16_t width, + uint16_t height, + uint16_t pos_x, + uint16_t pos_y) { + size_t p = mcu_frame(buf, plid, 0x05); + append_word(buf, &p, byte_count); + buf[p++] = 0x00; + buf[p++] = comp_type; + buf[p++] = page; + append_word(buf, &p, width); + append_word(buf, &p, height); + append_word(buf, &p, pos_x); + append_word(buf, &p, pos_y); + append_word(buf, &p, 0x0000); + buf[p++] = 0x88; + append_word(buf, &p, 0x0000); + for(int i = 0; i < 4; i++) buf[p++] = 0x00; + return terminate(buf, p); +} + +size_t tagtinker_make_image_data_frame( + uint8_t* buf, + const uint8_t plid[4], + uint16_t frame_index, + const uint8_t data_bytes[20]) { + size_t p = mcu_frame(buf, plid, 0x20); + append_word(buf, &p, frame_index); + memcpy(&buf[p], data_bytes, DATA_BYTES_PER_FRAME); + p += DATA_BYTES_PER_FRAME; + return terminate(buf, p); +} + +/* ── Image upload sequence builder ──────────────────────────── */ + void tagtinker_build_image_sequence( TagTinkerApp* app, const uint8_t plid[4], @@ -194,62 +513,20 @@ void tagtinker_build_image_sequence( uint16_t pos_x, uint16_t pos_y, uint16_t wake_repeats) { - size_t pixel_count = (size_t)width * height; - - /* Prepare pixel data — optionally add color-clear plane */ - size_t total_pixels; - uint8_t* combined = NULL; - const uint8_t* compress_src; - - if(app->color_clear) { - /* Two planes: BW + color-clear (removes red artifacts) */ - total_pixels = pixel_count * 2; - combined = malloc(total_pixels); - if(!combined) return; - memcpy(combined, pixels, pixel_count); - memset(combined + pixel_count, 1, pixel_count); /* 1 = Red Pigment OFF */ - compress_src = combined; - } else { - /* Single BW plane — faster, less data */ - total_pixels = pixel_count; - compress_src = pixels; - } - - /* Compress */ - size_t comp_cap = total_pixels + 256; - uint8_t* comp_bits = malloc(comp_cap); - if(!comp_bits) { if(combined) free(combined); return; } - - uint8_t comp_type; - size_t comp_len = tagtinker_rle_compress( - compress_src, total_pixels, comp_bits, comp_cap, &comp_type); - - const uint8_t* src_bits = (comp_type == 2) ? comp_bits : compress_src; - size_t src_len = (comp_type == 2) ? comp_len : total_pixels; - - /* Pack bits into bytes */ - size_t padding = (DATA_BITS_PER_FRAME - (src_len % DATA_BITS_PER_FRAME)) % DATA_BITS_PER_FRAME; - size_t padded_bits = src_len + padding; - size_t padded_bytes = padded_bits / 8; - - uint8_t* data_bytes = calloc(padded_bytes, 1); - if(!data_bytes) { free(comp_bits); free(combined); return; } - - for(size_t i = 0; i < src_len; i++) { - size_t byte_idx = i / 8; - size_t bit_idx = 7 - (i % 8); - if(byte_idx < padded_bytes && src_bits[i]) - data_bytes[byte_idx] |= (1 << bit_idx); - } - - size_t frame_count = padded_bytes / DATA_BYTES_PER_FRAME; + TagTinkerImagePayload payload; + if(!tagtinker_encode_image_payload( + pixels, width, height, app->color_clear, app->compression_mode, &payload)) + return; + 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", - width, height, page, comp_type, total_pixels, padded_bytes, frame_count); - - /* Free temp buffers */ - free(comp_bits); - if(combined) free(combined); + width, + height, + page, + payload.comp_type, + (size_t)width * height * (app->color_clear ? 2U : 1U), + payload.byte_count, + frame_count); /* Total: ping + params + N data + refresh */ size_t total = 2 + frame_count + 1; @@ -260,7 +537,7 @@ void tagtinker_build_image_sequence( app->frame_repeats = malloc(sizeof(uint16_t) * total); if(!app->frame_sequence || !app->frame_lengths || !app->frame_repeats) { - free(data_bytes); + tagtinker_free_image_payload(&payload); app->frame_seq_count = 0; return; } @@ -275,32 +552,25 @@ void tagtinker_build_image_sequence( /* 2. Parameters (cmd 0x05) */ app->frame_sequence[idx] = malloc(TAGTINKER_MAX_FRAME_SIZE); - size_t p = mcu_frame(app->frame_sequence[idx], plid, 0x05); - append_word(app->frame_sequence[idx], &p, (uint16_t)padded_bytes); - app->frame_sequence[idx][p++] = 0x00; - app->frame_sequence[idx][p++] = comp_type; - app->frame_sequence[idx][p++] = page; - append_word(app->frame_sequence[idx], &p, width); - append_word(app->frame_sequence[idx], &p, height); - append_word(app->frame_sequence[idx], &p, pos_x); - append_word(app->frame_sequence[idx], &p, pos_y); - append_word(app->frame_sequence[idx], &p, 0x0000); - app->frame_sequence[idx][p++] = 0x88; - append_word(app->frame_sequence[idx], &p, 0x0000); - for(int i = 0; i < 4; i++) app->frame_sequence[idx][p++] = 0x00; - app->frame_lengths[idx] = terminate(app->frame_sequence[idx], p); + app->frame_lengths[idx] = tagtinker_make_image_param_frame( + app->frame_sequence[idx], + plid, + (uint16_t)payload.byte_count, + payload.comp_type, + page, + width, + height, + pos_x, + pos_y); app->frame_repeats[idx] = 1; idx++; /* 3..N+2. Data frames (cmd 0x20) */ for(size_t fi = 0; fi < frame_count; fi++) { app->frame_sequence[idx] = malloc(TAGTINKER_MAX_FRAME_SIZE); - p = mcu_frame(app->frame_sequence[idx], plid, 0x20); - append_word(app->frame_sequence[idx], &p, (uint16_t)fi); size_t start = fi * DATA_BYTES_PER_FRAME; - memcpy(&app->frame_sequence[idx][p], &data_bytes[start], DATA_BYTES_PER_FRAME); - p += DATA_BYTES_PER_FRAME; - app->frame_lengths[idx] = terminate(app->frame_sequence[idx], p); + 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 */ idx++; } @@ -318,5 +588,5 @@ void tagtinker_build_image_sequence( app->frame_len = app->frame_lengths[1]; } - free(data_bytes); + tagtinker_free_image_payload(&payload); } diff --git a/protocol/tagtinker_proto.h b/protocol/tagtinker_proto.h index 265d213..8bf27fe 100644 --- a/protocol/tagtinker_proto.h +++ b/protocol/tagtinker_proto.h @@ -26,7 +26,72 @@ uint16_t tagtinker_crc16(const uint8_t* data, size_t len); /* ── Barcode / PLID ─────────────────────────────────────────── */ +typedef enum { + TagTinkerTagKindUnknown = 0, + TagTinkerTagKindDotMatrix, + TagTinkerTagKindSegment, +} TagTinkerTagKind; + +typedef enum { + TagTinkerTagColorMono = 0, + TagTinkerTagColorRed, + TagTinkerTagColorYellow, +} TagTinkerTagColor; + +typedef struct { + uint16_t type_code; + uint16_t width; + uint16_t height; + TagTinkerTagKind kind; + TagTinkerTagColor color; + bool known; +} TagTinkerTagProfile; + bool tagtinker_barcode_to_plid(const char* barcode, uint8_t plid[4]); +bool tagtinker_barcode_to_type(const char* barcode, uint16_t* type_code); +bool tagtinker_barcode_to_profile(const char* barcode, TagTinkerTagProfile* profile); + +typedef struct { + uint8_t* data; + size_t byte_count; + uint8_t comp_type; +} TagTinkerImagePayload; + +typedef enum { + TagTinkerCompressionAuto = 0, + TagTinkerCompressionRaw, + TagTinkerCompressionRle, +} TagTinkerCompressionMode; + +bool tagtinker_encode_image_payload( + const uint8_t* pixels, + uint16_t width, + uint16_t height, + bool color_clear, + TagTinkerCompressionMode mode, + TagTinkerImagePayload* payload); +bool tagtinker_encode_planes_payload( + const uint8_t* primary_pixels, + const uint8_t* secondary_pixels, + size_t pixel_count, + TagTinkerCompressionMode mode, + TagTinkerImagePayload* payload); +void tagtinker_free_image_payload(TagTinkerImagePayload* payload); +size_t tagtinker_make_image_param_frame( + uint8_t* buf, + const uint8_t plid[4], + uint16_t byte_count, + uint8_t comp_type, + uint8_t page, + uint16_t width, + uint16_t height, + uint16_t pos_x, + uint16_t pos_y); +size_t tagtinker_make_image_data_frame( + uint8_t* buf, + const uint8_t plid[4], + uint16_t frame_index, + const uint8_t data_bytes[20]); /* ── Frame builders ─────────────────────────────────────────── */ diff --git a/scenes/tagtinker_scene.c b/scenes/tagtinker_scene.c index 147e84d..130117f 100644 --- a/scenes/tagtinker_scene.c +++ b/scenes/tagtinker_scene.c @@ -17,6 +17,7 @@ void(*const tagtinker_scene_on_enter_handlers[])(void*) = { tagtinker_scene_preset_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, }; @@ -34,6 +35,7 @@ bool(*const tagtinker_scene_on_event_handlers[])(void*, SceneManagerEvent) = { tagtinker_scene_preset_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,6 +53,7 @@ void(*const tagtinker_scene_on_exit_handlers[])(void*) = { tagtinker_scene_preset_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, }; diff --git a/scenes/tagtinker_scene.h b/scenes/tagtinker_scene.h index 992ede5..50a3fa4 100644 --- a/scenes/tagtinker_scene.h +++ b/scenes/tagtinker_scene.h @@ -19,6 +19,7 @@ typedef enum { TagTinkerScenePresetList, TagTinkerSceneSizePicker, TagTinkerSceneImageUpload, + TagTinkerSceneImageOptions, TagTinkerSceneTransmit, TagTinkerSceneAbout, TagTinkerSceneCount, @@ -73,6 +74,10 @@ 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_image_options_on_enter(void* ctx); +bool tagtinker_scene_image_options_on_event(void* ctx, SceneManagerEvent event); +void tagtinker_scene_image_options_on_exit(void* ctx); + void tagtinker_scene_transmit_on_enter(void* ctx); bool tagtinker_scene_transmit_on_event(void* ctx, SceneManagerEvent event); void tagtinker_scene_transmit_on_exit(void* ctx); diff --git a/scenes/tagtinker_scene_barcode_input.c b/scenes/tagtinker_scene_barcode_input.c index 2f6e237..2708604 100644 --- a/scenes/tagtinker_scene_barcode_input.c +++ b/scenes/tagtinker_scene_barcode_input.c @@ -1,6 +1,6 @@ /* - * Barcode Input — custom numlock digit selector - * Auto-prefixes 'N', user enters 16 digits with arrow keys. + * Barcode Input — custom barcode selector + * User enters 1 letter + 16 digits with arrow keys. */ #include "../tagtinker_app.h" @@ -33,7 +33,7 @@ bool tagtinker_scene_barcode_input_on_event(void* ctx, SceneManagerEvent event) popup_reset(app->popup); popup_set_header(app->popup, "Invalid Barcode", 64, 20, AlignCenter, AlignCenter); popup_set_text(app->popup, - "Format: N + 4 + 14 digits", + "Format: Letter + 16 digits", 64, 40, AlignCenter, AlignCenter); popup_set_timeout(app->popup, 2000); popup_enable_timeout(app->popup); @@ -62,6 +62,7 @@ bool tagtinker_scene_barcode_input_on_event(void* ctx, SceneManagerEvent event) 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++; @@ -70,6 +71,10 @@ bool tagtinker_scene_barcode_input_on_event(void* ctx, SceneManagerEvent event) } } + if(app->selected_target >= 0) { + tagtinker_select_target(app, (uint8_t)app->selected_target); + } + uint32_t target_scene = scene_manager_get_scene_state( app->scene_manager, TagTinkerSceneBarcodeInput); scene_manager_next_scene(app->scene_manager, target_scene); diff --git a/scenes/tagtinker_scene_image_options.c b/scenes/tagtinker_scene_image_options.c new file mode 100644 index 0000000..6206a8b --- /dev/null +++ b/scenes/tagtinker_scene_image_options.c @@ -0,0 +1,196 @@ +#include "../tagtinker_app.h" + +enum { + ImageOptionPage, + ImageOptionOffsetX, + ImageOptionOffsetY, + ImageOptionCompression, + ImageOptionFrameRepeat, + ImageOptionTransmit, +}; + +static const char* image_option_compression_labels[] = {"Auto", "Raw", "RLE"}; +static const uint16_t image_option_coord_values[] = { + 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, + 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256, 264, + 272, 280, 288, 296, 304, 312, 320, 328, 336, 344, 352, 360, 368, 376, 384, 392, 400, + 408, 416, 424, 432, 440, 448, 456, 464, 472, 480, 488, 496, 504, 512, 520, 528, 536, + 544, 552, 560, 568, 576, 584, 592, 600, 608, 616, 624, 632, 640, 648, 656, 664, 672, + 680, 688, 696, 704, 712, 720, 728, 736, 744, 752, 760, 768, 776, 784, 792, 800, +}; + +#define IMAGE_OPTION_COORD_COUNT COUNT_OF(image_option_coord_values) + +static uint8_t image_option_nearest_coord_index(uint16_t value) { + uint8_t best_idx = 0; + uint16_t best_diff = UINT16_MAX; + + for(uint8_t i = 0; i < IMAGE_OPTION_COORD_COUNT; i++) { + uint16_t current = image_option_coord_values[i]; + uint16_t diff = (current > value) ? (current - value) : (value - current); + if(diff < best_diff) { + best_diff = diff; + best_idx = i; + } + } + + return best_idx; +} + +static void image_option_clamp_position(TagTinkerApp* app) { + if(!app) return; + if(app->selected_target < 0 || app->selected_target >= app->target_count) return; + + TagTinkerImageTxJob* job = &app->image_tx_job; + const TagTinkerTarget* target = &app->targets[app->selected_target]; + + if(!target->profile.known || !target->profile.width || !target->profile.height) return; + + uint16_t max_x = (job->width < target->profile.width) ? + (uint16_t)(target->profile.width - job->width) : + 0U; + uint16_t max_y = (job->height < target->profile.height) ? + (uint16_t)(target->profile.height - job->height) : + 0U; + + if(job->pos_x > max_x) job->pos_x = max_x; + if(job->pos_y > max_y) job->pos_y = max_y; +} + +static void image_option_page_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + app->image_tx_job.page = variable_item_get_current_value_index(item); + app->img_page = app->image_tx_job.page; + + char buf[4]; + snprintf(buf, sizeof(buf), "%u", app->image_tx_job.page); + variable_item_set_current_value_text(item, buf); +} + +static void image_option_offset_x_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + if(index >= IMAGE_OPTION_COORD_COUNT) index = IMAGE_OPTION_COORD_COUNT - 1U; + + app->image_tx_job.pos_x = image_option_coord_values[index]; + image_option_clamp_position(app); + app->draw_x = app->image_tx_job.pos_x; + + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->image_tx_job.pos_x); + variable_item_set_current_value_text(item, buf); +} + +static void image_option_offset_y_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + if(index >= IMAGE_OPTION_COORD_COUNT) index = IMAGE_OPTION_COORD_COUNT - 1U; + + app->image_tx_job.pos_y = image_option_coord_values[index]; + image_option_clamp_position(app); + app->draw_y = app->image_tx_job.pos_y; + + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->image_tx_job.pos_y); + variable_item_set_current_value_text(item, buf); +} + +static void image_option_compression_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + if(index > TagTinkerCompressionRle) index = TagTinkerCompressionRle; + + app->compression_mode = (TagTinkerCompressionMode)index; + variable_item_set_current_value_text(item, image_option_compression_labels[index]); +} + +static void image_option_frame_repeat_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + app->data_frame_repeats = index + 1U; + + char buf[4]; + snprintf(buf, sizeof(buf), "%u", app->data_frame_repeats); + variable_item_set_current_value_text(item, buf); +} + +static void image_option_enter_cb(void* ctx, uint32_t index) { + TagTinkerApp* app = ctx; + + if(index != ImageOptionTransmit) return; + + image_option_clamp_position(app); + app->tx_spam = false; + scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit); +} + +void tagtinker_scene_image_options_on_enter(void* ctx) { + TagTinkerApp* app = ctx; + VariableItemList* list = app->var_item_list; + TagTinkerImageTxJob* job = &app->image_tx_job; + + if(job->mode != TagTinkerTxModeBmpImage) { + scene_manager_previous_scene(app->scene_manager); + return; + } + + image_option_clamp_position(app); + variable_item_list_reset(list); + + VariableItem* item = + variable_item_list_add(list, "Page", 8, image_option_page_changed, app); + variable_item_set_current_value_index(item, job->page); + { + char buf[4]; + snprintf(buf, sizeof(buf), "%u", job->page); + variable_item_set_current_value_text(item, buf); + } + + item = variable_item_list_add( + list, "Offset X", IMAGE_OPTION_COORD_COUNT, image_option_offset_x_changed, app); + variable_item_set_current_value_index(item, image_option_nearest_coord_index(job->pos_x)); + { + char buf[8]; + snprintf(buf, sizeof(buf), "%u", job->pos_x); + variable_item_set_current_value_text(item, buf); + } + + item = variable_item_list_add( + list, "Offset Y", IMAGE_OPTION_COORD_COUNT, image_option_offset_y_changed, app); + variable_item_set_current_value_index(item, image_option_nearest_coord_index(job->pos_y)); + { + char buf[8]; + snprintf(buf, sizeof(buf), "%u", job->pos_y); + variable_item_set_current_value_text(item, buf); + } + + item = variable_item_list_add( + list, "Compression", 3, image_option_compression_changed, app); + variable_item_set_current_value_index(item, app->compression_mode); + variable_item_set_current_value_text(item, image_option_compression_labels[app->compression_mode]); + + item = variable_item_list_add( + list, "Frame Repeat", 5, image_option_frame_repeat_changed, app); + variable_item_set_current_value_index(item, app->data_frame_repeats - 1U); + { + char buf[4]; + snprintf(buf, sizeof(buf), "%u", app->data_frame_repeats); + variable_item_set_current_value_text(item, buf); + } + + variable_item_list_add(list, ">> Send BMP <<", 0, NULL, app); + variable_item_list_set_enter_callback(list, image_option_enter_cb, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewVarItemList); +} + +bool tagtinker_scene_image_options_on_event(void* ctx, SceneManagerEvent event) { + UNUSED(ctx); + UNUSED(event); + return false; +} + +void tagtinker_scene_image_options_on_exit(void* ctx) { + TagTinkerApp* app = ctx; + variable_item_list_reset(app->var_item_list); +} diff --git a/scenes/tagtinker_scene_image_upload.c b/scenes/tagtinker_scene_image_upload.c index 3eaef39..4c03dc3 100644 --- a/scenes/tagtinker_scene_image_upload.c +++ b/scenes/tagtinker_scene_image_upload.c @@ -47,43 +47,31 @@ void tagtinker_scene_image_upload_on_enter(void* ctx) { if(bpp == 1) { size_t w = (size_t)bmp_w; size_t h = (size_t)bmp_h; - uint8_t* img_data = malloc(w * h); - memset(img_data, 0, w * h); - - uint32_t row_stride = ((bmp_w + 31) / 32) * 4; - uint8_t* row_buf = malloc(row_stride); - - storage_file_seek(file, data_offset, true); - - for(int32_t y = 0; y < bmp_h; y++) { - /* target_y is the logical row index 0..h-1 */ - int32_t target_y = top_down ? y : (bmp_h - 1 - y); - storage_file_read(file, row_buf, row_stride); - - if(target_y >= 0 && target_y < (int32_t)h) { - for(int32_t x = 0; x < bmp_w && x < (int32_t)w; x++) { - uint8_t byte = row_buf[x / 8]; - uint8_t bit = (byte >> (7 - (x % 8))) & 1; - img_data[target_y * w + x] = (bit == 0) ? 0 : 1; - } - } - } - free(row_buf); - - /* Force color-clear and use BMP's own dimensions */ - bool prev_clr = app->color_clear; - app->color_clear = true; - tagtinker_build_image_sequence(app, target->plid, img_data, (uint16_t)w, (uint16_t)h, 0, 0, 0, 250); - app->color_clear = prev_clr; - - memcpy(app->frame_buf, app->frame_sequence[0], app->frame_lengths[0]); - app->frame_len = app->frame_lengths[0]; - free(img_data); - - app->tx_spam = false; - scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit); + 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, "Must be 1-bit BMP"); + show_error_dialog(app, "Use 1/24/32-bit BMP"); } } else { show_error_dialog(app, "Invalid BMP magic"); diff --git a/scenes/tagtinker_scene_preset_list.c b/scenes/tagtinker_scene_preset_list.c index cf81f1b..d757582 100644 --- a/scenes/tagtinker_scene_preset_list.c +++ b/scenes/tagtinker_scene_preset_list.c @@ -6,9 +6,6 @@ */ #include "../tagtinker_app.h" -#include "../views/tagtinker_font.h" -#include "../protocol/tagtinker_proto.h" - #define EVT_ADD_NEW 200 #define EVT_PRESET 0 @@ -116,26 +113,8 @@ bool tagtinker_scene_preset_list_on_event(void* ctx, SceneManagerEvent event) { FURI_LOG_I(TAGTINKER_TAG, "Preset %lu: %ux%u \"%s\"", idx, app->esl_width, app->esl_height, app->text_input_buf); - /* Render + transmit */ - size_t num_pixels = (size_t)app->esl_width * app->esl_height; - uint8_t* pixels = malloc(num_pixels); - if(!pixels) return false; - - if(app->invert_text) { - render_text_ex(pixels, app->esl_width, app->esl_height, - app->text_input_buf, 0, 1); - } else { - render_text(pixels, app->esl_width, app->esl_height, - app->text_input_buf); - } - TagTinkerTarget* target = &app->targets[app->selected_target]; - tagtinker_build_image_sequence( - app, target->plid, pixels, - app->esl_width, app->esl_height, app->img_page, - 0, 0, 250); - - free(pixels); + tagtinker_prepare_text_tx(app, target->plid); app->tx_spam = false; scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit); return true; diff --git a/scenes/tagtinker_scene_settings.c b/scenes/tagtinker_scene_settings.c index 7264397..99688f0 100644 --- a/scenes/tagtinker_scene_settings.c +++ b/scenes/tagtinker_scene_settings.c @@ -6,9 +6,42 @@ enum { SettingsItemStartupWarning, + SettingsItemSignalMode, + SettingsItemCompression, + SettingsItemFrameRepeat, + SettingsItemOffsetX, + SettingsItemOffsetY, }; static const char* settings_toggle_labels[] = {"Off", "On"}; +static const char* settings_signal_labels[] = {"PP4", "PP16"}; +static const char* settings_compression_labels[] = {"Auto", "Raw", "RLE"}; +static const uint16_t settings_coord_values[] = { + 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, + 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256, 264, + 272, 280, 288, 296, 304, 312, 320, 328, 336, 344, 352, 360, 368, 376, 384, 392, 400, + 408, 416, 424, 432, 440, 448, 456, 464, 472, 480, 488, 496, 504, 512, 520, 528, 536, + 544, 552, 560, 568, 576, 584, 592, 600, 608, 616, 624, 632, 640, 648, 656, 664, 672, + 680, 688, 696, 704, 712, 720, 728, 736, 744, 752, 760, 768, 776, 784, 792, 800, +}; + +#define SETTINGS_COORD_COUNT COUNT_OF(settings_coord_values) + +static uint8_t nearest_coord_index(uint16_t value) { + uint8_t best_idx = 0; + uint16_t best_diff = UINT16_MAX; + + for(uint8_t i = 0; i < SETTINGS_COORD_COUNT; i++) { + uint16_t current = settings_coord_values[i]; + uint16_t diff = (current > value) ? (current - value) : (value - current); + if(diff < best_diff) { + best_diff = diff; + best_idx = i; + } + } + + return best_idx; +} static void startup_warning_changed(VariableItem* item) { TagTinkerApp* app = variable_item_get_context(item); @@ -22,6 +55,77 @@ static void startup_warning_changed(VariableItem* item) { } } +static void signal_mode_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + if(index > TagTinkerSignalPP16) index = TagTinkerSignalPP16; + app->signal_mode = (TagTinkerSignalMode)index; + variable_item_set_current_value_text(item, settings_signal_labels[index]); + + if(!tagtinker_settings_save(app)) { + FURI_LOG_W(TAGTINKER_TAG, "Failed to save settings"); + } +} + +static void compression_mode_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + if(index > TagTinkerCompressionRle) index = TagTinkerCompressionRle; + app->compression_mode = (TagTinkerCompressionMode)index; + variable_item_set_current_value_text(item, settings_compression_labels[index]); + + if(!tagtinker_settings_save(app)) { + FURI_LOG_W(TAGTINKER_TAG, "Failed to save settings"); + } +} + +static void frame_repeat_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + + app->data_frame_repeats = index + 1U; + + char buf[4]; + snprintf(buf, sizeof(buf), "%u", app->data_frame_repeats); + variable_item_set_current_value_text(item, buf); + + if(!tagtinker_settings_save(app)) { + FURI_LOG_W(TAGTINKER_TAG, "Failed to save settings"); + } +} + +static void offset_x_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + if(index >= SETTINGS_COORD_COUNT) index = SETTINGS_COORD_COUNT - 1U; + app->draw_x = settings_coord_values[index]; + + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->draw_x); + variable_item_set_current_value_text(item, buf); + + if(!tagtinker_settings_save(app)) { + FURI_LOG_W(TAGTINKER_TAG, "Failed to save settings"); + } +} + +static void offset_y_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + if(index >= SETTINGS_COORD_COUNT) index = SETTINGS_COORD_COUNT - 1U; + app->draw_y = settings_coord_values[index]; + + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->draw_y); + variable_item_set_current_value_text(item, buf); + + if(!tagtinker_settings_save(app)) { + FURI_LOG_W(TAGTINKER_TAG, "Failed to save settings"); + } +} + void tagtinker_scene_settings_on_enter(void* ctx) { TagTinkerApp* app = ctx; VariableItemList* list = app->var_item_list; @@ -34,6 +138,38 @@ void tagtinker_scene_settings_on_enter(void* ctx) { variable_item_set_current_value_text( item, settings_toggle_labels[app->show_startup_warning ? 1 : 0]); + item = variable_item_list_add(list, "Signal Mode", 2, signal_mode_changed, app); + variable_item_set_current_value_index(item, app->signal_mode); + variable_item_set_current_value_text(item, settings_signal_labels[app->signal_mode]); + + item = variable_item_list_add(list, "Compression", 3, compression_mode_changed, app); + variable_item_set_current_value_index(item, app->compression_mode); + variable_item_set_current_value_text(item, settings_compression_labels[app->compression_mode]); + + item = variable_item_list_add(list, "Frame Repeat", 5, frame_repeat_changed, app); + variable_item_set_current_value_index(item, app->data_frame_repeats - 1U); + { + char buf[4]; + snprintf(buf, sizeof(buf), "%u", app->data_frame_repeats); + variable_item_set_current_value_text(item, buf); + } + + item = variable_item_list_add(list, "Offset X", SETTINGS_COORD_COUNT, offset_x_changed, app); + variable_item_set_current_value_index(item, nearest_coord_index(app->draw_x)); + { + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->draw_x); + variable_item_set_current_value_text(item, buf); + } + + item = variable_item_list_add(list, "Offset Y", SETTINGS_COORD_COUNT, offset_y_changed, app); + variable_item_set_current_value_index(item, nearest_coord_index(app->draw_y)); + { + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->draw_y); + variable_item_set_current_value_text(item, buf); + } + variable_item_list_set_selected_item(list, SettingsItemStartupWarning); view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewVarItemList); } diff --git a/scenes/tagtinker_scene_size_picker.c b/scenes/tagtinker_scene_size_picker.c index 49c0064..b29c0da 100644 --- a/scenes/tagtinker_scene_size_picker.c +++ b/scenes/tagtinker_scene_size_picker.c @@ -1,5 +1,5 @@ /* - * Size Picker — custom W/H (5px steps), mode, page, color, transmit + * Size Picker — exact native sizes plus a useful custom range. */ #include "../tagtinker_app.h" @@ -7,48 +7,105 @@ #include "../protocol/tagtinker_proto.h" #include -/* Limits */ -#define MIN_W 48 -#define MAX_W 200 -#define STEP_W 8 -#define MIN_H 50 -#define MAX_H 90 -#define STEP_H 5 +static const uint16_t width_values[] = { + 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 172, + 176, 184, 192, 200, 208, 224, 240, 256, 264, 272, 288, 296, 304, 320, 400, 648, 800, +}; -#define W_COUNT ((MAX_W - MIN_W) / STEP_W + 1) -#define H_COUNT ((MAX_H - MIN_H) / STEP_H + 1) +static const uint16_t height_values[] = { + 50, 55, 60, 65, 70, 72, 75, 80, 85, 90, 96, 104, 112, 120, 128, 140, 152, 176, 192, 300, 480, +}; + +static const uint16_t coord_values[] = { + 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, + 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256, 264, + 272, 280, 288, 296, 304, 312, 320, 328, 336, 344, 352, 360, 368, 376, 384, 392, 400, + 408, 416, 424, 432, 440, 448, 456, 464, 472, 480, 488, 496, 504, 512, 520, 528, 536, + 544, 552, 560, 568, 576, 584, 592, 600, 608, 616, 624, 632, 640, 648, 656, 664, 672, + 680, 688, 696, 704, 712, 720, 728, 736, 744, 752, 760, 768, 776, 784, 792, 800, +}; + +static const char* compression_labels[] = {"Auto", "Raw", "RLE"}; + +#define W_COUNT COUNT_OF(width_values) +#define H_COUNT COUNT_OF(height_values) +#define COORD_COUNT COUNT_OF(coord_values) /* Setting indices */ enum { SettingWidth, SettingHeight, SettingPage, - SettingColor, + SettingPolarity, SettingMode, + SettingCompression, + SettingFrameRepeat, + SettingOffsetX, + SettingOffsetY, SettingSave, SettingTransmit, }; /* ── Callbacks ── */ +static void clamp_current_offsets(TagTinkerApp* app); + static void width_changed(VariableItem* item) { TagTinkerApp* app = variable_item_get_context(item); uint8_t idx = variable_item_get_current_value_index(item); - app->esl_width = MIN_W + idx * STEP_W; + if(idx >= W_COUNT) idx = W_COUNT - 1; + app->esl_width = width_values[idx]; char buf[8]; snprintf(buf, sizeof(buf), "%u", app->esl_width); variable_item_set_current_value_text(item, buf); + clamp_current_offsets(app); } static void height_changed(VariableItem* item) { TagTinkerApp* app = variable_item_get_context(item); uint8_t idx = variable_item_get_current_value_index(item); - app->esl_height = MIN_H + idx * STEP_H; + if(idx >= H_COUNT) idx = H_COUNT - 1; + app->esl_height = height_values[idx]; char buf[8]; snprintf(buf, sizeof(buf), "%u", app->esl_height); variable_item_set_current_value_text(item, buf); + clamp_current_offsets(app); +} + +static uint8_t nearest_value_index(const uint16_t* values, uint8_t count, uint16_t target) { + uint8_t best_idx = 0; + uint16_t best_diff = UINT16_MAX; + + for(uint8_t i = 0; i < count; i++) { + uint16_t value = values[i]; + uint16_t diff = (value > target) ? (value - target) : (target - value); + if(diff < best_diff) { + best_diff = diff; + best_idx = i; + } + } + + return best_idx; +} + +static void clamp_current_offsets(TagTinkerApp* app) { + if(!app) return; + if(app->selected_target < 0 || app->selected_target >= app->target_count) return; + + const TagTinkerTarget* target = &app->targets[app->selected_target]; + if(!target->profile.known || !target->profile.width || !target->profile.height) return; + + uint16_t max_x = (app->esl_width < target->profile.width) ? + (uint16_t)(target->profile.width - app->esl_width) : + 0U; + uint16_t max_y = (app->esl_height < target->profile.height) ? + (uint16_t)(target->profile.height - app->esl_height) : + 0U; + + if(app->draw_x > max_x) app->draw_x = max_x; + if(app->draw_y > max_y) app->draw_y = max_y; } static void page_changed(VariableItem* item) { @@ -60,7 +117,7 @@ static void page_changed(VariableItem* item) { variable_item_set_current_value_text(item, buf); } -static void color_changed(VariableItem* item) { +static void polarity_changed(VariableItem* item) { TagTinkerApp* app = variable_item_get_context(item); app->invert_text = (variable_item_get_current_value_index(item) == 1); @@ -70,10 +127,55 @@ static void color_changed(VariableItem* item) { static void mode_changed(VariableItem* item) { TagTinkerApp* app = variable_item_get_context(item); + TagTinkerTarget* target = (app->selected_target >= 0) ? &app->targets[app->selected_target] : NULL; + bool accent = tagtinker_target_supports_accent(target); app->color_clear = (variable_item_get_current_value_index(item) == 1); variable_item_set_current_value_text( - item, app->color_clear ? "BW+Clr" : "BW Fast"); + item, + accent ? (app->color_clear ? "Accent" : "Black") + : (app->color_clear ? "Dual Plane" : "Mono Fast")); +} + +static void compression_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + if(index > TagTinkerCompressionRle) index = TagTinkerCompressionRle; + app->compression_mode = (TagTinkerCompressionMode)index; + variable_item_set_current_value_text(item, compression_labels[index]); +} + +static void frame_repeat_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + app->data_frame_repeats = variable_item_get_current_value_index(item) + 1U; + + char buf[4]; + snprintf(buf, sizeof(buf), "%u", app->data_frame_repeats); + variable_item_set_current_value_text(item, buf); +} + +static void offset_x_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + if(index >= COORD_COUNT) index = COORD_COUNT - 1U; + app->draw_x = coord_values[index]; + clamp_current_offsets(app); + + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->draw_x); + variable_item_set_current_value_text(item, buf); +} + +static void offset_y_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + uint8_t index = variable_item_get_current_value_index(item); + if(index >= COORD_COUNT) index = COORD_COUNT - 1U; + app->draw_y = coord_values[index]; + clamp_current_offsets(app); + + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->draw_y); + variable_item_set_current_value_text(item, buf); } static void save_presets_to_sd(TagTinkerApp* app) { @@ -128,24 +230,8 @@ static void setting_cb(void* ctx, uint32_t index) { app->esl_width, app->esl_height, app->img_page, app->invert_text, app->color_clear); - size_t num_pixels = (size_t)app->esl_width * app->esl_height; - uint8_t* pixels = malloc(num_pixels); - if(!pixels) return; - - if(app->invert_text) { - render_text_ex(pixels, app->esl_width, app->esl_height, app->text_input_buf, 0, 1); - } else { - render_text(pixels, app->esl_width, app->esl_height, app->text_input_buf); - } - TagTinkerTarget* target = &app->targets[app->selected_target]; - - tagtinker_build_image_sequence( - app, target->plid, pixels, - app->esl_width, app->esl_height, app->img_page, - 0, 0, 250); - - free(pixels); + tagtinker_prepare_text_tx(app, target->plid); app->tx_spam = false; scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit); } @@ -157,20 +243,11 @@ void tagtinker_scene_size_picker_on_enter(void* ctx) { variable_item_list_reset(app->var_item_list); - /* Clamp to valid grid positions */ - if(app->esl_width < MIN_W) app->esl_width = MIN_W; - if(app->esl_width > MAX_W) app->esl_width = MAX_W; - if(app->esl_height < MIN_H) app->esl_height = MIN_H; - if(app->esl_height > MAX_H) app->esl_height = MAX_H; - - /* Snap to nearest step */ - uint8_t w_idx = (app->esl_width - MIN_W + STEP_W / 2) / STEP_W; - if(w_idx >= W_COUNT) w_idx = W_COUNT - 1; - app->esl_width = MIN_W + w_idx * STEP_W; - - uint8_t h_idx = (app->esl_height - MIN_H + STEP_H / 2) / STEP_H; - if(h_idx >= H_COUNT) h_idx = H_COUNT - 1; - app->esl_height = MIN_H + h_idx * STEP_H; + uint8_t w_idx = nearest_value_index(width_values, W_COUNT, app->esl_width); + uint8_t h_idx = nearest_value_index(height_values, H_COUNT, app->esl_height); + app->esl_width = width_values[w_idx]; + app->esl_height = height_values[h_idx]; + clamp_current_offsets(app); /* Width */ VariableItem* item_w = variable_item_list_add( @@ -202,19 +279,57 @@ void tagtinker_scene_size_picker_on_enter(void* ctx) { variable_item_set_current_value_text(item_pg, buf); } - /* Color (text polarity) */ + /* Polarity */ VariableItem* item_col = variable_item_list_add( - app->var_item_list, "Color", 2, color_changed, app); + app->var_item_list, "Polarity", 2, polarity_changed, app); variable_item_set_current_value_index(item_col, app->invert_text ? 1 : 0); variable_item_set_current_value_text( item_col, app->invert_text ? "W on B" : "B on W"); - /* Mode (BW Fast vs BW+Color clear) */ + /* Text ink / plane mode */ + TagTinkerTarget* target = (app->selected_target >= 0) ? &app->targets[app->selected_target] : NULL; + bool accent = tagtinker_target_supports_accent(target); VariableItem* item_mode = variable_item_list_add( - app->var_item_list, "Mode", 2, mode_changed, app); + app->var_item_list, accent ? "Ink" : "Mode", 2, mode_changed, app); variable_item_set_current_value_index(item_mode, app->color_clear ? 1 : 0); variable_item_set_current_value_text( - item_mode, app->color_clear ? "BW+Clr" : "BW Fast"); + item_mode, + accent ? (app->color_clear ? "Accent" : "Black") + : (app->color_clear ? "Dual Plane" : "Mono Fast")); + + VariableItem* item_comp = variable_item_list_add( + app->var_item_list, "Compression", 3, compression_changed, app); + variable_item_set_current_value_index(item_comp, app->compression_mode); + variable_item_set_current_value_text(item_comp, compression_labels[app->compression_mode]); + + VariableItem* item_rep = variable_item_list_add( + app->var_item_list, "Frame Repeat", 5, frame_repeat_changed, app); + variable_item_set_current_value_index(item_rep, app->data_frame_repeats - 1U); + { + char buf[4]; + snprintf(buf, sizeof(buf), "%u", app->data_frame_repeats); + variable_item_set_current_value_text(item_rep, buf); + } + + VariableItem* item_x = variable_item_list_add( + app->var_item_list, "Offset X", COORD_COUNT, offset_x_changed, app); + variable_item_set_current_value_index( + item_x, nearest_value_index(coord_values, COORD_COUNT, app->draw_x)); + { + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->draw_x); + variable_item_set_current_value_text(item_x, buf); + } + + VariableItem* item_y = variable_item_list_add( + app->var_item_list, "Offset Y", COORD_COUNT, offset_y_changed, app); + variable_item_set_current_value_index( + item_y, nearest_value_index(coord_values, COORD_COUNT, app->draw_y)); + { + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->draw_y); + variable_item_set_current_value_text(item_y, buf); + } /* Save preset button */ variable_item_list_add(app->var_item_list, "[*] Save Preset", 0, NULL, app); diff --git a/scenes/tagtinker_scene_target_actions.c b/scenes/tagtinker_scene_target_actions.c index 379fdd8..0ba03e8 100644 --- a/scenes/tagtinker_scene_target_actions.c +++ b/scenes/tagtinker_scene_target_actions.c @@ -3,22 +3,64 @@ */ #include "../tagtinker_app.h" +#include static void target_actions_cb(void* ctx, uint32_t index) { TagTinkerApp* app = ctx; view_dispatcher_send_custom_event(app->view_dispatcher, index); } +static void show_target_details(TagTinkerApp* app, const TagTinkerTarget* target) { + if(!target) return; + + char body[160]; + if(target->profile.known && target->profile.width && target->profile.height) { + snprintf( + body, + sizeof(body), + "Type: %u\nKind: %s\nSize: %ux%u\nColor: %s", + target->profile.type_code, + tagtinker_profile_kind_label(target->profile.kind), + target->profile.width, + target->profile.height, + tagtinker_profile_color_label(target->profile.color)); + } else if(target->profile.known) { + snprintf( + body, + sizeof(body), + "Type: %u\nKind: %s\nColor: %s", + target->profile.type_code, + tagtinker_profile_kind_label(target->profile.kind), + tagtinker_profile_color_label(target->profile.color)); + } else { + snprintf(body, sizeof(body), "Type: %u\nProfile: Unknown", target->profile.type_code); + } + + DialogMessage* message = dialog_message_alloc(); + dialog_message_set_header(message, "Tag Details", 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); +} + void tagtinker_scene_target_actions_on_enter(void* ctx) { TagTinkerApp* app = ctx; + TagTinkerTarget* target = (app->selected_target >= 0) ? &app->targets[app->selected_target] : NULL; + bool allow_graphics = tagtinker_target_supports_graphics(target); submenu_reset(app->submenu); char header[24]; snprintf(header, sizeof(header), "Tag: %.8s...", app->barcode); submenu_set_header(app->submenu, header); - 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, "Tag Details", TagTinkerTargetDetails, 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, "LED Response Check", TagTinkerTargetPingFlash, target_actions_cb, app); view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewSubmenu); @@ -29,10 +71,15 @@ bool tagtinker_scene_target_actions_on_event(void* ctx, SceneManagerEvent event) if(event.type != SceneManagerEventTypeCustom) return false; switch(event.event) { + case TagTinkerTargetDetails: + show_target_details(app, &app->targets[app->selected_target]); + 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: + if(!tagtinker_target_supports_graphics(&app->targets[app->selected_target])) return true; scene_manager_next_scene(app->scene_manager, TagTinkerSceneImageUpload); return true; case TagTinkerTargetPingFlash: diff --git a/scenes/tagtinker_scene_target_menu.c b/scenes/tagtinker_scene_target_menu.c index 89fd36b..a46a2a1 100644 --- a/scenes/tagtinker_scene_target_menu.c +++ b/scenes/tagtinker_scene_target_menu.c @@ -50,10 +50,7 @@ bool tagtinker_scene_target_menu_on_event(void* ctx, SceneManagerEvent event) { /* Selected a saved target */ if(event.event < app->target_count) { - app->selected_target = (int8_t)event.event; - memcpy(app->barcode, app->targets[event.event].barcode, TAGTINKER_BC_LEN + 1); - memcpy(app->plid, app->targets[event.event].plid, 4); - app->barcode_valid = true; + tagtinker_select_target(app, (uint8_t)event.event); scene_manager_next_scene(app->scene_manager, TagTinkerSceneTargetActions); return true; } diff --git a/scenes/tagtinker_scene_transmit.c b/scenes/tagtinker_scene_transmit.c index 1550689..bb324f2 100644 --- a/scenes/tagtinker_scene_transmit.c +++ b/scenes/tagtinker_scene_transmit.c @@ -4,7 +4,9 @@ */ #include "../tagtinker_app.h" +#include "../views/tagtinker_font.h" #include +#include typedef struct { TagTinkerApp* app; @@ -13,6 +15,534 @@ typedef struct { bool ok; } TxViewModel; +typedef struct { + uint32_t data_offset; + uint32_t row_stride; + uint16_t width; + uint16_t height; + uint16_t bpp; + bool top_down; +} TxBmpInfo; + +static bool tx_is_pp16(const TagTinkerApp* app); + +#define TX_FULL_JOB_PIXEL_LIMIT 49152U + +static uint16_t tx_pick_chunk_height(uint16_t width, uint16_t height, bool second_plane); + +static uint16_t tx_apply_signal_mode(const TagTinkerApp* app, uint16_t repeats) { + if(repeats & 0x8000U) return repeats; + + if(tx_is_pp16(app)) { + return (uint16_t)(repeats | 0x8000U); + } + + return repeats; +} + +static TagTinkerTagColor tx_target_color(const TagTinkerApp* app) { + if(app->selected_target < 0 || app->selected_target >= app->target_count) { + return TagTinkerTagColorMono; + } + + return app->targets[app->selected_target].profile.color; +} + +static bool tx_is_pp16(const TagTinkerApp* app) { + return app->signal_mode == TagTinkerSignalPP16; +} + +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), 10); +} + +static bool tx_send_ping(TagTinkerApp* app, const uint8_t plid[4]) { + uint8_t frame[TAGTINKER_MAX_FRAME_SIZE]; + size_t len = tagtinker_make_ping_frame(frame, plid); + uint16_t repeats = tx_is_pp16(app) ? 500U : 250U; + 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, 50); +} + +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; + return pixel_count <= TX_FULL_JOB_PIXEL_LIMIT; +} + +static bool tx_send_image_start( + TagTinkerApp* app, + const uint8_t plid[4], + uint16_t byte_count, + uint8_t comp_type, + uint8_t page, + uint16_t width, + uint16_t height, + uint16_t pos_x, + uint16_t pos_y) { + uint8_t frame[TAGTINKER_MAX_FRAME_SIZE]; + size_t len = tagtinker_make_image_param_frame( + frame, plid, byte_count, comp_type, page, width, height, pos_x, pos_y); + return tx_send_frame(app, frame, len, 10); +} + +static bool tx_send_payload_frames( + TagTinkerApp* app, + const uint8_t plid[4], + const TagTinkerImagePayload* payload, + uint8_t page, + uint16_t width, + uint16_t height, + uint16_t pos_x, + uint16_t pos_y) { + uint8_t frame[TAGTINKER_MAX_FRAME_SIZE]; + bool ok = tx_send_image_start( + app, + plid, + (uint16_t)payload->byte_count, + payload->comp_type, + page, + width, + height, + pos_x, + pos_y); + if(ok) furi_delay_ms(tx_is_pp16(app) ? 80 : 120); + + size_t frame_count = payload->byte_count / 20U; + for(size_t i = 0; ok && i < frame_count; i++) { + size_t len = tagtinker_make_image_data_frame(frame, plid, (uint16_t)i, &payload->data[i * 20U]); + ok = tx_send_frame(app, frame, len, app->data_frame_repeats); + if(ok && ((i + 1U) % 16U) == 0U && (i + 1U) < frame_count) { + furi_delay_ms(1); + } + } + + return ok; +} + +static bool tx_send_image_chunk( + TagTinkerApp* app, + const uint8_t plid[4], + const uint8_t* primary_pixels, + const uint8_t* secondary_pixels, + uint16_t width, + uint16_t height, + uint8_t page, + uint16_t pos_x, + uint16_t pos_y) { + TagTinkerImagePayload payload; + size_t pixel_count = (size_t)width * height; + if(!tagtinker_encode_planes_payload( + primary_pixels, + secondary_pixels, + pixel_count, + app->compression_mode, + &payload)) { + return false; + } + + bool ok = tx_send_payload_frames(app, plid, &payload, page, width, height, pos_x, pos_y); + + tagtinker_free_image_payload(&payload); + return ok; +} + +static uint32_t tx_chunk_settle_delay_ms(uint16_t width, uint16_t height, bool color_clear) { + size_t work_pixels = (size_t)width * height * (color_clear ? 2U : 1U); + uint32_t delay_ms = 2000U + (uint32_t)(work_pixels / 5U); + if(delay_ms < 3500U) delay_ms = 3500U; + if(delay_ms > 9000U) delay_ms = 9000U; + return delay_ms; +} + +static uint16_t tx_pick_chunk_height(uint16_t width, uint16_t height, bool second_plane) { + uint16_t chunk_h = tagtinker_pick_chunk_height(width, second_plane); + size_t budget = second_plane ? 24576U : 24576U; + + if(second_plane && (height % 2U) == 0U) { + uint16_t half_h = height / 2U; + if(((size_t)width * half_h * 2U) <= budget) { + return half_h; + } + } + + if(chunk_h > height) chunk_h = height; + + if(chunk_h >= 16U) { + chunk_h = (uint16_t)(chunk_h & ~7U); + if(chunk_h == 0U) chunk_h = 8U; + } + + if(chunk_h > height) chunk_h = height; + if(chunk_h == 0U) chunk_h = 1U; + return chunk_h; +} + +static bool tx_send_full_payload( + TagTinkerApp* app, + const uint8_t plid[4], + const TagTinkerImagePayload* payload, + uint8_t page, + uint16_t width, + uint16_t height, + uint16_t pos_x, + uint16_t pos_y) { + bool ok = tx_send_ping(app, plid); + if(ok) furi_delay_ms(120); + if(ok) ok = tx_send_payload_frames(app, plid, payload, page, width, height, pos_x, pos_y); + if(ok) furi_delay_ms(120); + if(ok) ok = tx_send_refresh(app, plid); + return ok; +} + +static bool tx_send_full_text_image(TagTinkerApp* app) { + const TagTinkerImageTxJob* job = &app->image_tx_job; + 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; + bool use_second_plane = accent_capable || app->color_clear; + size_t pixel_count = (size_t)job->width * job->height; + TagTinkerTagColor accent_color = tx_target_color(app); + + if(!tx_should_send_full_job(job->width, job->height, use_second_plane)) { + return false; + } + + uint8_t* primary = malloc(pixel_count); + uint8_t* secondary = use_second_plane ? malloc(pixel_count) : NULL; + if(!primary || (use_second_plane && !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, job->width, job->height, app->text_input_buf, bg_primary, fg_primary); + render_text_ex(secondary, job->width, job->height, app->text_input_buf, 1, 0); + } else { + render_text_ex( + primary, + job->width, + job->height, + app->text_input_buf, + app->invert_text ? 0 : 1, + app->invert_text ? 1 : 0); + if(secondary) memset(secondary, 1, pixel_count); + } + + TagTinkerImagePayload payload; + bool ok = tagtinker_encode_planes_payload( + primary, secondary, pixel_count, app->compression_mode, &payload); + free(primary); + free(secondary); + + if(!ok) return false; + + ok = tx_send_full_payload( + app, + job->plid, + &payload, + job->page, + job->width, + job->height, + job->pos_x, + job->pos_y); + tagtinker_free_image_payload(&payload); + return ok; +} + +static bool tx_stream_text_image(TagTinkerApp* app) { + if(tx_send_full_text_image(app)) { + return true; + } + + const TagTinkerImageTxJob* job = &app->image_tx_job; + 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; + bool use_second_plane = accent_capable || app->color_clear; + TagTinkerTagColor accent_color = tx_target_color(app); + uint16_t chunk_h = tx_pick_chunk_height(job->width, job->height, use_second_plane); + uint8_t* primary = malloc((size_t)job->width * chunk_h); + uint8_t* secondary = use_second_plane ? malloc((size_t)job->width * chunk_h) : NULL; + if(!primary || (use_second_plane && !secondary)) { + free(primary); + free(secondary); + return false; + } + + bool ok = true; + + for(uint16_t y = 0; ok && y < job->height; y = (uint16_t)(y + chunk_h)) { + uint16_t actual_h = job->height - y; + if(actual_h > chunk_h) actual_h = chunk_h; + + if(accent_text) { + uint8_t bg_primary = app->invert_text ? 0 : 1; + uint8_t fg_primary = (accent_color == TagTinkerTagColorYellow) ? 0 : 1; + render_text_region_ex( + primary, + job->width, + job->height, + y, + actual_h, + app->text_input_buf, + bg_primary, + fg_primary); + render_text_region_ex( + secondary, + job->width, + job->height, + y, + actual_h, + app->text_input_buf, + 1, + 0); + } else { + render_text_region_ex( + primary, + job->width, + job->height, + y, + actual_h, + app->text_input_buf, + app->invert_text ? 0 : 1, + app->invert_text ? 1 : 0); + + if(secondary) memset(secondary, 1, (size_t)job->width * actual_h); + } + + ok = tx_send_ping(app, job->plid); + if(ok) furi_delay_ms(120); + ok = tx_send_image_chunk( + app, + job->plid, + primary, + secondary, + job->width, + actual_h, + job->page, + job->pos_x, + (uint16_t)(job->pos_y + y)); + if(ok) furi_delay_ms(100); + if(ok) ok = tx_send_refresh(app, job->plid); + + if(ok && (uint16_t)(y + actual_h) < job->height) { + furi_delay_ms(tx_chunk_settle_delay_ms(job->width, actual_h, use_second_plane)); + } + } + + free(primary); + free(secondary); + return ok; +} + +static bool tx_bmp_open(const char* path, File* file, TxBmpInfo* info) { + if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) return false; + + uint8_t header[54]; + if(storage_file_read(file, header, sizeof(header)) != sizeof(header)) return false; + if(header[0] != 'B' || header[1] != 'M') return false; + + uint16_t bpp = header[28] | (header[29] << 8); + if(!(bpp == 1 || bpp == 24 || bpp == 32)) return false; + info->bpp = bpp; + + int32_t bmp_h = header[22] | (header[23] << 8) | (header[24] << 16) | (header[25] << 24); + info->width = (uint16_t)(header[18] | (header[19] << 8) | (header[20] << 16) | (header[21] << 24)); + info->top_down = false; + if(bmp_h < 0) { + info->top_down = true; + bmp_h = -bmp_h; + } + + info->height = (uint16_t)bmp_h; + info->data_offset = header[10] | (header[11] << 8) | (header[12] << 16) | (header[13] << 24); + if(info->bpp == 1) { + info->row_stride = ((info->width + 31U) / 32U) * 4U; + } else if(info->bpp == 24) { + info->row_stride = ((info->width * 3U) + 3U) & ~3U; + } else { + info->row_stride = info->width * 4U; + } + return true; +} + +static void tx_map_rgb_pixel( + uint8_t r, + uint8_t g, + uint8_t b, + TagTinkerTagColor accent_color, + uint8_t* primary, + uint8_t* secondary) { + bool is_red = (r > 127U) && (g < 128U) && (b < 128U); + bool is_yellow = (r > 127U) && (g > 127U) && (b < 128U); + bool is_accent = is_red || is_yellow; + if(is_accent) { + *primary = (accent_color == TagTinkerTagColorYellow) ? 0U : 1U; + *secondary = 0U; + return; + } + + uint16_t luma = (uint16_t)(21U * r + 72U * g + 7U * b); + *primary = (luma < 12800U) ? 0U : 1U; + *secondary = 1U; +} + +static bool tx_bmp_read_chunk( + File* file, + const TxBmpInfo* info, + TagTinkerTagColor accent_color, + uint16_t chunk_y, + uint16_t chunk_h, + uint8_t* primary, + uint8_t* secondary) { + uint8_t* row_buf = malloc(info->row_stride); + if(!row_buf) return false; + + memset(primary, 1, (size_t)info->width * chunk_h); + if(secondary) memset(secondary, 1, (size_t)info->width * chunk_h); + + bool ok = true; + for(uint16_t local_y = 0; local_y < chunk_h; local_y++) { + uint16_t global_y = chunk_y + local_y; + uint32_t source_row = info->top_down ? global_y : (info->height - 1U - global_y); + uint32_t offset = info->data_offset + source_row * info->row_stride; + + if(!storage_file_seek(file, offset, true) || + storage_file_read(file, row_buf, info->row_stride) != info->row_stride) { + ok = false; + break; + } + + for(uint16_t x = 0; x < info->width; x++) { + size_t idx = (size_t)local_y * info->width + x; + + if(info->bpp == 1) { + uint8_t byte = row_buf[x / 8U]; + uint8_t bit = (byte >> (7U - (x % 8U))) & 1U; + primary[idx] = bit ? 1U : 0U; + if(secondary) secondary[idx] = 1U; + } else { + uint32_t pixel_off = x * (info->bpp / 8U); + uint8_t b = row_buf[pixel_off + 0U]; + uint8_t g = row_buf[pixel_off + 1U]; + uint8_t r = row_buf[pixel_off + 2U]; + if(secondary) { + tx_map_rgb_pixel(r, g, b, accent_color, &primary[idx], &secondary[idx]); + } else { + uint16_t luma = (uint16_t)(21U * r + 72U * g + 7U * b); + primary[idx] = (luma < 12800U) ? 0U : 1U; + } + } + } + } + + free(row_buf); + return ok; +} + +static bool tx_stream_bmp_image(TagTinkerApp* app) { + const TagTinkerImageTxJob* job = &app->image_tx_job; + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + TxBmpInfo info = {0}; + bool ok = tx_bmp_open(job->image_path, file, &info); + + if(ok && (info.width != job->width || info.height != job->height)) { + ok = false; + } + + TagTinkerTagColor accent_color = tx_target_color(app); + bool accent_capable = + accent_color == TagTinkerTagColorRed || accent_color == TagTinkerTagColorYellow; + bool use_second_plane = app->color_clear || accent_capable; + + if(ok && tx_should_send_full_job(job->width, job->height, use_second_plane)) { + size_t pixel_count = (size_t)job->width * job->height; + uint8_t* primary = malloc(pixel_count); + uint8_t* secondary = use_second_plane ? malloc(pixel_count) : NULL; + + if(!primary || (use_second_plane && !secondary)) { + ok = false; + } else { + ok = tx_bmp_read_chunk(file, &info, accent_color, 0, job->height, primary, secondary); + if(ok) { + TagTinkerImagePayload payload; + ok = tagtinker_encode_planes_payload( + primary, secondary, pixel_count, app->compression_mode, &payload); + if(ok) { + ok = tx_send_full_payload( + app, + job->plid, + &payload, + job->page, + job->width, + job->height, + job->pos_x, + job->pos_y); + tagtinker_free_image_payload(&payload); + } + } + } + + free(primary); + free(secondary); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return ok; + } + + uint16_t chunk_h = tx_pick_chunk_height(job->width, job->height, use_second_plane); + uint8_t* primary = ok ? malloc((size_t)job->width * chunk_h) : NULL; + uint8_t* secondary = (ok && use_second_plane) ? malloc((size_t)job->width * chunk_h) : NULL; + if(ok && (!primary || (use_second_plane && !secondary))) ok = false; + + for(uint16_t y = 0; ok && y < job->height; y = (uint16_t)(y + chunk_h)) { + uint16_t actual_h = job->height - y; + if(actual_h > chunk_h) actual_h = chunk_h; + + ok = tx_send_ping(app, job->plid); + if(ok) furi_delay_ms(120); + ok = tx_bmp_read_chunk(file, &info, accent_color, y, actual_h, primary, secondary); + if(ok) + ok = tx_send_image_chunk( + app, + job->plid, + primary, + secondary, + job->width, + actual_h, + job->page, + job->pos_x, + (uint16_t)(job->pos_y + y)); + if(ok) furi_delay_ms(100); + if(ok) ok = tx_send_refresh(app, job->plid); + + if(ok && (uint16_t)(y + actual_h) < job->height) { + furi_delay_ms(tx_chunk_settle_delay_ms(job->width, actual_h, use_second_plane)); + } + } + + free(primary); + free(secondary); + storage_file_close(file); + storage_file_free(file); + furi_record_close(RECORD_STORAGE); + return ok; +} + static int32_t tx_thread_callback(void* context) { TagTinkerApp* app = context; bool ok = true; @@ -20,7 +550,11 @@ static int32_t tx_thread_callback(void* context) { tagtinker_ir_init(); do { - if(app->frame_seq_count > 0) { + if(app->image_tx_job.mode == TagTinkerTxModeTextImage) { + ok = tx_stream_text_image(app); + } else if(app->image_tx_job.mode == TagTinkerTxModeBmpImage) { + ok = tx_stream_bmp_image(app); + } else if(app->frame_seq_count > 0) { for(size_t i = 0; i < app->frame_seq_count; i++) { if(!app->tx_active) { ok = false; break; } if(i > 0) furi_delay_ms(20); @@ -248,15 +782,6 @@ void tagtinker_scene_transmit_on_exit(void* context) { furi_thread_join(app->tx_thread); tagtinker_ir_deinit(); - if(app->frame_sequence) { - for(size_t i = 0; i < app->frame_seq_count; i++) - free(app->frame_sequence[i]); - free(app->frame_sequence); - free(app->frame_lengths); - free(app->frame_repeats); - app->frame_sequence = NULL; - app->frame_lengths = NULL; - app->frame_repeats = NULL; - app->frame_seq_count = 0; - } + tagtinker_free_frame_sequence(app); + memset(&app->image_tx_job, 0, sizeof(app->image_tx_job)); } diff --git a/tagtinker_app.c b/tagtinker_app.c index 2c0019a..0f011f5 100644 --- a/tagtinker_app.c +++ b/tagtinker_app.c @@ -29,16 +29,209 @@ static bool custom_event_cb(void* ctx, uint32_t event) { extern const SceneManagerHandlers tagtinker_scene_handlers; +#define TAGTINKER_STREAM_PIXEL_BUDGET 24576U + +static void tagtinker_clamp_region_to_target( + const TagTinkerApp* app, + uint16_t width, + uint16_t height, + uint16_t* pos_x, + uint16_t* pos_y) { + if(!app || !pos_x || !pos_y) return; + if(app->selected_target < 0 || app->selected_target >= app->target_count) return; + + const TagTinkerTarget* target = &app->targets[app->selected_target]; + if(!target->profile.known || !target->profile.width || !target->profile.height) return; + + uint16_t max_x = + (width < target->profile.width) ? (uint16_t)(target->profile.width - width) : 0U; + uint16_t max_y = + (height < target->profile.height) ? (uint16_t)(target->profile.height - height) : 0U; + + if(*pos_x > max_x) *pos_x = max_x; + if(*pos_y > max_y) *pos_y = max_y; +} + +void tagtinker_target_refresh_profile(TagTinkerTarget* target) { + if(!target) return; + + memset(&target->profile, 0, sizeof(target->profile)); + tagtinker_barcode_to_profile(target->barcode, &target->profile); +} + +bool tagtinker_target_supports_graphics(const TagTinkerTarget* target) { + if(!target) return false; + + return target->profile.kind != TagTinkerTagKindSegment; +} + +bool tagtinker_target_supports_accent(const TagTinkerTarget* target) { + if(!target) return false; + + return target->profile.color == TagTinkerTagColorRed || + target->profile.color == TagTinkerTagColorYellow; +} + +const char* tagtinker_profile_kind_label(TagTinkerTagKind kind) { + switch(kind) { + case TagTinkerTagKindDotMatrix: + return "Dot Matrix"; + case TagTinkerTagKindSegment: + return "Segment"; + default: + return "Unknown"; + } +} + +const char* tagtinker_profile_color_label(TagTinkerTagColor color) { + switch(color) { + case TagTinkerTagColorRed: + return "Red"; + case TagTinkerTagColorYellow: + return "Yellow"; + default: + return "Mono"; + } +} + +void tagtinker_free_frame_sequence(TagTinkerApp* app) { + if(!app || !app->frame_sequence) return; + + for(size_t i = 0; i < app->frame_seq_count; i++) { + free(app->frame_sequence[i]); + } + + free(app->frame_sequence); + free(app->frame_lengths); + free(app->frame_repeats); + app->frame_sequence = NULL; + app->frame_lengths = NULL; + app->frame_repeats = NULL; + app->frame_seq_count = 0; +} + +uint16_t tagtinker_pick_chunk_height(uint16_t width, bool color_clear) { + if(width == 0) return 1; + + size_t plane_budget = color_clear ? (TAGTINKER_STREAM_PIXEL_BUDGET / 2U) : TAGTINKER_STREAM_PIXEL_BUDGET; + uint16_t chunk_h = (uint16_t)(plane_budget / width); + if(chunk_h == 0) chunk_h = 1; + return chunk_h; +} + +void tagtinker_prepare_text_tx(TagTinkerApp* app, const uint8_t plid[4]) { + if(!app) return; + + tagtinker_free_frame_sequence(app); + 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; + app->image_tx_job.width = app->esl_width; + app->image_tx_job.height = app->esl_height; + app->image_tx_job.pos_x = app->draw_x; + app->image_tx_job.pos_y = app->draw_y; + tagtinker_clamp_region_to_target( + app, + app->image_tx_job.width, + app->image_tx_job.height, + &app->image_tx_job.pos_x, + &app->image_tx_job.pos_y); +} + +void tagtinker_prepare_bmp_tx( + TagTinkerApp* app, + const uint8_t plid[4], + const char* image_path, + uint16_t width, + uint16_t height, + uint8_t page) { + if(!app) return; + + tagtinker_free_frame_sequence(app); + memset(&app->image_tx_job, 0, sizeof(app->image_tx_job)); + 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; + app->image_tx_job.width = width; + app->image_tx_job.height = height; + app->image_tx_job.pos_x = app->draw_x; + app->image_tx_job.pos_y = app->draw_y; + tagtinker_clamp_region_to_target( + app, + app->image_tx_job.width, + app->image_tx_job.height, + &app->image_tx_job.pos_x, + &app->image_tx_job.pos_y); + if(image_path) { + strncpy(app->image_tx_job.image_path, image_path, TAGTINKER_IMAGE_PATH_LEN); + app->image_tx_job.image_path[TAGTINKER_IMAGE_PATH_LEN] = '\0'; + } +} + +void tagtinker_select_target(TagTinkerApp* app, uint8_t index) { + if(!app || index >= app->target_count) return; + + TagTinkerTarget* target = &app->targets[index]; + app->selected_target = (int8_t)index; + memcpy(app->barcode, target->barcode, TAGTINKER_BC_LEN + 1); + memcpy(app->plid, target->plid, sizeof(target->plid)); + app->barcode_valid = true; + + if(target->profile.known && target->profile.kind == TagTinkerTagKindDotMatrix && + target->profile.width && target->profile.height) { + app->esl_width = target->profile.width; + app->esl_height = target->profile.height; + } +} + void tagtinker_settings_load(TagTinkerApp* app) { app->show_startup_warning = true; + app->signal_mode = TagTinkerSignalPP4; + app->compression_mode = TagTinkerCompressionAuto; + app->data_frame_repeats = 3; Storage* storage = furi_record_open(RECORD_STORAGE); File* file = storage_file_alloc(storage); if(storage_file_open(file, APP_DATA_PATH("settings.txt"), FSAM_READ, FSOM_OPEN_EXISTING)) { - char value = '1'; - if(storage_file_read(file, &value, 1) == 1) { - app->show_startup_warning = (value != '0'); + char buf[64]; + uint16_t read = storage_file_read(file, buf, sizeof(buf) - 1); + buf[read] = '\0'; + + if(strchr(buf, '=')) { + char* line = buf; + while(line && *line) { + char* nl = strchr(line, '\n'); + if(nl) *nl = '\0'; + + if(strncmp(line, "warning=", 8) == 0) { + app->show_startup_warning = (line[8] != '0'); + } else if(strncmp(line, "signal=", 7) == 0) { + int value = atoi(line + 7); + app->signal_mode = (value == 2) ? TagTinkerSignalPP16 : TagTinkerSignalPP4; + } else if(strncmp(line, "compression=", 12) == 0) { + int value = atoi(line + 12); + if(value >= TagTinkerCompressionAuto && value <= TagTinkerCompressionRle) { + app->compression_mode = (TagTinkerCompressionMode)value; + } + } else if(strncmp(line, "frame_repeat=", 13) == 0) { + int value = atoi(line + 13); + if(value >= 1 && value <= 5) { + app->data_frame_repeats = (uint8_t)value; + } + } else if(strncmp(line, "draw_x=", 7) == 0) { + int value = atoi(line + 7); + if(value >= 0) app->draw_x = (uint16_t)value; + } else if(strncmp(line, "draw_y=", 7) == 0) { + int value = atoi(line + 7); + if(value >= 0) app->draw_y = (uint16_t)value; + } + + line = nl ? nl + 1 : NULL; + } + } else if(read >= 1) { + app->show_startup_warning = (buf[0] != '0'); } storage_file_close(file); } @@ -55,8 +248,18 @@ bool tagtinker_settings_save(const TagTinkerApp* app) { bool ok = false; if(storage_file_open(file, APP_DATA_PATH("settings.txt"), FSAM_WRITE, FSOM_CREATE_ALWAYS)) { - const char value = app->show_startup_warning ? '1' : '0'; - ok = (storage_file_write(file, &value, 1) == 1); + char buf[128]; + int len = snprintf( + buf, + sizeof(buf), + "warning=%d\nsignal=%d\ncompression=%d\nframe_repeat=%u\ndraw_x=%u\ndraw_y=%u\n", + app->show_startup_warning ? 1 : 0, + (int)app->signal_mode, + (int)app->compression_mode, + app->data_frame_repeats, + app->draw_x, + app->draw_y); + ok = (len > 0) && storage_file_write(file, buf, (uint16_t)len); storage_file_close(file); } @@ -102,6 +305,7 @@ void tagtinker_targets_load(TagTinkerApp* app) { snprintf(target->name, TAGTINKER_TARGET_NAME_LEN + 1, "Tag ...%s", suffix); } + tagtinker_target_refresh_profile(target); app->target_count++; } } @@ -154,8 +358,8 @@ static TagTinkerApp* app_alloc(void) { app->page = 0; app->duration = 15; app->repeats = 200; - app->draw_width = 48; - app->draw_height = 48; + app->draw_x = 0; + app->draw_y = 0; app->img_page = 1; app->esl_width = 200; app->esl_height = 80; @@ -226,7 +430,7 @@ static TagTinkerApp* app_alloc(void) { /* Allocate Thread and Timer for animations and TX */ app->tx_thread = furi_thread_alloc(); furi_thread_set_name(app->tx_thread, "TagTinkerTx"); - furi_thread_set_stack_size(app->tx_thread, 2048); + furi_thread_set_stack_size(app->tx_thread, 4096); furi_thread_set_priority(app->tx_thread, FuriThreadPriorityHighest); furi_thread_set_context(app->tx_thread, app); @@ -236,13 +440,7 @@ static TagTinkerApp* app_alloc(void) { static void app_free(TagTinkerApp* app) { tagtinker_ir_deinit(); - if(app->frame_sequence) { - for(size_t i = 0; i < app->frame_seq_count; i++) - free(app->frame_sequence[i]); - free(app->frame_sequence); - free(app->frame_lengths); - free(app->frame_repeats); - } + tagtinker_free_frame_sequence(app); view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewSubmenu); view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewVarItemList); diff --git a/tagtinker_app.h b/tagtinker_app.h index c1edb42..887dd98 100644 --- a/tagtinker_app.h +++ b/tagtinker_app.h @@ -33,6 +33,7 @@ #define TAGTINKER_TARGET_NAME_LEN 16 #define TAGTINKER_MAX_PRESETS 6 #define TAGTINKER_PRESET_TEXT_LEN 32 +#define TAGTINKER_IMAGE_PATH_LEN 255 /* Views */ typedef enum { @@ -53,8 +54,31 @@ typedef struct { char name[TAGTINKER_TARGET_NAME_LEN + 1]; char barcode[TAGTINKER_BC_LEN + 1]; uint8_t plid[4]; + TagTinkerTagProfile profile; } TagTinkerTarget; +typedef enum { + TagTinkerTxModeDirect = 0, + TagTinkerTxModeTextImage, + TagTinkerTxModeBmpImage, +} TagTinkerTxMode; + +typedef enum { + TagTinkerSignalPP4 = 0, + TagTinkerSignalPP16, +} TagTinkerSignalMode; + +typedef struct { + TagTinkerTxMode mode; + uint8_t plid[4]; + uint8_t page; + uint16_t width; + uint16_t height; + uint16_t pos_x; + uint16_t pos_y; + char image_path[TAGTINKER_IMAGE_PATH_LEN + 1]; +} TagTinkerImageTxJob; + struct TagTinkerApp { /* GUI */ Gui* gui; @@ -90,6 +114,9 @@ struct TagTinkerApp { 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]; @@ -133,11 +160,14 @@ struct TagTinkerApp { /* Image settings */ uint8_t img_page; - uint16_t draw_width; - uint16_t draw_height; + uint16_t draw_x; + uint16_t draw_y; /* Indicates which mode triggered raw cmd (0=broadcast, 1=targeted) */ uint8_t raw_mode; + + /* Chunked image/text TX */ + TagTinkerImageTxJob image_tx_job; }; /* Main menu items */ @@ -157,6 +187,7 @@ typedef enum { /* Target action items */ typedef enum { + TagTinkerTargetDetails, TagTinkerTargetPushText, TagTinkerTargetPushImage, TagTinkerTargetPingFlash, @@ -166,3 +197,19 @@ 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); +bool tagtinker_target_supports_graphics(const TagTinkerTarget* target); +bool tagtinker_target_supports_accent(const TagTinkerTarget* target); +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]); +void tagtinker_prepare_bmp_tx( + TagTinkerApp* app, + const uint8_t plid[4], + const char* image_path, + 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); diff --git a/views/numlock_input.c b/views/numlock_input.c index c3c9524..9596a01 100644 --- a/views/numlock_input.c +++ b/views/numlock_input.c @@ -1,7 +1,7 @@ /* * TagTinker — Number Lock Barcode Input * - * Clean centered digit entry: N + 16 digits in groups of 4. + * Clean centered barcode entry: 1 letter + 16 digits in groups of 4. * UP/DOWN cycle, LEFT/RIGHT move, OK confirm, BACK cancel. */ @@ -17,10 +17,15 @@ #define PREFIX_W 10 typedef struct { + char prefix; uint8_t digits[NUM_DIGITS]; uint8_t cursor; } NumlockModel; +static uint8_t prefix_x(void) { + return 4; +} + 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. */ @@ -48,16 +53,34 @@ static void numlock_draw(Canvas* canvas, void* model_v) { const uint8_t baseline = 36; canvas_set_font(canvas, FontPrimary); - - /* Fixed 'N' Prefix */ - canvas_draw_str(canvas, 4, baseline, "N"); + + /* Editable letter prefix */ + char prefix[2] = {m->prefix, '\0'}; + if(m->cursor == 0) { + uint8_t x = prefix_x(); + 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, prefix); + canvas_set_color(canvas, ColorBlack); + + 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); + + 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); + } else { + 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 == m->cursor) { + 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); @@ -109,11 +132,21 @@ static bool numlock_input(InputEvent* input, void* ctx) { { switch(input->key) { case InputKeyUp: - m->digits[m->cursor] = (m->digits[m->cursor] + 1) % 10; + if(m->cursor == 0) { + m->prefix = (m->prefix == 'Z') ? 'A' : (m->prefix + 1); + } else { + uint8_t digit_idx = m->cursor - 1; + m->digits[digit_idx] = (m->digits[digit_idx] + 1) % 10; + } consumed = true; break; case InputKeyDown: - m->digits[m->cursor] = (m->digits[m->cursor] + 9) % 10; + if(m->cursor == 0) { + m->prefix = (m->prefix == 'A') ? 'Z' : (m->prefix - 1); + } else { + uint8_t digit_idx = m->cursor - 1; + m->digits[digit_idx] = (m->digits[digit_idx] + 9) % 10; + } consumed = true; break; case InputKeyLeft: @@ -121,12 +154,12 @@ static bool numlock_input(InputEvent* input, void* ctx) { consumed = true; break; case InputKeyRight: - if(m->cursor < NUM_DIGITS - 1) m->cursor++; + if(m->cursor < NUM_DIGITS) m->cursor++; consumed = true; break; case InputKeyOk: { char barcode[18]; - barcode[0] = 'N'; + barcode[0] = m->prefix; for(uint8_t i = 0; i < NUM_DIGITS; i++) barcode[1 + i] = '0' + m->digits[i]; barcode[17] = '\0'; @@ -158,6 +191,7 @@ NumlockInput* numlock_input_alloc(void) { numlock->view, NumlockModel * m, { + m->prefix = 'N'; memset(m->digits, 0, NUM_DIGITS); m->digits[0] = 4; m->cursor = 1; @@ -186,6 +220,7 @@ void numlock_input_reset(NumlockInput* numlock) { numlock->view, NumlockModel * m, { + m->prefix = 'N'; memset(m->digits, 0, NUM_DIGITS); m->digits[0] = 4; m->cursor = 1; diff --git a/views/numlock_input.h b/views/numlock_input.h index 86654a0..76a6790 100644 --- a/views/numlock_input.h +++ b/views/numlock_input.h @@ -1,8 +1,8 @@ /* * TagTinker - number lock barcode input view * - * Custom view for entering a 16-digit ESL barcode. - * Format: N + 16 digits (auto-prefixed 'N'). + * Custom view for entering a 17-character ESL barcode. + * Format: Letter + 16 digits. * * Controls: * UP/DOWN - cycle digit 0-9 diff --git a/views/tagtinker_font.h b/views/tagtinker_font.h index caf28e3..298fc84 100644 --- a/views/tagtinker_font.h +++ b/views/tagtinker_font.h @@ -116,6 +116,19 @@ static inline void set_pixel(uint8_t* buf, uint16_t w, uint16_t h, uint16_t x, u if(x < w && y < h) buf[y * w + x] = val; } +static inline void set_region_pixel( + uint8_t* buf, + uint16_t region_w, + uint16_t region_h, + uint16_t x, + uint16_t global_y, + uint16_t region_y, + uint8_t val) { + if(x < region_w && global_y >= region_y && global_y < (uint16_t)(region_y + region_h)) { + buf[(global_y - region_y) * region_w + x] = val; + } +} + /* * Render text to fill the entire buffer. * bg_val/fg_val control polarity: @@ -167,6 +180,54 @@ static inline void render_text_ex(uint8_t* buf, uint16_t w, uint16_t h, } } +static inline void render_text_region_ex( + uint8_t* buf, + uint16_t full_w, + uint16_t full_h, + uint16_t region_y, + uint16_t region_h, + const char* text, + uint8_t bg_val, + uint8_t fg_val) { + memset(buf, bg_val, full_w * region_h); + + size_t len = strlen(text); + if(len == 0) return; + + uint16_t scale_w = full_w / (len * (FONT_CHAR_W + 1)); + uint16_t scale_h = full_h / FONT_CHAR_H; + + if(scale_w < 1) scale_w = 1; + if(scale_h < 1) scale_h = 1; + + uint16_t scale = (scale_w < scale_h) ? scale_w : scale_h; + uint16_t text_w = len * (FONT_CHAR_W + 1) * scale; + uint16_t text_h = FONT_CHAR_H * scale; + uint16_t start_x = (full_w - text_w) / 2; + uint16_t start_y = (full_h - text_h) / 2; + + for(size_t i = 0; i < len; i++) { + char c = text[i]; + if(c < 32 || c > 126) c = '?'; + uint8_t glyph_idx = c - 32; + + for(uint8_t gx = 0; gx < FONT_CHAR_W; gx++) { + uint8_t col = font_5x7[glyph_idx][gx]; + for(uint8_t gy = 0; gy < FONT_CHAR_H; gy++) { + if(col & (1 << gy)) { + for(uint16_t sx = 0; sx < scale; sx++) { + for(uint16_t sy = 0; sy < scale; sy++) { + uint16_t px = start_x + (i * (FONT_CHAR_W + 1) + gx) * scale + sx; + uint16_t py = start_y + gy * scale + sy; + set_region_pixel(buf, full_w, region_h, px, py, region_y, fg_val); + } + } + } + } + } + } +} + /* Convenience: normal polarity (white bg=1, black text=0) */ static inline void render_text(uint8_t* buf, uint16_t w, uint16_t h, const char* text) { render_text_ex(buf, w, h, text, 1, 0);