mirror of
https://github.com/i12bp8/ESLPwn.git
synced 2026-08-28 13:24:05 +00:00
Add profile-aware transmit controls and image options
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 ─────────────────────────────────────────── */
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user