Add profile-aware transmit controls and image options

This commit is contained in:
i12bp8
2026-04-07 00:18:41 +02:00
parent d2eb38d590
commit f3095f389f
19 changed files with 1907 additions and 234 deletions
+1
View File
@@ -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",
+346 -76
View File
@@ -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);
}
+65
View File
@@ -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 ─────────────────────────────────────────── */
+3
View File
@@ -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,
};
+5
View File
@@ -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);
+8 -3
View File
@@ -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);
+196
View File
@@ -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);
}
+24 -36
View File
@@ -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");
+1 -22
View File
@@ -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;
+136
View File
@@ -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);
}
+166 -51
View File
@@ -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 <storage/storage.h>
/* 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);
+49 -2
View File
@@ -3,22 +3,64 @@
*/
#include "../tagtinker_app.h"
#include <dialogs/dialogs.h>
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:
+1 -4
View File
@@ -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;
}
+537 -12
View File
@@ -4,7 +4,9 @@
*/
#include "../tagtinker_app.h"
#include "../views/tagtinker_font.h"
#include <furi_hal.h>
#include <storage/storage.h>
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));
}
+213 -15
View File
@@ -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);
+49 -2
View File
@@ -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);
+44 -9
View File
@@ -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;
+2 -2
View File
@@ -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
+61
View File
@@ -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);