2232 Commits
Author SHA1 Message Date
Rafael Fadel 82ed10e91c SubGHz: fix subghz rx, tx and tx_from_file over the CLI failing on the internal radio (#1216)
The CLI checks the result of subghz_devices_begin() since #1171, but the
internal CC1101 has no begin hook and subghz_devices_begin() returned false
for any device without one. Every CLI rx/tx on device 0 stopped at
"Device 0 failed to start", and the failure path shut the radio down.

A device with no begin hook has nothing to start, so starting it succeeds.
The external module still reports its real self-test result.

Fixes #1215
2026-11-13 13:00:50 +03:00
MX 53ff663b65 smol fix for belkorn 2026-11-13 03:26:11 +03:00
MX edc19780ed subghz add belkorn protocol
also improve history display, and some other small things
2026-11-13 03:11:29 +03:00
MX 2f47ba8943 oops [ci skip] 2026-11-12 04:29:09 +03:00
MX 25d23c93fa small fix for nfc related apps from catalog [ci skip] 2026-11-12 04:23:31 +03:00
MX d2754d73e0 apply some subghz fixes 2026-11-12 00:22:19 +03:00
Mykhailo ShevchukandClaude Opus 5 3433331e31 Infrared: fix ir universal rebooting the device, and the universal record lookup behind it (#1211)
The dictionary lookup in infrared_brute_force_calculate_messages was guarded by
furi_assert(record), which is compiled out whenever FURI_DEBUG is undefined. On every
release build, a database name the caller had not registered dereferenced NULL.

`ir universal <remote> <signal>` registers exactly one name and then walks the whole
database, so the first name that is not the one asked for crashed the device. Every
shipped database holds several distinct names, so the command rebooted the Flipper every
time it ran and could never have worked. The same crash was reachable from the Universal
screens with any user-supplied .ir database holding a button the screen does not use; the
six shipped ones are safe only because each scene happens to register a list that matches
its asset exactly.

A miss is skipped now, which is also the semantics the CLI wants - collect the records it
asked for, ignore the rest - so the command sends instead of crashing.

Three more things in the same code:

- calculate_messages reported success when a signal failed validation: error stayed
  InfraredErrorCodeNone, the loop broke, and the function returned clean. So a database
  holding an invalid signal validated clean.
- set_raw_signal ignores a signal with no timings and returns without touching the
  destination, so a raw entry with an empty data: left the previously read signal in
  place. Read as the entry after a parsed one, it passed infrared_signal_is_valid against
  that stale message, and its file position went into the dictionary to be transmitted
  later as whatever the signal object happened to hold. Reported at the read now, where
  every caller sees it - and it is the concrete asset that reaches the validation exit
  above.
- infrared_brute_force_reset did not clear db_filename, so a caller that forgot
  set_db_filename silently re-read the previous database. The CLI also stopped the
  instance a second time if the database disappeared between calculate_messages and
  start, since start stops itself on a failed open; dead code until the lookup fix made
  the path reachable.

The four universal database tests no longer register any names. Their assertion is that
every signal in the shipped asset parses and is valid, and the name lists were four
hand-maintained copies of the ones the app scenes keep - the drift that produced the test
bug fixed in #1199. fans.ir and leds.ir are covered now, so all six shipped databases are
tested instead of four, and one new test covers the dictionary hit, which nothing
exercised once the lists went.

Verified on an f7. Before, `ir universal tv Power` dropped USB and reset uptime from
137h3m52s to 12s. After, it reports "Sending 295 signal(s)..." and transmits; all six
remotes send (tv 295, audio 131, fans 116, leds 51, projectors 44, ac 31), the Universal
screens load and send unchanged, and uptime ran unbroken across the whole session. All
seven infrared unit tests pass on device.

Fixes #1201

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-11-10 14:35:04 +02:00
Dinko KorunicandMX 57d471868b SubGHz: Add Proteco MOVER protocol (433.92MHz AM650, 24 bit static, decoder + encoder) (#1210)
* SubGHz: add Proteco MOVER protocol

Proteco MOVER gate remotes (433.92 MHz AM650, 24 bit fixed code) use CAME
line coding at Te ~440us instead of 320us, so the CAME decoder rejected them
and they only worked as RAW. Adds decoder and encoder, unit tests and docs.
https://github.com/flipperdevices/flipperzero-firmware/issues/2345

* SubGHz: Proteco Add Manually entry, B/C button decoder tests

Add Manually: "Proteco 433MHz", a random 24 bit key with the low 2 bits
set to button A (gate), as on the Proteco TX3 remote (A = 0x2, B = 0x0,
C = 0x1). Advanced mode lets the key be edited.

Unit tests: one-press RAW captures of buttons B and C from a TX3 remote,
next to the existing button A capture.

* proteco custom buttons, guard checked against frame te

---------

Co-authored-by: MX <10697207+xMasterX@users.noreply.github.com>
2026-11-10 01:52:26 +03:00
MX 24b053f847 mode 10 for keeloq 2026-11-09 05:13:13 +03:00
MX faa3b6f3bd RA - add 3 new systems detection 2026-11-09 04:07:30 +03:00
MX 069616d840 Merge remote-tracking branch 'OFW/dev' into dev 2026-11-08 19:47:27 +03:00
Leptopt1los 80e69c093f iButton: TM01x cleanup/refactor (#4511)
* refactor(*): tm01x refactor

* feat(*): ibutton write refusal notification added
2026-11-08 02:50:12 +03:00
MX c8556be673 one more thing 2026-11-07 14:45:37 +03:00
MX acc61778e4 allow dallas to write keys with bad crc again 2026-11-07 14:40:34 +03:00
MX cb2c8218bb Merge remote-tracking branch 'OFW/dev' into dev 2026-11-07 05:43:06 +03:00
Leptopt1los 0aa3d5b74f iButton: TM01x Cyfral && Metakom write support (#4508)
* feat(*): tm01 write cyfral and metakom impl

* refactor(*): reuse lib functions

* refactor(*): cleanup
2026-11-07 04:54:58 +03:00
Leptopt1los edc3bc6fc1 iButton: TM01x Cyfral && Metakom write support - #4508 (#1209)
* feat(*): tm01 write cyfral and metakom impl

* refactor(*): reuse bitlib

* refactor(*): fix review
2026-11-07 04:43:12 +03:00
MX 24f539c382 subghz add faac tm support 2026-11-07 04:28:03 +03:00
MX 840cb4e69b subghz add hiland support 2026-11-07 03:47:33 +03:00
MX 644cd8c5c2 improve counter modes for keeloq 2026-11-05 17:29:45 +03:00
MX 2d89f1e1f4 fix two protocols 2026-11-04 23:54:26 +03:00
MX 2e9d147ef5 new subghz remote analyzer app and new protocol [ci skip]
also improve some stuff in existing apps
2026-11-04 02:52:09 +03:00
Mykhailo ShevchukandClaude Opus 5 1cdda9a446 SubGHz: fix the 6 test_subghz failures (#1207)
Five encoder round-trips and the random decode count. Two of the five are
protocol bugs, three were the test feeding the decoder something no radio
ever sends.

The round trip fed the encoder's upload straight to the decoder. Nothing
on the device does that: every transmission passes through
furi_hal_subghz_async_tx_middleware_get_duration(), which sums
consecutive same-level durations and emits one per level change, because
the timer only toggles. Legrand emits its sync and the first key bit as
two lows, Dooya and Revers RB2 the frame's last low and the next repeat's
first low - Revers RB2's 320 us tail is meant to join the next frame's
leading 250 us into the ~570 us gap its decoder syncs on. On air each of
those is one carrier gap; fed separately the decoder never syncs. The
test merges them the same way, which also makes the round trip a test of
what we transmit rather than of an intermediate buffer. Only the merging
is mirrored: the middleware also drops a leading low and a trailing
pending low, since the timer can only toggle, but the carrier is off
either side of a burst regardless, so a receiver does see both.

Marantec24 could not decode a frame the Flipper itself sends. The gap
between frames is te_long * 9 + te_short - 15200 us, which is what the
encoder emits - but the decoder centred its window on te_long * 9 and
allowed te_delta * 4, putting that gap exactly 800 us away, and
DURATION_DIFF is compared with '<'. Captured remotes decoded only because
they fall a little short of it: measured across the 99 long lows in
marantec24_raw.sub they run 15118-15188 us, so the worst real frame
cleared the old window by 12 us. The window is centred on the gap now, so
those frames sit 12-82 us off centre instead, and the expression is a
macro the encoder uses too - the two had drifted apart, which is the bug.

Linear Delta 3 decoded one key in three frames instead of one in two, and
none at all from our own encoder. On a complete key it set the step to
SaveDuration without clearing the counters, and LinearDecoderStepReset is
the only other place they are cleared, so the count ran past 8; the next
long gap does leave the parser in Reset, but resyncing from there costs
two more frames, and the encoder sends three repeats in total. The
counters are cleared on a complete key now, as the five sibling decoders
using the same two-identical-keys check already do.

Linear Delta 3 also sends a fourth repeat now. One decode was the whole
margin and it came from the stream's last entry, which the middleware
drops; a fourth frame gives the two-identical-keys check a complete
interior pair. It is the encoder's default rather than an override in the
test fixture, because a receiver needs that frame whoever is listening -
the first frame is spent finding the header, leaving three for a check
that needs two identical keys after it.

Holtek HT12X gets its own decoder reset, clearing last_data as its four
siblings do. The shared reset writes only parser_step, so last_data
survived it and the first frame after a receiver reset could be confirmed
against a key from before it.

Two more things the review turned up:

  * Both test helpers now reset the receiver, so a case starts from a
    known decoder state instead of inheriting one - the Linear Delta 3
    decoder test runs before its encoder test and leaves last_data
    behind. It clears last_data only for the protocols that define their
    own reset, which is why Holtek HT12X needed one; no reset clears the
    bit counters, which only a header re-sync does.
  * Both loops that read the file encoder worker skip a wait marker.
    level_duration_wait() sets only the level field, so the duration bits
    are whatever they were, and the loops fed that to a decoder. It is
    reachable: the worker returns a wait when this loop drains its buffer
    faster than it refills, and feeding one moves the random test's
    count, which is the whole result of that test. The encoder loop needs
    no such check - subghz_transmitter_yield never returns a wait.
  * A test that fails now says why: a protocol missing from the
    receiver's decodable set, a fixture the transmitter would not
    deserialize, a protocol with no encoder (guarded rather than left to
    furi_check, which would take the suite down with it), and a round
    trip whose own decoder read nothing back all printed nothing before.

The random test prints what it decoded when the count differs from
TEST_RANDOM_COUNT_PARSE. That number is the entire result of the test and
it drifts whenever a protocol is added or made stricter - both fixes
above make their decoders fire more often - so it has to be visible to be
updatable. It is deliberately not guessed at here.

CHANGELOG: every entry is one line now, per the rule
`- Category: **Title** (by @author | PR #n | Closes #n)`. 64 entries had
grown a paragraph of prose each; the detail belongs in the PR, which is
linked from the entry.

Host-verified: both firmwares build, lint clean, and each state machine
was transcribed and run through the same merge to reproduce every failure
and then watch the fix take (legrand 0 to 2 decodes, dooya 0 to 2,
marantec24 0 to 1, linear delta 3 0 to 2 at its new default of four
repeats, and 0 at any repeat count below five without the clear). The run that
settles it needs a Flipper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-11-03 17:06:33 +02:00
MX d3c03c16cc improve nero radio dynamic detection
this still does not roll, we are far from it
2026-11-03 01:00:53 +03:00
MX 1804b3982b improve subghz memory handling [ci skip]
remake it to use dynamic scalable approach, add extra checks, apply same fixes to wardriving app
do some small fixes along the way
2026-11-02 07:35:52 +03:00
MX 6197941a06 subghz protocols buffer fixes 2026-11-02 03:53:07 +03:00
MX 06ff65aa41 keeloq mode 7 fix 2026-11-02 03:52:37 +03:00
MX f5e39f9115 subghz: improve ansonic false positives rejection and came twee reading 2026-11-01 14:54:22 +03:00
MX b5982f98bd Merge remote-tracking branch 'OFW/dev' into dev 2026-10-30 22:24:42 +03:00
Kostya4ki 9981d9d4aa NFC: reject Ultralight READ/WRITE starting at the page count (#4483) 2026-10-30 22:23:12 +03:00
Mykhailo ShevchukandClaude Opus 5 2cbad71c92 T5577: Sniff Password, a user dictionary, and two fixes for the 38-bit frame hazard (#1192)
* RFID: do not send a T5577 password to a chip that is not asking

A passworded read and a plain write are both 38 bits and the tag tells
them apart by length alone, so a password frame that reaches a tag with
no password set is parsed as a write of `password << 1` into the block it
addressed - with the password's bit 31 landing on that block's lock bit,
which is one way.

A restore escalates to that frame when its read back fails, and until now
it decided to from the dump alone. Block 0 goes back last, so every write
lands on the chip as it was found while the read back lands on the one the
order has just made: the two take their answer from different places, and
`t5577_restore_password()` is where that is now said once.

Also here, from the same run:

- the plain detect is retried three times before escalating, because it is
  three reads of block 0 and a badly coupled tag can lose all three; a
  false negative there is what reaches for the dangerous frame
- the restore logs which of the two reasons left it unable to check, which
  the screen's "could not check" cannot distinguish
- a block is checked with t5577_block_holds() before its value is
  recovered. Block 1 of an EM4100 key is nine 1s and then long runs of one
  bit - the fewest transitions of any block to lock onto - so a key that
  was written perfectly is exactly where value recovery loses, and the
  restore then called it a tag that had moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: recover a password by listening to a reader or cloner

Sniff Password holds the coil against another device and decodes the
T5577 commands it sends. It transmits nothing - TIM2's external trigger
is wired to the carrier so the carrier resets the timer rather than
clocking it, and the timer only overflows when the carrier stops, which
is a gap. Every antenna pin stays low, so pointing it at hardware you do
not own cannot change anything.

Nothing is decoded in the handler. It records raw gap widths and the
periods between them into a ring, and the framing and decoding run
afterwards over the whole capture: a reader repeats one frame forever,
but a cloner sends each command once, and a framing rule applied in the
handler throws away the only copy.

Only the frame LENGTH picks the command, and 38 bits is a plain write or
a passworded read with the fields in different places. Both readings are
filled and neither is claimed unless the capture rules one out: a pause
too short for anything to have programmed rules out the write, and a
read-mode command after a programming-time wait rules out the read. That
command follows every read as well as every write, so on its own it
proves nothing - the wait is what separates them.

The bit threshold is measured per capture rather than fixed. A fixed one
cannot decode leading-zero mode at all, whose two classes both sit below
it, and a sender this has never met is the normal case. The classes are
the two peaks of a histogram filled from bursts whose length could be a
frame; clustering was tried and separates the data from its outliers
rather than one class from the other.

Validated against a blue cloner: its password recovered identically over
four runs, with its whole write sequence in clear, including the config
words that put the PWD bit on and take it off again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: do not report an unreadable reader from an empty coil

The verdict for "the carrier was cut but never at a width this can
measure" rested on `stalls > 0 && gaps == 0`, and the first half of that
is always true: the timer's trigger pin is never quiet, and open air
alone logs thousands of stalls with nothing in front of it. So an empty
coil reported a reader this could not read - a claim about somebody's
hardware drawn from a failure to parse, which is the one thing this
feature must not do.

Caught on a desk: "Can't measure - carrier cut, but never for a readable
width", with no reader present.

Removed. "Nothing heard" now carries the counts instead of advice. The
advice was also wrong in the case it mattered: "hold it on the reader's
coil" is unhelpful when the coil already is on one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* RFID: keep T5577 passwords the user found in a dictionary of their own

The shipped list says so itself - "Do not edit, this file will be
overwritten after firmware update" - so a password somebody recovered and
added to it survives until the next update and then does not. The NFC key
dictionaries solved this years ago with a system list and a user list;
this is the same split.

t5577_dict_user.lfrfid sits beside the shipped file and is read first. A
word somebody kept is likelier to open the tag in front of them than one
of the hundred-odd the vendor ships, and the order matters for more than
speed: every attempt before the right one is a password frame sent to a
tag that may not want it, and about a third of the shipped words have bit
31 set.

Both totals are counted at the start so the bar and the "of N" on the
status line do not change under the user when the sweep hands over, and
the hand-over happens when the first list runs out rather than when a
word count reaches its total - a list that stops early would otherwise
take the shipped words down with it.

Nothing creates the file. It opens as an empty list when absent, which is
what the sweep already does with a missing shipped list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: offer to keep a sniffed password

The app could learn a password off the air and had nowhere to put it, so
the next tag protected with it got the whole sweep again. Sniff Password
now offers Save, which appends to the user's own list.

Both lists are checked, not just the one being written to. The shipped
list holds a hundred-odd words a cloner is likely to be using - 51243648,
the first password this ever recovered off the air, is already line 79 of
it - so "already known" is the common answer for a stock cloner, and
saying so is more use than silently writing a duplicate. This follows
nfc_key_dict_import() rather than the hand-typed path, which checks only
the user list: a word that came off a cloner is far likelier to be one of
the shipped ones than a word somebody typed.

Offered only where the capture ESTABLISHED a password - a passworded read
or write, or a plain write into block 7. Never for the unresolved 38-bit
reading: that one is a guess between a password and a write, and the list
it would go into is one Clear Password later sends at tags, which is the
frame that cannot be taken back when it is wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: show what the coil is picking up, not a progress bar

A capture has no total to count towards. One press of a cloner is a dozen
commands and every answer it holds, so a bar was wrong twice: first
counting commands against the frame table, which read as a target the
user was failing to fill, then counting down a window nobody had asked
about.

What somebody holding a Flipper against a cloner wants to know is whether
it is hearing anything, and how that changes as they move it. So the
listening screen now draws a trace - one bar per tick, tall where gaps
arrived and flat where none did, scrolling left. It shows activity and
never progress, and it is useful while positioning the coil rather than
only afterwards.

Its own view rather than a mode on the work view: that one's bar is
shared with Read, Restore, Wipe and Clear, where a total genuinely exists
and the bar is right.

A sample goes in every tick whatever happened, so a flat trace is a
measurement - a screen that only moved on activity could not be told from
one that had stopped updating. The scale puts a desk near the floor and a
sender at the ceiling: forty gaps in a tick fills it, against the five a
Flipper on a desk averages and the hundred-plus a cloner puts through one
mid-frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: make the write run findable in a capture full of idling

A sender idles either side of whatever it came to do. A blue cloner's
write run was twelve frames of forty-six, and a second press put the same
run in twice - so the list opened on junk, and its length read as a wall
of findings when most of it was the coil listening to a quiet room.

Two changes. The header now says how many of the frames decoded, so the
count that matters is on screen before any scrolling; it keeps the
dropped count, since the list silently stops at the table's end and a
short list is otherwise indistinguishable from a short capture.

And the Frames button opens on the frame the headline came from rather
than at the top. The verdict already knows which frame it drew the
password from - it just was not saying.

Nothing is hidden. The undecoded rows are the evidence that diagnoses a
sender this cannot frame, and their lengths are the whole of what is
known about them. This is about finding the run, not filtering the list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: stop the sniff result's buttons running into each other

With a password found the screen carries three buttons, and the widget
centres the middle one and right-aligns the last - so their widths simply
have to fit. Measured off a device: "Again" 36 px, "Save" 37, "Frames"
44, which put the centre button's right edge at 82 and the right
button's left edge at 83. One pixel between them, and they read as one
shape.

"More" instead, which is what the sister app calls the same screen and
what the app's own read result does with its third button. The lines that
told the user where to look follow the button's name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: drop the picture from the listening screen

It was the work view's, which shows a card being put ONTO a Flipper -
the opposite of what this screen asks for, where the Flipper goes
against somebody else's reader. A FAP cannot link the firmware's own
icons, so there was no better one to reach for, and drawing a cloner
would have replaced a wrong picture with a guess at one.

Removing it is the honest version and the better layout. The picture was
using sixty of the hundred and twenty-eight pixels, which had squeezed
the trace into a 62 px column and cut the status line to one word. The
trace now runs the full width and holds sixty ticks instead of
twenty-eight - about twelve seconds, so a write run is still on screen
after it finishes rather than scrolling away while the user looks up.

What the picture was failing to say is now said in words at the bottom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: review fixes - say which read back failed, and fix the hazard's own wording

Five review agents over the parts of this branch that had never been looked
at. No crashes and no memory faults; what they found was the documentation
of the hazard being wrong about its own frames, and the screens being
silent about the one thing that cannot be taken back.

The rationale on t5577_restore_password() applied the 38-bit collision to
all eleven frames it governs. Ten of them are passworded WRITES at 70 bits,
which no other command is, so a chip that wants no password simply refuses
them. Only the read back - a passworded read, 38 bits, the same length as a
plain write - is the frame the flag exists to withhold. The derivation now
sits where it is true.

The same function claimed the caller gates it again. It does not: the
caller's test reduces to the same expression over the same dump. What
actually keeps the frame from going out first is the plain detect having
failed, which is evidence from the tag rather than from a file, and the
comment now says so. Three agents found this independently.

The restore's three "could not check" outcomes reached one sentence,
identical to what holding a Flipper over an empty desk produces. They are
now three, because they call for different things: a tag that wants a
password this dump does not hold, the password frame having gone out with
nothing answering it, and nothing answering at all. The detect also raises
its near miss - a chip that answered and then turned out not to be asking
means the frame was read as a write - instead of logging it as success.

Sniff's Save now asks before keeping a word the capture deduced from
neighbouring frames rather than read off a length. The headline already
said "if that was a read"; Save dropped the caveat and wrote it as fact
into a list that Clear Password later sends at tags.

"Nothing heard" claimed no readable gap while in-band gaps had been
counted. It reports both numbers now.

Tests: two read-back cases that a sixteen-mutation sweep left uncovered -
a dump with neither block 0 nor a password, which must not send a frame
carrying zero, and a protected dump whose block 7 was never read, which
must carry the word the chip was opened with rather than block 7's padding.
The host suite also only ever asserted t5577_sniff_commanded() false, so a
version returning false unconditionally passed all of it; both sides of
T5577_SNIFF_COMMAND_MIN are pinned now.

Also: the ring doubled to 2048 entries and three comments still said four
kilobytes; two comments still called the More button Frames; the app
struct called the feature Sniff Reader; the burst-versus-command note and
the why-not-a-bar note were each stated twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: simplify after the review pass

Four cleanup agents. Nothing behavioural except where the review pass had
left two ways of saying one thing.

The dictionary handover wrote its failure path twice, verbatim including
the log line, nested two deep. It is a loop that can only run twice - the
user list goes to the shipped one once, and a second miss returns - so it
is a while now.

The restore result picked three sentences with an if/else-if over a
four-value enum, where this app's own convention is a switch with no
default so a value added later is a build failure rather than a wrong
sentence on screen. The enum's fourth value was never read: every path
that reaches the screen assigns one of the other three first, so Silent is
the zero and the initial state both.

The listen view took a level in pixels, which meant the scene owned both
halves of the scaling - how many gaps saturate a tick, which is a radio
measurement, and how tall a saturated tick is drawn, which is not. It now
takes the two numbers the scene measured and does the arithmetic once, and
the pixel count leaves the header.

The password-to-an-open-chip hazard was spelled out in full in three
places; scene_clear.c was pointed at the authoritative @warning during the
review and the other two were not. The read-back gate's branch selector
was a negated compound with the uninteresting case first, and said "block
0 goes back last" three times in one function.

Also: an unreachable saturation guard on the quiet counter, which the
capture's own timeout already bounds; a comment explaining a loop sixteen
lines below it; a kind name returning the same literal its own fallback
does; and four rationales that were each stated twice.

Two findings recorded and not taken. Spreading the restore's three detect
attempts across timer ticks would cut the worst-case input freeze from
5.76 s to 1.44 s and fits an existing 1-byte hole in the app struct, but
it changes the restore path and wants a tag to test against. Reordering
T5577SniffFrame saves 192 bytes of heap at the cost of separating fields
from the comments that explain them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: list, add and delete the user's own passwords

The app could already learn a password off the air and write it to the
user's dictionary, and it sweeps that dictionary in Clear Password. What
it could not do was show you what is in it.

Passwords, on the main menu, follows the NFC key dictionaries: List, Add
and Delete all act on the USER list only, and the shipped one is only
ever counted. That is not an omission - the shipped file is replaced
wholesale by the next firmware update, so editing it achieves nothing,
and its own header says as much.

Add checks BOTH lists even though it writes to one. The shipped list
holds a hundred and twenty-seven words a cloner is likely to be using, so
"already known" is a more useful answer than a silent duplicate - and it
is the common one. That check is the same helper the sniff result's Save
uses, moved into the app so a word typed in and a word recovered off the
air get the same four answers.

The user's file is still created in exactly one place, by a successful
add. Counting it, listing it and reading a word out of it all open it
OpenExisting, so looking at this screen does not bring a file into being
- which is what T5577_DICT_USER_PATH's documentation promises. NFC uses
OpenAlways throughout and would have created one; this is the one place
the parity is deliberately not followed.

Deleting the last word returns to the menu rather than to a list with
nothing in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: say "system dict" and "user dict", and lift the dialogs off the button

The Passwords screen now reads as the NFC key dictionaries do - same title
shape, same two count lines, same key beside them, Add and List in the
same places. It said "Shipped" and "Yours", which are the same two ideas
under names only this app used; somebody who has met the NFC screen should
not have to learn a second vocabulary for the same file pair.

The key is this app's own. A FAP cannot link the firmware's icons, so
I_Keychain_39x36 is not reachable and Key_39x36.png is drawn to sit in
the same box at the same place.

Separately, t5577_app_say() put its header at y=10 and its body at y=32.
A two line body then ran from 32 to 52 and pushed its descenders through
the OK button, with fourteen empty pixels sitting above it. The app's own
t5577_app_ask() has used 4 and 20 all along and looks right; say() was
the odd one out and now matches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* CHANGELOG: the T5577 sniffer, the user dictionary, and the restore fix

PR numbers go in once the branch is pushed. fap_version stays at 1.1 - the
app is the same app, with more in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: look for the tag a tick at a time, so Back is answered

Every other step of the restore does one unit of work per tick; the detect was
the exception, spending three plain reads and then the password read back in a
single handler. That is around six seconds in which no input is read, so Back
queued unanswered and the presses arrived together at the end - which also left
the dump's own row in the file browser dead until the list was navigated away
from and back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: the screens say "cloner", since that is what is held against them

A door reader works too - the feature was proven against one - but a cloner is
what a user brings to these screens, and "reader" next to a Flipper reads as
the Flipper. Both lines keep the length they had, so neither can start wrapping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: a plain read before the passworded one, a tick each

Reading with a password was the last flow in the app to send the 38 bit frame
on nothing but the user's word. A chip that asks for no password reads it as a
write of that word shifted up, its top bit into the addressed block's lock bit
and not coming back out - so the plain read goes first here as it already does
in Clear Password and in the restore's read back, and a tag that answers it
never needed the frame.

The two detects are separate ticks. Each is most of a second, and two in one
handler is what left Back unanswered in the restore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* CHANGELOG: the unfreeze, and the plain read that now goes first everywhere

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: keep the host tests that were only ever in a scratch folder

21 cases over the sniffer's framing and decode, built on synthetic rings whose
answer is known before any of it meets a cloner, and the MU_TEST body for
t5577_restore_password run on the host. Both found real bugs while the feature
was being written and neither lived anywhere a second person could run them.

They are host tests rather than unit_tests cases because unit_tests cannot link
in this tree - a pre-existing DWARF error, nothing to do with this app.

application.fam has to say "!test": the default source glob is "*.c*" and it
recurses, so the suites would otherwise be compiled into the app, where their
host stubs cannot build and their main() collides with the entry point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: warn about the frame that went out, not the word that was typed

Review of the previous commit. Before a plain read ran first, password_entered
implied the 38 bit frame had been sent, so the read result screen could say
"that frame wrote block 0" on password_entered && !with_pass. The plain read
broke that: it succeeds with with_pass clear and no frame sent at all, so the
one path added to AVOID the hazard became the path reporting it - and the
remedy it offered was to wipe a healthy tag. The fact is now recorded where
the frame is actually built.

Also from the review, all comment or reporting rather than behaviour:
- "That frame is 38 bits" sat above the PLAIN read, which is 6 bits and the
  safe one. It now names the passworded frame and gives both lengths.
- An attempt costs most of two seconds, not "most of a second": a detect is
  three reads of block 0, one per rate. Both places said the smaller figure.
- The restore's "no read back" line credited the tag with an answer it never
  gave - `asks` reads the dump, and by there nothing has been heard from the
  tag at all. It now says so, and logs how many plain reads were spent.
- The restore now says "Trying password" on the tick before the risky frame,
  as the read already did; it showed "Finding tag" throughout.
- CHANGELOG: the menu item is "Passwords", not the heading on the screen it
  opens; a password typed at the prompt is not kept, only one added by hand.
  And the Back entry counted a bug state that never shipped - the three
  attempt loop lands on this same branch, so what users have is two detects.
  The dead file browser row came out with it: that went in on observation and
  file_browser.c drops OK for a row outside the loaded window all by itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: cut the MU_TEST body out of the unit test instead of copying it

The copy committed with the harness was already two cases behind the original:
it predated f13ee509c, which added the `blind` and `kept` cases - and those are
exactly the two the README advertised as the gaps mutation testing had found.
So the suite claimed coverage it did not run, and the "keep it in step" note
had been violated before it was written.

run.py now slices the named MU_TEST out of lfrfid_protocols.c on every run and
writes it to a gitignored file the harness includes, so editing that test
cannot leave this suite behind. 164 lines now, against the copy's 144.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: say why the tests live on the host, including the reason that outlives the bug

The DWARF link error is the reason they were written here; it is not the reason
they belong here. unit_tests is its own app set in fbt_options.py, replacing
main_apps and system_apps, so a case there costs a second firmware and a flash -
which is the wrong price for arithmetic over a synthetic ring buffer. Worth
writing down, so that fixing the link error does not read as an invitation to
move them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: six seconds, not seven - the figure is four attempts of 1.47 s

Derived rather than estimated: a failed block 0 read is 450 ms settle plus a
30 ms silence break plus two frames, about 490 ms, and a failed detect pays
that at all three rates. Three plain attempts and the password read back come
to 5.88 s for a silent tag; seven is only reached when the capture window runs
its full 150 ms each time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: fold the step into the find, and stop saying things twice

Moving on to the blocks is what finding the tag means, so it belongs in
t5577_app_scene_read_found() rather than at both call sites - where one of the
two wrote a step the scene never reads again, the failure path leaving for the
result screen without looking at it.

The restore's password doc comment ended by saying a plain read goes first,
which the paragraph four lines above it now says: that sentence earned its
place when this function still contained the loop.

run.py: the suite names carried their own trailing spaces, stripped back off at
both failure sites and still not lining up with the extract line; the format
string does it now. HERE is gone from the restore include path - the only
quoted include is the generated body, which sits beside the file including it.
And the docstring restated the README's own paragraphs, so two files had to
agree about the same facts.

Left alone on the reviewers' own advice: the restore's three-level detect
branch, where every flatter form trades the nesting for a subtler obscurity;
open_try as a struct field, which is where this app keeps every per-run
counter; and password_entered beside password_sent, which have different
lifetimes as well as different meanings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* CHANGELOG: PR #1192

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: drop the host test harness from the repo

Not something this repo carries. The MU_TESTs for t5577_restore_password stay
where they belong, in applications/debug/unit_tests/tests/lfrfid.

application.fam goes back to the default source glob with them: "!test" was
only there to keep the harness out of the app build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-29 23:24:15 +02:00
MX 5178d39cf0 port dooya custom button support
by Lechnio
2026-10-28 16:05:20 +03:00
MX 4c5e74cce3 finish nice o code 2026-10-27 00:41:25 +03:00
MX c66f01dce7 fix nice o code counter 2026-10-26 01:19:26 +03:00
MX d920ae3532 fix false reads in nice o code 2026-10-24 19:21:53 +03:00
MX 6dcf8b822b nice o code counter fix 2026-10-24 15:30:25 +03:00
MX 44ef0b91ea show rolling nero radio remotes too
it will not work fully, only same capture replay, if captured out of range will work one time!
2026-10-23 16:42:04 +03:00
Mykhailo ShevchukandClaude Opus 5 e611e31583 RFID: review follow-ups to the T5577 dumps in the Saved list (#1189)
* RFID: review follow-ups to the T5577 dumps in the Saved list

What #1188 left behind, found by reviewing it again after it was open.

The missing-app dialog told the user to install the T5577 app from the
apps catalogue. It is not there: the .fap ships in the firmware's own SD
resources, which is what the message says now, and the comment on
T5577_APP_PATH said the same wrong thing.

A dump that will not open now says whether it is no longer on the card or
is not a dump this version reads - one message covered four causes, and
the likeliest one on a handoff, the file being gone, was not even logged.
Long names are cut to fit, as the file menu's header already did; that
clipping is one helper now rather than two copies.

The T5577 app's Saved no longer counts the dumps before opening the
browser. The browser draws "<Empty>" for an empty folder by itself, so
the scan only chose the cursor row - and since dumps moved in beside the
keys it read past every key in the folder first, then the browser read
the same folder twice more. The legacy folder is still listed, being a
subfolder, and is now made only when a block test writes its backup
rather than at every launch.

In the lfrfid app, a launch argument naming a dump no longer needs a verb
of its own: the extension already says what it is, which removes the
browse branch and its custom event, and the start scene goes back to
plain menu dispatch. A dump renamed in the T5577 app reopens the list in
the folder it was in rather than at the root. The path check asks the
storage service whether the file is under the app folder instead of
comparing bytes, since /ext is case-insensitive.

Archive: "Show" asks a predicate rather than comparing a file type
against ArchiveFileTypeBadUsb in three places, so where a type sits in
the enum stops deciding whether it can be shown as text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: keep the test backup out of the open-dump path

A block test borrowed app->path to build its backup's filename. That
string is the dump the user has open, and the saved browser opens where
it points - so after a test, Saved went to the backup's folder instead
of the dumps folder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-23 15:21:02 +03:00
MX 2534886419 some more nice o code fixes 2026-10-23 05:57:24 +03:00
Mykhailo ShevchukandClaude Opus 5 09c6ba256a RFID: show T5577 dumps in the 125 kHz Saved list and open them in their app (#1188)
* RFID: show T5577 dumps in the Saved list and open them in their app

The T5577 app saved into a subfolder of the lfrfid one, and the lfrfid
saved list filters on .rfid alone - so the folder appeared, and entering
it showed nothing.

Dumps now save to /ext/lfrfid beside the keys, and the saved list and the
archive's 125 kHz tab list both kinds. Picking a dump hands it to the
T5577 app, which opens it on its own file menu, and queues this app back
behind it on the same file, the way nfc_app_run_external chains mfkey32.
A path ending in .t5577 passed to the lfrfid app now means that list,
where before the app started and immediately exited.

In the T5577 app the browser became a scene of its own, so leaving a dump
returns to the list it was picked from instead of the start menu, and a
rename now writes to the folder the dump is in rather than the app folder
- opened from a subfolder or the archive, it used to move the file and
report success. Its own folder keeps the test backup and the dumps older
versions saved there, so neither shows up among the user's keys.

The archive's tab filters moved into a table of their own, which drops
two pseudo file types that existed only to carry a filter string.

Closes #1187

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chlog: T5577 dumps in the Saved list

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-23 04:22:45 +03:00
MX 3c5f8a29cc remake nice o code cert 2026-10-23 04:19:20 +03:00
Mykhailo ShevchukandClaude Opus 5 1672f7c54d LF RFID: full T5577 support - read a tag's blocks, save them, and write them back (#1179)
Adds the T5577 block read to lib/lfrfid, the .t5577 dump format, and a bundled
T5577 app: read a tag's blocks and save them, look them over, edit any block or
compose block 0 by name, write a dump back with every block read back again,
test which blocks a chip really takes, wipe a tag, and clear its password.

Wipe and Clear Password move here from RFID -> Extra Actions, since both
address blocks and are judged by reading one back. Their password list moves
out of internal flash to /ext/lfrfid/assets/t5577_dict.lfrfid, read with
keys_dict the same way the NFC key dictionaries are.

Also rebuilds the lfrfid "Write and set password" flow on verified blocks, and
bounds a verify read that could wait forever - the two together left a tag
protected with the screen still on "Writing with password".

Fixes #1180.

API 88.22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaBUeP1J6NwqCJjhbnTUND
2026-10-21 16:02:47 +03:00
Mykhailo ShevchukandClaude Opus 5 5082af55ba Core: parse decimals directly instead of via newlib's strtod (-7,424 B) (#1186)
`__wrap_strtof`/`__wrap_strtod` forwarded to newlib's `strtof_l`/`strtod_l`,
which pull in `_Balloc`/`__gethex` arbitrary-precision machinery for hex-float
literals, inf/nan spellings and correct rounding of long mantissas. That is
8,100 bytes of flash for input nothing passes them. What reaches these is file
fields (`flipper_format_stream.c:391`), CLI arguments (`args.c:45`, `args.c:126`)
and JS/JSON numbers (`mjs_tok.c`, `mjs_parser.c`, `mjs_json.c`) - plain decimal
literals in every case.

Parse it directly: accumulate a `uint64_t` mantissa and a decimal exponent,
then scale by binary exponentiation over a 9-entry power-of-ten table. An
integer needs no scaling at all, so it still converts exactly up to 2^53.

    .text      700,256 -> 693,252   -7,004
    .rodata    172,308 -> 171,888     -420
    flash pages    214 -> 212

Checked by linking this file against the host libc and comparing every value
and tail pointer with the host's correctly-rounded `strtod`/`strtof` over
~480,000 inputs: no tail mismatch, worst 4 ulp as a double (0 ulp on 55% of
cases, >=3 ulp on 0.3%), denormals exact down to `5e-324`, and plain integers
to 2^53 at 0 ulp. `strtof` is bit-exact on everything `"%f"` emits, which is
the `.ir`/`.sub`/`.nfc` round-trip contract; it can differ by 1 float ulp when
a ~10-digit integer carries a ~10-digit fraction tail, because the double it
narrows from rounds twice.

These are exported to apps (`api_symbols.csv:3722-3725`), so the narrowed
contract is public and `wrappers.h` states it: no hex floats, no `inf`/`nan`
spellings, and a value within 2 ulp of `DBL_MAX` saturates to infinity.
`errno = ERANGE` on overflow and underflow is kept - `args_read_duration()`
checks it - and the tail pointer follows the standard throughout.

mJS special-cased hex for lowercase `0x` only and let `0X` fall through to
newlib's hex-float support, so `var a = 0X1A;` would have become a parse error.
Teach `mjs_tok.c` and `mjs_parser.c` the uppercase form instead.

No unit test: `FIRMWARE_APP_SET=unit_tests` does not link on dev, so one cannot
even be compiled. Filed as #1185; the test is ready to land once that is fixed.

Closes #1184

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-21 15:43:24 +03:00
Mykhailo ShevchukandClaude Opus 5 a70a69d9d4 RFID: share the duplicated write_data bodies (-992 B of flash) (#1183)
* RFID: share the duplicated write_data bodies (-992 B of flash)

Nearly every LF RFID protocol filled a write request the same way and differed
only in its config word: protocol_viking_write_data() and
protocol_jablotron_write_data() were byte-identical except for four constants,
and six protocols carried a verbatim copy of the same 64-bit reversal loop.

27 call sites across 20 protocols now share two bodies in
protocols/lfrfid_write_fill.c. The block or word count is read out of the
config word's maxblock field rather than passed alongside it - it is the same
number in both places at all 27 sites, and the chip streams what that field
says, so the two could never legitimately disagree.

Behaviour is unchanged. Each original inline loop was transcribed and compared
against the shared body over random vectors for every bit width in use, and the
derived counts were checked against the ones the call sites used to pass.

No API change: the per-protocol callbacks were never exported, and
protocol_dict_get_write_data() and the lfrfid_protocols vtable are untouched.

Left alone on purpose: em4100, electra and h10301 keep their encoded data as a
uint64_t or uint32_t[] rather than an MSB-first byte buffer, so a bit_lib
helper would byte-swap them; and gallagher's EM4305 arm reads its payload one
bit higher than every sibling (#1181), which folding would silently normalise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JoYukcdV7vyusuaXtYcEdu

* upd changelog

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JoYukcdV7vyusuaXtYcEdu

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-20 21:41:38 +03:00
MX 07c6f68032 Merge remote-tracking branch 'OFW/dev' into dev 2026-10-20 03:33:25 +03:00
Zhaoqi Xu d8dfb3147c FatFs: apply upstream R0.12c patches, fix 0-byte files in fragmented exFAT directories (#4477)
* fatfs: clear n_frag when opening an existing file on exFAT

This is ChaN's FatFs R0.12c patch 1 (2017-03-11). f_open() did not
reset fp->obj.n_frag when it opened an existing file on an exFAT
volume, so a FIL taken from uninitialized memory could carry a stale
fragment count into f_write()/f_lseek() and break the FAT chain.

* fatfs: set NoFatChain again when f_truncate makes a chain contiguous

This is ChaN's FatFs R0.12c patch 2 (2017-03-18). On exFAT, a file that
was already fragmented when it was opened kept a FAT chain after
f_truncate() made it contiguous again, so later reads and writes kept
walking the FAT for no reason.

* fatfs: fix exFAT directory growth and f_sync in a stretched directory

This is ChaN's FatFs R0.12c patch 3 (2017-04-22): "On the exFAT volume,
new cluster allocation for directory fails with FR_INT_ERR."

load_obj_dir() left dp->obj.n_frag uninitialized. f_sync() calls it
with a DIR on the stack, so when the file's entry sits past the first
cluster of a FAT-chained directory, get_fat() can see a non-zero n_frag,
return EOC, and the directory entry is never updated: the data is
written but the file stays 0 bytes and its clusters leak.

dir_register() also needed the rest of the patch: clear the 'stretched'
flag after handling it, fill the FAT fragments for the root directory
too, and grow objsize only after the containing entry block is loaded.
With only the n_frag line, creating the next file in the same directory
fails with FR_INT_ERR.

Fixes #4464
2026-10-20 03:26:51 +03:00
MX 525dcce0e4 improve fdx-b reading 2026-10-20 00:43:33 +03:00
MX 3c6d6fa34f nice o code little fix 2026-10-19 00:58:13 +03:00
MX 79f306afbb some subghz remote fixes and api method 2026-10-18 18:00:35 +03:00
MX 5d3c950e98 nice o code 112 bit support?
and update docs on that topic too
2026-10-17 23:13:23 +03:00