diff --git a/application.fam b/application.fam index 36b5d05..3e8ade1 100644 --- a/application.fam +++ b/application.fam @@ -34,6 +34,7 @@ App( "views/numlock_input.c", "nfc/tagtinker_nfc.c", "scenes/tagtinker_scene_nfc_scan.c", + "scenes/tagtinker_scene_custom_size.c", "wifi/tagtinker_wifi.c", "wifi/tagtinker_wifi_bmp.c", "scenes/tagtinker_scene_wifi_plugins.c", diff --git a/protocol/tagtinker_proto.c b/protocol/tagtinker_proto.c index 066f23f..c4391ef 100644 --- a/protocol/tagtinker_proto.c +++ b/protocol/tagtinker_proto.c @@ -119,6 +119,10 @@ bool tagtinker_barcode_to_plid(const char* barcode, uint8_t plid[4]) { return true; } +bool tagtinker_type_is_known(uint16_t type_code) { + return find_profile_entry(type_code) != NULL; +} + 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; diff --git a/protocol/tagtinker_proto.h b/protocol/tagtinker_proto.h index 1861cb7..4ddbeb8 100644 --- a/protocol/tagtinker_proto.h +++ b/protocol/tagtinker_proto.h @@ -105,6 +105,14 @@ static inline void tagtinker_profile_glass_size( bool tagtinker_is_barcode_valid(const char* barcode); bool tagtinker_barcode_to_plid(const char* barcode, uint8_t plid[4]); bool tagtinker_barcode_to_type(const char* barcode, uint16_t* type_code); + +/* True when this type code has an entry in the built-in profile table. */ +bool tagtinker_type_is_known(uint16_t type_code); + +/* Bounds for a user-entered custom tag size (pixels). Width is byte-aligned. */ +#define TAGTINKER_CUSTOM_SIZE_MIN 8U +#define TAGTINKER_CUSTOM_SIZE_MAX 800U +#define TAGTINKER_CUSTOM_SIZE_STEP 8U bool tagtinker_barcode_to_profile(const char* barcode, TagTinkerTagProfile* profile); typedef struct { diff --git a/scenes/tagtinker_scene.c b/scenes/tagtinker_scene.c index d8a102d..4a71b38 100644 --- a/scenes/tagtinker_scene.c +++ b/scenes/tagtinker_scene.c @@ -25,6 +25,7 @@ void(*const tagtinker_scene_on_enter_handlers[])(void*) = { tagtinker_scene_wifi_plugins_on_enter, tagtinker_scene_wifi_setup_on_enter, tagtinker_scene_wifi_run_on_enter, + tagtinker_scene_custom_size_on_enter, }; bool(*const tagtinker_scene_on_event_handlers[])(void*, SceneManagerEvent) = { @@ -48,6 +49,7 @@ bool(*const tagtinker_scene_on_event_handlers[])(void*, SceneManagerEvent) = { tagtinker_scene_wifi_plugins_on_event, tagtinker_scene_wifi_setup_on_event, tagtinker_scene_wifi_run_on_event, + tagtinker_scene_custom_size_on_event, }; void(*const tagtinker_scene_on_exit_handlers[])(void*) = { @@ -71,6 +73,7 @@ void(*const tagtinker_scene_on_exit_handlers[])(void*) = { tagtinker_scene_wifi_plugins_on_exit, tagtinker_scene_wifi_setup_on_exit, tagtinker_scene_wifi_run_on_exit, + tagtinker_scene_custom_size_on_exit, }; const SceneManagerHandlers tagtinker_scene_handlers = { diff --git a/scenes/tagtinker_scene.h b/scenes/tagtinker_scene.h index 434931f..e7c103b 100644 --- a/scenes/tagtinker_scene.h +++ b/scenes/tagtinker_scene.h @@ -27,6 +27,7 @@ typedef enum { TagTinkerSceneWifiPlugins, TagTinkerSceneWifiSetup, TagTinkerSceneWifiRun, + TagTinkerSceneCustomSize, TagTinkerSceneCount, } TagTinkerScene; @@ -109,3 +110,7 @@ void tagtinker_scene_wifi_setup_on_exit(void* ctx); void tagtinker_scene_wifi_run_on_enter(void* ctx); bool tagtinker_scene_wifi_run_on_event(void* ctx, SceneManagerEvent event); void tagtinker_scene_wifi_run_on_exit(void* ctx); + +void tagtinker_scene_custom_size_on_enter(void* ctx); +bool tagtinker_scene_custom_size_on_event(void* ctx, SceneManagerEvent event); +void tagtinker_scene_custom_size_on_exit(void* ctx); diff --git a/scenes/tagtinker_scene_custom_size.c b/scenes/tagtinker_scene_custom_size.c new file mode 100644 index 0000000..4e6886b --- /dev/null +++ b/scenes/tagtinker_scene_custom_size.c @@ -0,0 +1,130 @@ +/* + * Custom size scene. + * + * Lets the user give a width, height and colour to a target whose type code is + * not in the profile table, so Set Text and Set Image work for it. The IR + * framing is identical to a table profile; only the screen geometry differs. + * This does not guess any protocol and only applies to unknown-type targets. + */ + +#include "../tagtinker_app.h" +#include "../protocol/tagtinker_proto.h" + +enum { + CustomSizeWidth, + CustomSizeHeight, + CustomSizeColor, + CustomSizeSave, +}; + +/* index 0 -> MIN, then MIN + i*STEP, up to MAX. */ +static uint32_t size_value_count(void) { + return (TAGTINKER_CUSTOM_SIZE_MAX - TAGTINKER_CUSTOM_SIZE_MIN) / TAGTINKER_CUSTOM_SIZE_STEP + + 1U; +} + +static uint16_t size_index_to_px(uint8_t index) { + return (uint16_t)(TAGTINKER_CUSTOM_SIZE_MIN + (uint32_t)index * TAGTINKER_CUSTOM_SIZE_STEP); +} + +static uint8_t size_px_to_index(uint16_t px) { + if(px < TAGTINKER_CUSTOM_SIZE_MIN) return 0U; + uint16_t clamped = px > TAGTINKER_CUSTOM_SIZE_MAX ? TAGTINKER_CUSTOM_SIZE_MAX : px; + return (uint8_t)((clamped - TAGTINKER_CUSTOM_SIZE_MIN) / TAGTINKER_CUSTOM_SIZE_STEP); +} + +static void width_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + app->custom_size_w = size_index_to_px(variable_item_get_current_value_index(item)); + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->custom_size_w); + variable_item_set_current_value_text(item, buf); +} + +static void height_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + app->custom_size_h = size_index_to_px(variable_item_get_current_value_index(item)); + char buf[8]; + snprintf(buf, sizeof(buf), "%u", app->custom_size_h); + variable_item_set_current_value_text(item, buf); +} + +static void color_changed(VariableItem* item) { + TagTinkerApp* app = variable_item_get_context(item); + app->custom_size_color = (uint8_t)variable_item_get_current_value_index(item); + variable_item_set_current_value_text( + item, tagtinker_profile_color_label((TagTinkerTagColor)app->custom_size_color)); +} + +static void custom_size_cb(void* ctx, uint32_t index) { + TagTinkerApp* app = ctx; + if(index != CustomSizeSave) return; + if(app->selected_target < 0 || app->selected_target >= app->target_count) return; + + TagTinkerTarget* target = &app->targets[app->selected_target]; + target->custom_width = app->custom_size_w; + target->custom_height = app->custom_size_h; + target->custom_color = app->custom_size_color; + tagtinker_target_refresh_profile(target); + tagtinker_targets_save(app); + + /* Keep the app's working size in step with the target we just sized. */ + app->esl_width = target->profile.width; + app->esl_height = target->profile.height; + + scene_manager_previous_scene(app->scene_manager); +} + +void tagtinker_scene_custom_size_on_enter(void* ctx) { + TagTinkerApp* app = ctx; + + uint16_t w = TAGTINKER_CUSTOM_SIZE_MIN; + uint16_t h = TAGTINKER_CUSTOM_SIZE_MIN; + uint8_t color = 0U; + if(app->selected_target >= 0 && app->selected_target < app->target_count) { + const TagTinkerTarget* t = &app->targets[app->selected_target]; + if(t->custom_width > 0) w = t->custom_width; + if(t->custom_height > 0) h = t->custom_height; + color = t->custom_color; + } + app->custom_size_w = w; + app->custom_size_h = h; + app->custom_size_color = color; + + variable_item_list_reset(app->var_item_list); + + char buf[8]; + VariableItem* item_w = variable_item_list_add( + app->var_item_list, "Width", (uint8_t)size_value_count(), width_changed, app); + variable_item_set_current_value_index(item_w, size_px_to_index(w)); + snprintf(buf, sizeof(buf), "%u", w); + variable_item_set_current_value_text(item_w, buf); + + VariableItem* item_h = variable_item_list_add( + app->var_item_list, "Height", (uint8_t)size_value_count(), height_changed, app); + variable_item_set_current_value_index(item_h, size_px_to_index(h)); + snprintf(buf, sizeof(buf), "%u", h); + variable_item_set_current_value_text(item_h, buf); + + VariableItem* item_c = + variable_item_list_add(app->var_item_list, "Color", 3, color_changed, app); + variable_item_set_current_value_index(item_c, color); + variable_item_set_current_value_text( + item_c, tagtinker_profile_color_label((TagTinkerTagColor)color)); + + variable_item_list_add(app->var_item_list, ">> Save <<", 0, NULL, app); + variable_item_list_set_enter_callback(app->var_item_list, custom_size_cb, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewVarItemList); +} + +bool tagtinker_scene_custom_size_on_event(void* ctx, SceneManagerEvent event) { + UNUSED(ctx); + UNUSED(event); + return false; +} + +void tagtinker_scene_custom_size_on_exit(void* ctx) { + TagTinkerApp* app = ctx; + variable_item_list_reset(app->var_item_list); +} diff --git a/scenes/tagtinker_scene_target_actions.c b/scenes/tagtinker_scene_target_actions.c index e39e022..7bb37e7 100644 --- a/scenes/tagtinker_scene_target_actions.c +++ b/scenes/tagtinker_scene_target_actions.c @@ -3,6 +3,7 @@ */ #include "../tagtinker_app.h" +#include "../protocol/tagtinker_proto.h" static void target_actions_cb(void* ctx, uint32_t index) { TagTinkerApp* app = ctx; @@ -98,6 +99,14 @@ void tagtinker_scene_target_actions_on_enter(void* ctx) { submenu_add_item(app->submenu, "WiFi Plugins", TagTinkerTargetWifiPlugins, target_actions_cb, app); } + /* An unknown type has no table profile; let the user give it a size so the + * graphics actions above become usable. Shown whether or not a size is set, + * so it can be changed later. */ + if(target && !tagtinker_type_is_known(target->profile.type_code)) { + submenu_add_item( + app->submenu, "Set Custom Size", TagTinkerTargetCustomSize, target_actions_cb, app); + } + submenu_add_item(app->submenu, "LED Test", TagTinkerTargetPingFlash, target_actions_cb, app); submenu_add_item(app->submenu, "Delete Tag", TagTinkerTargetDeleteTag, target_actions_cb, app); @@ -129,6 +138,9 @@ bool tagtinker_scene_target_actions_on_event(void* ctx, SceneManagerEvent event) if(!tagtinker_target_supports_graphics(&app->targets[app->selected_target])) return true; scene_manager_next_scene(app->scene_manager, TagTinkerSceneWifiPlugins); return true; + case TagTinkerTargetCustomSize: + scene_manager_next_scene(app->scene_manager, TagTinkerSceneCustomSize); + return true; case TagTinkerTargetPingFlash: { TagTinkerTarget* target = &app->targets[app->selected_target]; diff --git a/tagtinker_app.c b/tagtinker_app.c index bb4fbb5..fa1822f 100644 --- a/tagtinker_app.c +++ b/tagtinker_app.c @@ -59,6 +59,17 @@ void tagtinker_target_refresh_profile(TagTinkerTarget* target) { memset(&target->profile, 0, sizeof(target->profile)); tagtinker_barcode_to_profile(target->barcode, &target->profile); + + /* If the barcode's type is not in the table but the user gave a size, build + * a profile from it. A real table profile is left untouched. */ + if(!target->profile.known && target->custom_width > 0 && target->custom_height > 0) { + target->profile.width = target->custom_width; + target->profile.height = target->custom_height; + target->profile.color = (TagTinkerTagColor)target->custom_color; + target->profile.kind = TagTinkerTagKindDotMatrix; + target->profile.model_name = "Custom"; + target->profile.known = true; + } } void tagtinker_target_set_default_name(TagTinkerApp* app, TagTinkerTarget* target) { @@ -512,22 +523,35 @@ void tagtinker_targets_load(TagTinkerApp* app) { if(nl) *nl = '\0'; if(*line) { - char* sep = strchr(line, '|'); - if(sep) *sep = '\0'; + /* Split into up to 5 fields: barcode|name|cw|ch|ccolor. + * Older files have only barcode|name; the rest default to 0. */ + char* fields[5] = {line, NULL, NULL, NULL, NULL}; + char* p = line; + for(uint8_t f = 1; f < 5; f++) { + char* sep = strchr(p, '|'); + if(!sep) break; + *sep = '\0'; + fields[f] = sep + 1; + p = sep + 1; + } - if(tagtinker_barcode_to_plid(line, app->targets[app->target_count].plid)) { + if(tagtinker_barcode_to_plid(fields[0], app->targets[app->target_count].plid)) { TagTinkerTarget* target = &app->targets[app->target_count]; - strncpy(target->barcode, line, TAGTINKER_BC_LEN); + memset(target, 0, sizeof(*target)); + strncpy(target->barcode, fields[0], TAGTINKER_BC_LEN); target->barcode[TAGTINKER_BC_LEN] = '\0'; - memset(target->name, 0, sizeof(target->name)); - if(sep && *(sep + 1)) { - strncpy(target->name, sep + 1, TAGTINKER_TARGET_NAME_LEN); + if(fields[1] && *fields[1]) { + strncpy(target->name, fields[1], TAGTINKER_TARGET_NAME_LEN); target->name[TAGTINKER_TARGET_NAME_LEN] = '\0'; } else { tagtinker_target_set_default_name(app, target); } + if(fields[2]) target->custom_width = (uint16_t)atoi(fields[2]); + if(fields[3]) target->custom_height = (uint16_t)atoi(fields[3]); + if(fields[4]) target->custom_color = (uint8_t)atoi(fields[4]); + tagtinker_target_refresh_profile(target); app->target_count++; } @@ -551,13 +575,16 @@ bool tagtinker_targets_save(const TagTinkerApp* app) { if(storage_file_open(file, APP_DATA_PATH("targets.txt"), FSAM_WRITE, FSOM_CREATE_ALWAYS)) { ok = true; for(uint8_t i = 0; i < app->target_count; i++) { - char line[64]; + char line[96]; int len = snprintf( line, sizeof(line), - "%s|%s\n", + "%s|%s|%u|%u|%u\n", app->targets[i].barcode, - app->targets[i].name); + app->targets[i].name, + app->targets[i].custom_width, + app->targets[i].custom_height, + app->targets[i].custom_color); if(len <= 0 || !storage_file_write(file, line, (uint16_t)len)) { ok = false; diff --git a/tagtinker_app.h b/tagtinker_app.h index d6a010b..c3bc57c 100644 --- a/tagtinker_app.h +++ b/tagtinker_app.h @@ -105,6 +105,12 @@ typedef struct { char barcode[TAGTINKER_BC_LEN + 1]; uint8_t plid[4]; TagTinkerTagProfile profile; + /* User-entered geometry for a tag whose type code is not in the profile + * table. 0x0 means unset. Applied by tagtinker_target_refresh_profile only + * when the barcode's type is unknown, so a real profile is never overridden. */ + uint16_t custom_width; + uint16_t custom_height; + uint8_t custom_color; /* TagTinkerTagColor */ } TagTinkerTarget; struct TagTinkerApp { @@ -156,6 +162,11 @@ struct TagTinkerApp { /* Saved targets */ TagTinkerTarget targets[TAGTINKER_MAX_TARGETS]; + + /* Custom-size scene working values. */ + uint16_t custom_size_w; + uint16_t custom_size_h; + uint8_t custom_size_color; uint8_t target_count; /* Text to push */ @@ -294,6 +305,7 @@ typedef enum { TagTinkerTargetDeleteSyncedImages, TagTinkerTargetPingFlash, TagTinkerTargetDeleteTag, + TagTinkerTargetCustomSize, } TagTinkerTargetActionItem; void tagtinker_target_refresh_profile(TagTinkerTarget* target);