diff --git a/application.fam b/application.fam index 98a34c0..12e27b3 100644 --- a/application.fam +++ b/application.fam @@ -21,6 +21,7 @@ App( "*.c", "aeabi_uldivmod.sx", "!hf_interface_fal/*.c", + "!wiegand_interface_fal/*.c", ], fap_icon="icons/logo.png", fap_category="NFC", @@ -60,7 +61,7 @@ App( apptype=FlipperAppType.PLUGIN, entry_point="plugin_wiegand_ep", requires=["seader"], - sources=["hf_interface_fal/wiegand.c"], + sources=["wiegand_interface_fal/wiegand.c"], fal_embedded=True, ) diff --git a/docs/OWNERSHIP_MODEL.md b/docs/OWNERSHIP_MODEL.md index 0ea41b5..10cb89a 100644 --- a/docs/OWNERSHIP_MODEL.md +++ b/docs/OWNERSHIP_MODEL.md @@ -79,7 +79,7 @@ Rules: | `HF` | `Loaded` | plugin manager, plugin EP, plugin ctx may be non-`NULL`; pollers may be `NULL` | | `HF` | `Active` | plugin manager, plugin EP, plugin ctx must be non-`NULL`; active pollers may be non-`NULL` | | `HF` | `TearingDown` | teardown owns all pointer mutation; no scene code may touch HF runtime | -| `UHF` | `Unloaded` | all HF runtime pointers must be `NULL` | +| `UHF` | `Unloaded` | all HF runtime pointers must be `NULL`; UHF maintenance/probe flow owns mode runtime | Invalid combinations are bugs: @@ -92,14 +92,23 @@ Invalid combinations are bugs: ### HF startup -1. Verify `mode_runtime == None` -2. Verify `hf_session_state == Unloaded` -3. Load `plugin_hf.fal` -4. Resolve plugin entry point -5. Allocate plugin context -6. Set `hf_session_state = Loaded` -7. Set `mode_runtime = HF` -8. Start read and transition to `Active` +Legal startup paths: + +1. Cold acquire: + - verify `mode_runtime == None` + - verify `hf_session_state == Unloaded` + - load `plugin_hf.fal` + - resolve plugin entry point + - allocate plugin context + - set `hf_session_state = Loaded` + - set `mode_runtime = HF` + - start read and transition to `Active` +2. Fast-path re-acquire: + - allowed only when the existing HF runtime is already coherent + - preserve the existing `Loaded` or `Active` state + - do not unload/reload the plugin + +Any partial pointer/state combination must first normalize to `Unloaded`. ### HF teardown @@ -113,7 +122,8 @@ Invalid combinations are bugs: 8. Set `hf_session_state = Unloaded` 9. Set `mode_runtime = None` -This order must be implemented in one worker-owned path and nowhere else. +The blocking fallback teardown path must use the same state machine and ordering. +This order must be implemented in one worker-owned release primitive and nowhere else. ## Forbidden actions @@ -125,17 +135,28 @@ This order must be implemented in one worker-owned path and nowhere else. - Starting UHF while HF session state is not `Unloaded` - Starting HF while `mode_runtime == UHF` -## Plugin boundary +## UHF runtime -`hf_interface_fal/` is part of this repository. It is not a submodule. +`SeaderModeRuntimeUHF` is active only while the SAM maintenance/SNMP probe flow is active. Rules: -- HF plugin source must remain in-tree and follow this contract. +- UHF runtime must be entered when the probe starts. +- UHF runtime must be cleared when the probe finishes. +- While UHF runtime is active, HF acquire must be rejected. +- UHF runtime must not coexist with any live HF runtime pointer. + +## Plugin boundary + +`hf_interface_fal/` and `wiegand_interface_fal/` are part of this repository. They are not submodules. + +Rules: + +- HF and Wiegand plugin sources must remain in-tree and follow this contract. - The host/plugin boundary is narrow: - host owns orchestration, SAM transport, UI routing, and lifetime - - plugin owns HF protocol execution only -- The plugin must not directly own scene transitions or global app teardown. + - each plugin owns only its protocol-specific execution +- Plugins must not directly own scene transitions or global app teardown. ## Change checklist @@ -147,4 +168,5 @@ Before merging a change that touches HF/UHF/session code, confirm: - no scene code mutates live HF runtime - no teardown path mutates app-lifetime callback wiring - all state-table invariants still hold +- `OWNERSHIP_MODEL.md` changed in the same patch as any lifetime/order/state-machine change - this document still matches the implementation diff --git a/hf_interface_fal/HF_README.md b/hf_interface_fal/HF_README.md new file mode 100644 index 0000000..52c917a --- /dev/null +++ b/hf_interface_fal/HF_README.md @@ -0,0 +1,9 @@ +# Seader embedded HF plugin sources + +This directory is part of the main Seader repository. + +It contains the embedded HF `.fal` plugin sources used by Seader: +- `hf.c` +- `hf_interface.h` + +The HF plugin source path in `application.fam` must point at `hf_interface_fal/hf.c`. diff --git a/hf_interface_fal/README.md b/hf_interface_fal/README.md deleted file mode 100644 index fdc0c54..0000000 --- a/hf_interface_fal/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Seader embedded plugin sources - -This directory is part of the main Seader repository. - -It contains the embedded `.fal` plugin sources used by Seader: -- `wiegand.c` -- `hf.c` - -The source paths in `application.fam` must point at `hf_interface_fal/*.c`. diff --git a/sam_api.c b/sam_api.c index 9ff62d7..688bd64 100644 --- a/sam_api.c +++ b/sam_api.c @@ -71,6 +71,17 @@ static SeaderWorker* seader_get_active_worker(Seader* seader) { return seader ? seader->worker : NULL; } +static void seader_reset_cached_sam_metadata(Seader* seader) { + if(!seader) { + return; + } + + seader->sam_version[0] = 0U; + seader->sam_version[1] = 0U; + seader->uhf_status_label[0] = '\0'; + seader_uhf_snmp_probe_init(&seader->snmp_probe); +} + static bool seader_snmp_probe_send_next_request(Seader* seader) { SeaderUartBridge* seader_uart = seader_get_uart(seader); uint8_t* scratch = seader_uart ? (seader_uart->tx_buf + MAX_FRAME_HEADERS) : NULL; @@ -99,6 +110,9 @@ static void seader_snmp_probe_finish(Seader* seader) { return; } + if(seader->mode_runtime == SeaderModeRuntimeUHF) { + seader->mode_runtime = SeaderModeRuntimeNone; + } seader_sam_set_state(seader, SeaderSamStateIdle, SeaderSamIntentNone, SamCommand_PR_NOTHING); } @@ -107,6 +121,7 @@ static void seader_start_snmp_probe(Seader* seader) { return; } + seader->mode_runtime = SeaderModeRuntimeUHF; seader_uhf_snmp_probe_init(&seader->snmp_probe); seader_update_uhf_status_label(seader); seader_sam_set_state( @@ -648,6 +663,7 @@ void seader_worker_send_serial_number(Seader* seader) { void seader_worker_send_version(Seader* seader) { SamCommand_t samCommand = {0}; samCommand.present = SamCommand_PR_version; + seader_reset_cached_sam_metadata(seader); seader->sam_present = true; seader_update_sam_key_label(seader, NULL, 0U); seader_sam_set_state( diff --git a/scenes/seader_scene_read_common.c b/scenes/seader_scene_read_common.c index 386bfde..db35e97 100644 --- a/scenes/seader_scene_read_common.c +++ b/scenes/seader_scene_read_common.c @@ -21,9 +21,6 @@ void seader_scene_read_prepare(Seader* seader) { seader->samCommand = SamCommand_PR_NOTHING; } memset(seader->read_error, 0, sizeof(seader->read_error)); - if(seader->worker) { - seader_worker_reset_poller_session(seader->worker); - } } void seader_scene_read_cleanup(Seader* seader) { diff --git a/seader.c b/seader.c index ede9602..65975c4 100644 --- a/seader.c +++ b/seader.c @@ -799,13 +799,13 @@ static void seader_hf_teardown_blocking(Seader* seader) { return; } + seader->hf_session_state = SeaderHfSessionStateTearingDown; if(!seader_worker_acquire(seader) || !seader->worker || !seader->uart) { FURI_LOG_W(TAG, "HF blocking teardown fallback"); seader_hf_plugin_release(seader); return; } - seader->hf_session_state = SeaderHfSessionStateTearingDown; seader_worker_stop(seader->worker); FURI_LOG_I(TAG, "HF teardown blocking"); seader_worker_start( @@ -820,12 +820,11 @@ static void seader_hf_teardown_blocking(Seader* seader) { void seader_hf_plugin_release(Seader* seader) { furi_assert(seader); + seader->hf_session_state = SeaderHfSessionStateTearingDown; + if(seader->plugin_hf && seader->hf_plugin_ctx) { seader->plugin_hf->stop(seader->hf_plugin_ctx); - seader->plugin_hf->free(seader->hf_plugin_ctx); } - seader->hf_plugin_ctx = NULL; - seader->plugin_hf = NULL; if(seader->poller) { FURI_LOG_I(TAG, "Stopping host NFC poller"); @@ -841,12 +840,22 @@ void seader_hf_plugin_release(Seader* seader) { seader->picopass_poller = NULL; } + if(seader->plugin_hf && seader->hf_plugin_ctx) { + seader->plugin_hf->free(seader->hf_plugin_ctx); + } + seader->hf_plugin_ctx = NULL; + seader->plugin_hf = NULL; + if(seader->hf_plugin_manager) { FURI_LOG_I(TAG, "Unloading HF plugin"); plugin_manager_free(seader->hf_plugin_manager); seader->hf_plugin_manager = NULL; } + if(seader->worker) { + seader_worker_reset_poller_session(seader->worker); + } + if(seader->mode_runtime == SeaderModeRuntimeHF) { seader->mode_runtime = SeaderModeRuntimeNone; } diff --git a/seader_i.h b/seader_i.h index dfe5cbb..7d515c8 100644 --- a/seader_i.h +++ b/seader_i.h @@ -39,7 +39,7 @@ #include #include -#include "hf_interface_fal/interface.h" +#include "wiegand_interface_fal/interface.h" #include "hf_interface_fal/hf_interface.h" #include #include diff --git a/seader_worker.c b/seader_worker.c index 8edb91a..99b3bc4 100644 --- a/seader_worker.c +++ b/seader_worker.c @@ -24,9 +24,6 @@ static void seader_worker_release_hf_session(Seader* seader) { } seader_hf_plugin_release(seader); - if(seader->worker) { - seader_worker_reset_poller_session(seader->worker); - } } typedef struct { @@ -222,7 +219,7 @@ void seader_worker_start( seader_worker_stop(seader_worker); } - seader_worker->stage = SeaderPollerEventTypeCardDetect; + seader_worker_reset_poller_session(seader_worker); seader_worker->callback = callback; seader_worker->context = context; seader_worker->uart = uart; diff --git a/uhf_status_label.c b/uhf_status_label.c index e4919fe..73c3cf4 100644 --- a/uhf_status_label.c +++ b/uhf_status_label.c @@ -10,16 +10,35 @@ static size_t seader_uhf_append_family( bool* wrote_any, const char* name, bool key_present) { - if(*wrote_any) { - pos += (size_t)snprintf(out + pos, out_size - pos, "/"); - } else { - pos += (size_t)snprintf(out + pos, out_size - pos, "UHF: "); - *wrote_any = true; + int written = 0; + + if(pos >= out_size) { + return out_size - 1U; + } + + if(*wrote_any) { + written = snprintf(out + pos, out_size - pos, "/"); + } else { + written = snprintf(out + pos, out_size - pos, "UHF: "); + *wrote_any = true; + } + pos += (size_t)written; + if(pos >= out_size) { + return out_size - 1U; + } + + written = snprintf(out + pos, out_size - pos, "%s", name); + pos += (size_t)written; + if(pos >= out_size) { + return out_size - 1U; } - pos += (size_t)snprintf(out + pos, out_size - pos, "%s", name); if(!key_present) { - pos += (size_t)snprintf(out + pos, out_size - pos, " [no key]"); + written = snprintf(out + pos, out_size - pos, " [no key]"); + pos += (size_t)written; + if(pos >= out_size) { + return out_size - 1U; + } } return pos; } diff --git a/wiegand_interface_fal/README.md b/wiegand_interface_fal/README.md new file mode 100644 index 0000000..a38e05e --- /dev/null +++ b/wiegand_interface_fal/README.md @@ -0,0 +1,9 @@ +# Seader embedded Wiegand plugin sources + +This directory is part of the main Seader repository. + +It contains the embedded Wiegand `.fal` plugin sources used by Seader: +- `wiegand.c` +- `interface.h` + +The Wiegand plugin source path in `application.fam` must point at `wiegand_interface_fal/wiegand.c`. diff --git a/hf_interface_fal/interface.h b/wiegand_interface_fal/interface.h similarity index 100% rename from hf_interface_fal/interface.h rename to wiegand_interface_fal/interface.h diff --git a/hf_interface_fal/wiegand.c b/wiegand_interface_fal/wiegand.c similarity index 100% rename from hf_interface_fal/wiegand.c rename to wiegand_interface_fal/wiegand.c