From b3e70f0ca12de3119944196e78fc9b9eda215cd6 Mon Sep 17 00:00:00 2001 From: kdmukAI-bot <263064613+kdmukAI-bot@users.noreply.github.com> Date: Sat, 4 Jul 2026 20:02:51 -0500 Subject: [PATCH] Add opt-in weighted-mixed-frames progress estimate Adds a weighted progress estimate for animated (fountain-coded) UR decoding, alongside the existing reference estimate. Ported from SeedSigner/seedsigner#541, where it has been the progress metric shown during SeedSigner's live animated-QR scans in production since 2024. The reference estimate, min(0.99, processed_parts_count / (expected_part_count * 1.75)), is a frame-count guesstimate that reaches its 0.99 cap well before a long animated QR completes and sits there, reporting a misleading "almost done" for many frames -- a common real-world source of user frustration. The weighted estimate counts information actually recovered: a decoded fragment scores 1.0; a fragment present only inside mixed (XOR'd) frames gets partial credit (each mixed frame contributes 1/(fragments mixed) to every fragment it covers, summed across mixed frames), capped at 0.75 per fragment so an undecoded fragment never counts as much as a decoded one and the percentage can't decrease. - fountain_decoder.c/.h: fountain_decoder_estimated_percent_complete_weighted(). - ur_decoder.c/.h: ur_decoder_estimated_percent_complete_weighted() wrapper. - uUR.c: estimated_percent_complete gains an opt-in weight_mixed_frames flag (default False returns the reference estimate byte-for-byte; existing callers are unaffected). Verified on ESP32-P4; weighted values match the reference Python decoder. Co-Authored-By: kdmukai <934746+kdmukai@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) --- src/fountain_decoder.c | 55 ++++++++++++++++++++++++++++++++++++++++++ src/fountain_decoder.h | 9 +++++++ src/ur_decoder.c | 7 ++++++ src/ur_decoder.h | 7 ++++++ uUR.c | 28 ++++++++++++++++----- 5 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/fountain_decoder.c b/src/fountain_decoder.c index f9c8b29..e63e9d3 100644 --- a/src/fountain_decoder.c +++ b/src/fountain_decoder.c @@ -1373,6 +1373,61 @@ fountain_decoder_estimated_percent_complete(fountain_decoder_t *decoder) { return progress > 0.99 ? 0.99 : progress; } +// Weighted-mixed-frames completion estimate. Ports SeedSigner's +// weight_mixed_frames=True method (helpers/ur2/fountain_decoder.py): count the +// fully decoded fragments, plus partial credit for fragments that are still +// only present inside mixed (XOR'd) frames. Each mixed frame contributes +// 1/(frames mixed) to every index it covers; each index's total contribution +// is capped at 0.75 so a not-yet-decoded fragment never counts as much as a +// decoded one (and so the reported percentage cannot decrease mid-decode). +// Backward-compatible addition — the reference estimate above is unchanged. +double fountain_decoder_estimated_percent_complete_weighted( + fountain_decoder_t *decoder) { + if (!decoder) + return 0.0; + if (fountain_decoder_is_complete(decoder)) + return 1.0; + if (!decoder->expected_part_indexes || + decoder->expected_part_indexes->count == 0) + return 0.0; + + size_t parts = decoder->expected_part_indexes->count; + + // Per-index partial scores from the mixed parts. Fragment indexes are in + // [0, seq_len) == [0, parts), so a scratch array keyed by index mirrors the + // Python dict `mixed_index_scoring`. Fall back to the reference estimate if + // the scratch allocation fails. + double *scoring = calloc(parts, sizeof(double)); + if (!scoring) + return fountain_decoder_estimated_percent_complete(decoder); + + mixed_parts_hash_t *hash = decoder->mixed_parts_hash; + if (hash) { + for (size_t b = 0; b < hash->capacity; b++) { + for (hash_entry_t *entry = hash->buckets[b]; entry; entry = entry->next) { + size_t cnt = entry->key.count; + if (cnt == 0) + continue; + double score = 1.0 / (double)cnt; + for (size_t k = 0; k < cnt; k++) { + size_t index = entry->key.indexes[k]; + if (index < parts) + scoring[index] += score; + } + } + } + } + + double mixed_score = 0.0; + for (size_t i = 0; i < parts; i++) { + mixed_score += scoring[i] < 0.75 ? scoring[i] : 0.75; + } + free(scoring); + + double num_complete = (double)decoder->received_part_indexes.count; + return (num_complete + mixed_score) / (double)parts; +} + uint8_t *fountain_decoder_result_message(fountain_decoder_t *decoder) { if (!decoder || !decoder->result) return NULL; diff --git a/src/fountain_decoder.h b/src/fountain_decoder.h index 4b0b876..7ee5030 100644 --- a/src/fountain_decoder.h +++ b/src/fountain_decoder.h @@ -77,6 +77,15 @@ size_t fountain_decoder_processed_parts_count(fountain_decoder_t *decoder); */ double fountain_decoder_estimated_percent_complete(fountain_decoder_t *decoder); +/** + * Get estimated completion percentage using the weighted-mixed-frames method + * (partial credit for fragments still only present inside mixed/XOR'd frames). + * @param decoder Pointer to fountain decoder + * @return Completion percentage (0.0 to 1.0) + */ +double fountain_decoder_estimated_percent_complete_weighted( + fountain_decoder_t *decoder); + /** * Get result message * @param decoder Pointer to fountain decoder diff --git a/src/ur_decoder.c b/src/ur_decoder.c index 55178f8..485695b 100644 --- a/src/ur_decoder.c +++ b/src/ur_decoder.c @@ -458,6 +458,13 @@ double ur_decoder_estimated_percent_complete(ur_decoder_t *decoder) { return fountain_decoder_estimated_percent_complete(decoder->fountain_decoder); } +double ur_decoder_estimated_percent_complete_weighted(ur_decoder_t *decoder) { + if (!decoder || !decoder->fountain_decoder) + return 0.0; + return fountain_decoder_estimated_percent_complete_weighted( + decoder->fountain_decoder); +} + ur_decoder_error_t ur_decoder_get_last_error(ur_decoder_t *decoder) { return decoder ? decoder->last_error : UR_DECODER_ERROR_NULL_POINTER; } diff --git a/src/ur_decoder.h b/src/ur_decoder.h index 6d4552c..c4ed058 100644 --- a/src/ur_decoder.h +++ b/src/ur_decoder.h @@ -97,6 +97,13 @@ size_t ur_decoder_processed_parts_count(ur_decoder_t *decoder); */ double ur_decoder_estimated_percent_complete(ur_decoder_t *decoder); +/** + * Get estimated completion percentage using the weighted-mixed-frames method. + * @param decoder Pointer to URDecoder instance + * @return Completion percentage (0.0 to 1.0) + */ +double ur_decoder_estimated_percent_complete_weighted(ur_decoder_t *decoder); + /** * Get last error * @param decoder Pointer to URDecoder instance diff --git a/uUR.c b/uUR.c index fa02b26..35790cf 100644 --- a/uUR.c +++ b/uUR.c @@ -295,18 +295,34 @@ static mp_obj_t ur_decoder_processed_parts_count_py(mp_obj_t self_in) { static MP_DEFINE_CONST_FUN_OBJ_1(ur_decoder_processed_parts_count_obj, ur_decoder_processed_parts_count_py); -// estimated_percent_complete method -static mp_obj_t ur_decoder_estimated_percent_complete_py(mp_obj_t self_in) { - mp_obj_ur_decoder_t *self = MP_OBJ_TO_PTR(self_in); +// estimated_percent_complete(weight_mixed_frames=False) method. +// weight_mixed_frames is an opt-in flag: the default (False) returns the +// original reference estimate byte-for-byte, so existing callers are +// unaffected; True selects the weighted-mixed-frames method, which gives +// partial credit for fragments still only present inside mixed/XOR'd frames. +static mp_obj_t ur_decoder_estimated_percent_complete_py(size_t n_args, + const mp_obj_t *pos_args, + mp_map_t *kw_args) { + static const mp_arg_t allowed_args[] = { + {MP_QSTR_weight_mixed_frames, MP_ARG_BOOL, {.u_bool = false}}, + }; + mp_arg_val_t parsed[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, + MP_ARRAY_SIZE(allowed_args), allowed_args, parsed); + mp_obj_ur_decoder_t *self = MP_OBJ_TO_PTR(pos_args[0]); if (!self->decoder) { return mp_obj_new_float(0.0); } - return mp_obj_new_float(ur_decoder_estimated_percent_complete(self->decoder)); + double pct = + parsed[0].u_bool + ? ur_decoder_estimated_percent_complete_weighted(self->decoder) + : ur_decoder_estimated_percent_complete(self->decoder); + return mp_obj_new_float(pct); } -static MP_DEFINE_CONST_FUN_OBJ_1(ur_decoder_estimated_percent_complete_obj, - ur_decoder_estimated_percent_complete_py); +static MP_DEFINE_CONST_FUN_OBJ_KW(ur_decoder_estimated_percent_complete_obj, 1, + ur_decoder_estimated_percent_complete_py); // URDecoder locals dict static const mp_rom_map_elem_t ur_decoder_locals_dict_table[] = {