mirror of
https://github.com/i12bp8/TagTinker.git
synced 2026-08-28 13:24:08 +00:00
Initial release
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
# Build artifacts
|
||||
*.o
|
||||
*.d
|
||||
*.elf
|
||||
*.fap
|
||||
*.map
|
||||
dist/
|
||||
|
||||
# ufbt
|
||||
.ufbt/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Python bytecode
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# OS garbage
|
||||
.DS_Store
|
||||
Desktop.ini
|
||||
@@ -0,0 +1,58 @@
|
||||
# Contributing to TagTinker
|
||||
|
||||
Thanks for wanting to help out. Here's how to get involved.
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Fork the repo
|
||||
2. Create a feature branch: `git checkout -b my-feature`
|
||||
3. Make your changes
|
||||
4. Test on owned lab hardware if possible (or at minimum, ensure `ufbt build` passes)
|
||||
5. Commit with a clear message
|
||||
6. Open a pull request
|
||||
|
||||
## Code style
|
||||
|
||||
- C99, no C++ features
|
||||
- 4-space indentation (no tabs)
|
||||
- `snake_case` for functions and variables
|
||||
- Keep functions short and focused
|
||||
- Comment non-obvious logic, but don't over-comment
|
||||
|
||||
## What we're looking for
|
||||
|
||||
- Bug fixes (especially memory-related — the Flipper has very limited heap)
|
||||
- New ESL tag compatibility (different sizes and display variants)
|
||||
- Better compression or transmission reliability
|
||||
- UI/UX improvements
|
||||
- Documentation improvements
|
||||
|
||||
## Project boundaries
|
||||
|
||||
- Keep contributions aligned with educational use on ESL hardware the operator owns.
|
||||
- Do not add documentation aimed at store use, retail deployment, or third-party systems.
|
||||
- Prefer generic ESL wording over store- or brand-specific framing in user-facing docs.
|
||||
- Preserve credit to furrtek's reverse-engineering research where relevant.
|
||||
|
||||
## What to avoid
|
||||
|
||||
- Don't break existing functionality without good reason
|
||||
- Don't add external dependencies — this runs on a microcontroller with strict constraints
|
||||
- Don't submit AI-generated code without reviewing and testing it
|
||||
- Keep memory usage minimal — every byte counts on the Flipper Zero
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Open a GitHub issue with:
|
||||
- What you expected to happen
|
||||
- What actually happened
|
||||
- Your Flipper firmware version
|
||||
- The ESL model you're using (if applicable)
|
||||
|
||||
## Hardware safety
|
||||
|
||||
Be careful with repeated refresh or LED test loops on your own tags. Power and display behavior vary by model, and long-running tests can have side effects.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under GPL-3.0.
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,152 @@
|
||||
# TagTinker
|
||||
|
||||
<p align="center">
|
||||
<strong>Educational ESL research tools for Flipper Zero</strong><br>
|
||||
<sub>Protocol study, signal analysis, and controlled display experiments on authorized lab hardware</sub>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="License: GPL-3.0" src="https://img.shields.io/badge/license-GPL--3.0-blue.svg">
|
||||
<img alt="Platform: Flipper Zero" src="https://img.shields.io/badge/platform-Flipper%20Zero-orange.svg">
|
||||
<img alt="Status: Research Project" src="https://img.shields.io/badge/status-Research%20Project-informational.svg">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<img src="https://github.com/i12bp8/TagTinker/blob/main/demo.jpg">
|
||||
<p align="center"><sub>Owner-authorized lab ESL display experiment</sub></p>
|
||||
|
||||
## Overview
|
||||
|
||||
TagTinker is an educational electronic shelf label (ESL) research project for Flipper Zero.
|
||||
|
||||
It is intended solely for:
|
||||
|
||||
- protocol study,
|
||||
- signal analysis,
|
||||
- interoperability research,
|
||||
- and controlled display experiments on ESL hardware that you personally own or are explicitly authorized to test.
|
||||
|
||||
This project is maintained as a home-lab and research tool. It is **not** intended for use against deployed retail systems, commercial infrastructure, or third-party equipment.
|
||||
|
||||
---
|
||||
|
||||
## Important Notice
|
||||
|
||||
Use this project **only** on hardware that:
|
||||
|
||||
- you personally own, or
|
||||
- you have explicit permission to study and test in a controlled environment.
|
||||
|
||||
Do **not** use this project:
|
||||
|
||||
- in stores,
|
||||
- on deployed retail systems,
|
||||
- on third-party equipment,
|
||||
- on production infrastructure,
|
||||
- to alter prices, product information, or operational displays,
|
||||
- to interfere with commercial operations,
|
||||
- to bypass pairing, authorization, access controls, or security measures,
|
||||
- or in any manner that is unlawful, unauthorized, or harmful.
|
||||
|
||||
The publication of this source code does **not** grant permission to access, modify, transmit to, or interfere with any third-party system or device.
|
||||
|
||||
---
|
||||
|
||||
## Project Scope
|
||||
|
||||
This repository is limited to research and lab use, including:
|
||||
|
||||
- educational study of infrared ESL communication,
|
||||
- display and protocol experiments on authorized test hardware,
|
||||
- local tooling for preparing monochrome display assets,
|
||||
- controlled diagnostics and signal observation for interoperability research.
|
||||
|
||||
This README intentionally does **not** include installation, deployment, targeting, or operational instructions for use against deployed systems.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- Lab-only ESL protocol study tools
|
||||
- Controlled display experiments for authorized test hardware
|
||||
- Single-device research functions for text, image, test-pattern, and LED-response experiments
|
||||
- Local image preparation utility in `tools/tagtinker.html`
|
||||
|
||||
---
|
||||
|
||||
## Responsible Use
|
||||
|
||||
TagTinker is published for legitimate educational, research, and interoperability purposes only.
|
||||
|
||||
You are solely responsible for ensuring that any use of this project complies with:
|
||||
|
||||
- applicable law,
|
||||
- property rights,
|
||||
- contractual restrictions,
|
||||
- operator policies,
|
||||
- and all required permissions and authorizations.
|
||||
|
||||
If you do not own the hardware or do not have explicit authorization to test it, do not use this project on that hardware.
|
||||
|
||||
---
|
||||
|
||||
## No Affiliation
|
||||
|
||||
This project is an independent research effort.
|
||||
|
||||
It is **not** affiliated with, endorsed by, authorized by, or sponsored by any ESL manufacturer, retailer, or system operator.
|
||||
|
||||
Any brand names or product names referenced in the source tree or documentation are used only where necessary for descriptive, compatibility, or research purposes.
|
||||
|
||||
---
|
||||
|
||||
## Security and Disclosure
|
||||
|
||||
This repository is intended to support lawful research and interoperability work.
|
||||
|
||||
If you are a vendor, operator, or security researcher and believe some part of this repository raises a concrete security concern, please open a responsible disclosure issue or contact the maintainer privately with sufficient detail.
|
||||
|
||||
---
|
||||
|
||||
## Distribution Policy
|
||||
|
||||
This repository contains source code for research purposes.
|
||||
|
||||
To reduce the risk of misuse, this project does not provide:
|
||||
|
||||
- hosted binaries,
|
||||
- prebuilt application files,
|
||||
- automated deployment packages,
|
||||
- or step-by-step operational instructions for use on deployed systems.
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
This project builds on public reverse-engineering research by [furrtek](https://github.com/furrtek/PrecIR) and related community work.
|
||||
|
||||
Please see the upstream project for prior research context and historical protocol analysis.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Licensed under **GPL-3.0**.
|
||||
See [`LICENSE`](LICENSE) for details.
|
||||
|
||||
If this project incorporates or adapts GPL-covered upstream work, the corresponding notices and licensing terms apply accordingly.
|
||||
|
||||
---
|
||||
|
||||
## Warranty Disclaimer
|
||||
|
||||
This project is provided **“as is”**, without warranty of any kind, express or implied, including but not limited to merchantability, fitness for a particular purpose, and noninfringement, to the maximum extent permitted by applicable law.
|
||||
|
||||
---
|
||||
|
||||
## Maintainer Statement
|
||||
|
||||
This project is maintained as a narrowly scoped educational and research repository.
|
||||
|
||||
The maintainer does not authorize, encourage, or condone use against third-party devices, deployed retail systems, or any environment without explicit permission.
|
||||
@@ -0,0 +1,20 @@
|
||||
App(
|
||||
appid="tagtinker",
|
||||
name="TagTinker",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="tagtinker_app_main",
|
||||
requires=["gui", "notification", "dialogs", "storage"],
|
||||
stack_size=4 * 1024,
|
||||
fap_icon="tagtinker_10px.png",
|
||||
fap_category="Infrared",
|
||||
fap_author="i12bp8",
|
||||
fap_description="Educational ESL study tool for owned hardware",
|
||||
fap_version="1.1",
|
||||
sources=[
|
||||
"tagtinker_app.c",
|
||||
"scenes/*.c",
|
||||
"protocol/*.c",
|
||||
"ir/*.c",
|
||||
"views/*.c",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* TagTinker IR transmitter - implementation (v2)
|
||||
*
|
||||
* Drives the Flipper Zero's built-in IR LEDs at ~1.255 MHz carrier
|
||||
* for the ESL pulse-position modulation (PPM) protocol used here.
|
||||
*
|
||||
* v2 changes from v1:
|
||||
* - Removed TIM2 ISR approach (TIM2 conflicts with Flipper's IR RX subsystem!)
|
||||
* - Uses DWT cycle counter for precise timing instead (zero timer conflicts)
|
||||
* - Properly handles TIM1 bus state (was crashing if already enabled)
|
||||
* - Non-blocking repeat loop with cancellation support
|
||||
*
|
||||
* Architecture:
|
||||
* TIM1 Channel 3N: 1.255 MHz PWM carrier on built-in IR LED (PB9)
|
||||
* DWT->CYCCNT: Cycle-accurate timing for PPM symbol encoding
|
||||
*
|
||||
* CRITICAL: The built-in IR LED is on PB9 = TIM1_CH3N (complementary output).
|
||||
* NOT CH3! The carrier is gated by toggling OC3M bits in TIM1->CCMR2
|
||||
* (which control both CH3 and CH3N). The firmware uses PWM2 mode.
|
||||
* For CH3N (complementary), Force Inactive = LED off, PWM2 = carrier on.
|
||||
*/
|
||||
|
||||
#include "tagtinker_ir.h"
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <furi_hal_bus.h>
|
||||
#include <furi_hal_resources.h>
|
||||
#include <furi_hal_gpio.h>
|
||||
#include <furi_hal_cortex.h>
|
||||
|
||||
#include <stm32wbxx_ll_tim.h>
|
||||
|
||||
/* ─── Carrier configuration ───
|
||||
* Target: 1.25 MHz carrier for ESL signaling
|
||||
* Flipper: 64 MHz system clock, TIM1 PSC=0
|
||||
* ARR = 51-1 = 50 → 64MHz/51 = 1,254,901 Hz (+4.9 kHz off, within ±10kHz)
|
||||
*/
|
||||
#define CARRIER_TIM TIM1
|
||||
#define CARRIER_ARR (51 - 1)
|
||||
#define CARRIER_CCR 25 /* ~50% duty cycle */
|
||||
|
||||
/* ─── PP4 timing in CPU cycles (64 MHz) ───
|
||||
*
|
||||
* Protocol reference: ESL Blaster FW03 ir.c, furrtek.org ESL page
|
||||
* Base period t = 1/32768 Hz = 30.518 µs
|
||||
*
|
||||
* Burst duration: ~40 µs = 50 carrier cycles at 1.25 MHz
|
||||
* ESL Blaster: 4 ticks × 10.08µs = 40.32 µs
|
||||
* Our value: 40 µs × 64 = 2560 cycles
|
||||
*
|
||||
* Symbol gap durations (index = 2-bit symbol value):
|
||||
* From ir.c pp4_steps ordering:
|
||||
* pp4_steps[0] = 5 ticks → sym 00: 6 × 10.08 = 60.48 µs
|
||||
* pp4_steps[1] = 23 ticks → sym 01: 24 × 10.08 = 241.92 µs
|
||||
* pp4_steps[2] = 11 ticks → sym 10: 12 × 10.08 = 120.96 µs
|
||||
* pp4_steps[3] = 17 ticks → sym 11: 18 × 10.08 = 181.44 µs
|
||||
*
|
||||
* Converting to 64 MHz cycles:
|
||||
*/
|
||||
#define PP4_BURST_CYCLES 2581 /* 40.33 µs */
|
||||
static const uint32_t pp4_gap_cycles[4] = {
|
||||
3871, /* Symbol 00: 60.48 µs */
|
||||
15483, /* Symbol 01: 241.92 µs */
|
||||
7741, /* Symbol 10: 120.96 µs */
|
||||
11612, /* Symbol 11: 181.44 µs */
|
||||
};
|
||||
|
||||
/* ─── PP16 timing in CPU cycles (64 MHz) ───
|
||||
* Derived from esl_blaster ir.c: TIM16 steps of 4us.
|
||||
* Gap values (27us to 107us) mapped to 16 symbols.
|
||||
* Burst: 21us * 64 = 1344 cycles.
|
||||
*/
|
||||
#define PP16_BURST_CYCLES 1344
|
||||
static const uint32_t pp16_gap_cycles[16] = {
|
||||
1728, // 0000: 27µs
|
||||
3264, // 0001: 51µs
|
||||
2240, // 0010: 35µs
|
||||
2752, // 0011: 43µs
|
||||
9408, // 0100: 147µs
|
||||
7872, // 0101: 123µs
|
||||
8896, // 0110: 139µs
|
||||
8384, // 0111: 131µs
|
||||
5312, // 1000: 83µs
|
||||
3776, // 1001: 59µs
|
||||
4800, // 1010: 75µs
|
||||
4288, // 1011: 67µs
|
||||
5824, // 1100: 91µs
|
||||
7360, // 1101: 115µs
|
||||
6336, // 1110: 99µs
|
||||
6848 // 1111: 107µs
|
||||
};
|
||||
|
||||
/* ─── Module state ─── */
|
||||
static bool ir_initialized = false;
|
||||
static volatile bool ir_stop_requested = false;
|
||||
|
||||
/* ─── Carrier control (TIM1 OC3M register manipulation) ─── */
|
||||
|
||||
static inline void carrier_on(void) {
|
||||
/* OC3M = PWM Mode 2 (111) — matching Flipper firmware.
|
||||
* For CH3N (complementary): PWM2 inverts → CH3N gets active-low PWM = carrier burst.
|
||||
* This matches INFRARED_TX_CCMR_HIGH in furi_hal_infrared.c */
|
||||
uint32_t ccmr2 = CARRIER_TIM->CCMR2;
|
||||
ccmr2 &= ~(TIM_CCMR2_OC3M);
|
||||
ccmr2 |= (TIM_CCMR2_OC3M_2 | TIM_CCMR2_OC3M_1 | TIM_CCMR2_OC3M_0); /* PWM mode 2 */
|
||||
CARRIER_TIM->CCMR2 = ccmr2;
|
||||
}
|
||||
|
||||
static inline void carrier_off(void) {
|
||||
/* OC3M = Force Inactive (100) — CH3N goes idle = LED off.
|
||||
* This matches INFRARED_TX_CCMR_LOW in furi_hal_infrared.c */
|
||||
uint32_t ccmr2 = CARRIER_TIM->CCMR2;
|
||||
ccmr2 &= ~(TIM_CCMR2_OC3M);
|
||||
ccmr2 |= TIM_CCMR2_OC3M_2;
|
||||
CARRIER_TIM->CCMR2 = ccmr2;
|
||||
}
|
||||
|
||||
/* ─── Cycle-accurate delay using DWT ─── */
|
||||
|
||||
static inline void delay_cycles(uint32_t cycles) {
|
||||
uint32_t start = DWT->CYCCNT;
|
||||
while((DWT->CYCCNT - start) < cycles) {
|
||||
/* Busy wait — handles wraparound via unsigned subtraction */
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Send a single PP4-encoded frame ───
|
||||
*
|
||||
* PPM on-air sequence for N symbols:
|
||||
* [burst][gap₀][burst][gap₁]...[burst][gapₙ₋₁][burst]
|
||||
* = N+1 bursts with N gaps, each gap encoding one 2-bit symbol.
|
||||
*
|
||||
* Data is sent LSB first, 2 bits at a time per byte (matching ir.c).
|
||||
*/
|
||||
static void send_frame_pp4(const uint8_t* data, size_t len) {
|
||||
for(size_t byte_idx = 0; byte_idx < len; byte_idx++) {
|
||||
uint8_t current_byte = data[byte_idx];
|
||||
|
||||
for(int sym = 0; sym < 4; sym++) {
|
||||
uint8_t symbol = current_byte & 0x03;
|
||||
current_byte >>= 2;
|
||||
|
||||
/* Burst (carrier ON) */
|
||||
carrier_on();
|
||||
delay_cycles(PP4_BURST_CYCLES);
|
||||
|
||||
/* Gap (carrier OFF) — duration encodes the symbol */
|
||||
carrier_off();
|
||||
delay_cycles(pp4_gap_cycles[symbol]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Final burst (N+1th burst, required by PPM protocol) */
|
||||
carrier_on();
|
||||
delay_cycles(PP4_BURST_CYCLES);
|
||||
carrier_off();
|
||||
}
|
||||
|
||||
/* ─── Send a single PP16-encoded frame ───
|
||||
* Encodes 4 bits per symbol, resulting in half the pulses of PP4.
|
||||
*/
|
||||
static void send_frame_pp16(const uint8_t* data, size_t len) {
|
||||
for(size_t byte_idx = 0; byte_idx < len; byte_idx++) {
|
||||
uint8_t current_byte = data[byte_idx];
|
||||
|
||||
for(int sym = 0; sym < 2; sym++) {
|
||||
uint8_t symbol = current_byte & 0x0F;
|
||||
current_byte >>= 4;
|
||||
|
||||
/* Burst (carrier ON) */
|
||||
carrier_on();
|
||||
delay_cycles(PP16_BURST_CYCLES);
|
||||
|
||||
/* Gap (carrier OFF) */
|
||||
carrier_off();
|
||||
delay_cycles(pp16_gap_cycles[symbol]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Final burst */
|
||||
carrier_on();
|
||||
delay_cycles(PP16_BURST_CYCLES);
|
||||
carrier_off();
|
||||
}
|
||||
|
||||
/* ─── Public API ─── */
|
||||
|
||||
void tagtinker_ir_init(void) {
|
||||
if(ir_initialized) return;
|
||||
|
||||
/* Safely claim TIM1: must handle case where it's already enabled
|
||||
* by the firmware's IR subsystem. Bus enable asserts if already on! */
|
||||
if(furi_hal_bus_is_enabled(FuriHalBusTIM1)) {
|
||||
furi_hal_bus_disable(FuriHalBusTIM1);
|
||||
}
|
||||
furi_hal_bus_enable(FuriHalBusTIM1);
|
||||
|
||||
/* Configure GPIO for IR TX (built-in IR LEDs) */
|
||||
furi_hal_gpio_init_ex(
|
||||
&gpio_infrared_tx,
|
||||
GpioModeAltFunctionPushPull,
|
||||
GpioPullNo,
|
||||
GpioSpeedVeryHigh,
|
||||
GpioAltFn1TIM1);
|
||||
|
||||
/* Configure TIM1 as 1.255 MHz carrier */
|
||||
LL_TIM_SetPrescaler(CARRIER_TIM, 0);
|
||||
LL_TIM_SetAutoReload(CARRIER_TIM, CARRIER_ARR);
|
||||
LL_TIM_SetCounter(CARRIER_TIM, 0);
|
||||
|
||||
/* Channel 3 config — controls both CH3 and CH3N outputs.
|
||||
* The IR LED is on CH3N (PB9), so we must enable CC3NE.
|
||||
* PWM2 mode with preload, matching the firmware. */
|
||||
LL_TIM_OC_SetMode(CARRIER_TIM, LL_TIM_CHANNEL_CH3, LL_TIM_OCMODE_PWM2);
|
||||
LL_TIM_OC_SetCompareCH3(CARRIER_TIM, CARRIER_CCR);
|
||||
LL_TIM_OC_EnablePreload(CARRIER_TIM, LL_TIM_CHANNEL_CH3);
|
||||
|
||||
/* CRITICAL: Enable CH3N (complementary output), NOT CH3!
|
||||
* IR LED is on PB9 = TIM1_CH3N. Without this, no IR output at all. */
|
||||
LL_TIM_CC_EnableChannel(CARRIER_TIM, LL_TIM_CHANNEL_CH3N);
|
||||
|
||||
/* Main output enable (required for TIM1 advanced timer) */
|
||||
LL_TIM_EnableAllOutputs(CARRIER_TIM);
|
||||
|
||||
/* Start timer but force carrier OFF initially */
|
||||
carrier_off();
|
||||
LL_TIM_EnableCounter(CARRIER_TIM);
|
||||
LL_TIM_GenerateEvent_UPDATE(CARRIER_TIM);
|
||||
|
||||
ir_stop_requested = false;
|
||||
ir_initialized = true;
|
||||
|
||||
FURI_LOG_I("TagTinker", "IR TX initialized: carrier %.3f MHz",
|
||||
(double)(64000000.0f / (CARRIER_ARR + 1) / 1000000.0f));
|
||||
}
|
||||
|
||||
void tagtinker_ir_deinit(void) {
|
||||
if(!ir_initialized) return;
|
||||
|
||||
tagtinker_ir_stop();
|
||||
|
||||
/* Force carrier off and disable TIM1 */
|
||||
carrier_off();
|
||||
LL_TIM_DisableAllOutputs(CARRIER_TIM);
|
||||
LL_TIM_CC_DisableChannel(CARRIER_TIM, LL_TIM_CHANNEL_CH3N);
|
||||
LL_TIM_DisableCounter(CARRIER_TIM);
|
||||
|
||||
/* Reset TIM1 bus so firmware can reclaim it for normal IR */
|
||||
if(furi_hal_bus_is_enabled(FuriHalBusTIM1)) {
|
||||
furi_hal_bus_disable(FuriHalBusTIM1);
|
||||
}
|
||||
|
||||
/* Restore GPIO to safe state */
|
||||
furi_hal_gpio_init(&gpio_infrared_tx, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
||||
|
||||
ir_initialized = false;
|
||||
FURI_LOG_I("TagTinker", "IR TX deinitialized");
|
||||
}
|
||||
|
||||
bool tagtinker_ir_transmit(const uint8_t* data, size_t len, uint16_t repeats_raw, uint8_t delay) {
|
||||
if(!ir_initialized) return false;
|
||||
if(len == 0 || len > 255) return false;
|
||||
|
||||
ir_stop_requested = false;
|
||||
|
||||
// MSB of repeats indicates PP16 protocol!
|
||||
bool is_pp16 = (repeats_raw & 0x8000) != 0;
|
||||
uint32_t repeats = repeats_raw & 0x7FFF;
|
||||
|
||||
FURI_LOG_I("TagTinker", "TX start: %zu bytes, %lu repeats (PP%d), %u delay",
|
||||
len, repeats, is_pp16 ? 16 : 4, delay);
|
||||
|
||||
/* Transmit frame with repeats.
|
||||
* Between repeats we yield briefly so FreeRTOS stays happy
|
||||
* and the user can cancel via tagtinker_ir_stop(). */
|
||||
for(uint32_t rep = 0; rep <= repeats; rep++) {
|
||||
if(ir_stop_requested) {
|
||||
FURI_LOG_I("TagTinker", "TX cancelled at repeat %lu", rep);
|
||||
carrier_off();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Send one frame (interrupts stay enabled — DWT handles timing) */
|
||||
if(is_pp16) {
|
||||
send_frame_pp16(data, len);
|
||||
} else {
|
||||
send_frame_pp4(data, len);
|
||||
}
|
||||
|
||||
/* Delay between repeats: delay × 500µs (matching ESL Blaster).
|
||||
* ESL Blaster: TickCounter = RepeatDelay * 50 ticks, each ~10µs = 500µs per unit */
|
||||
if(rep < repeats) {
|
||||
uint32_t delay_us = (uint32_t)delay * 500;
|
||||
if(delay_us > 0) {
|
||||
uint32_t delay_ms_yield = delay_us / 1000;
|
||||
uint32_t delay_us_busy = delay_us % 1000;
|
||||
|
||||
/* Yield to FreeRTOS for the bulk of the delay to allow the GUI to animate! */
|
||||
if(delay_ms_yield > 0) {
|
||||
furi_delay_ms(delay_ms_yield);
|
||||
}
|
||||
|
||||
/* Busy loop only for the sub-millisecond remainder */
|
||||
if(delay_us_busy > 0) {
|
||||
delay_cycles(delay_us_busy * 64); /* 64 cycles per µs */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback yield to FreeRTOS every 10 repeats if the delay parameter was 0 or 1 */
|
||||
if(((uint32_t)delay * 500) < 1000 && (rep % 10) == 9) {
|
||||
furi_delay_ms(1);
|
||||
}
|
||||
}
|
||||
|
||||
FURI_LOG_I("TagTinker", "TX complete");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool tagtinker_ir_is_busy(void) {
|
||||
return false; /* Transmit is blocking in this version */
|
||||
}
|
||||
|
||||
void tagtinker_ir_stop(void) {
|
||||
ir_stop_requested = true;
|
||||
carrier_off();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* TagTinker IR transmitter - header
|
||||
*
|
||||
* Low-level IR transmitter for the ESL protocol used by this project.
|
||||
* Drives the Flipper Zero's built-in IR LEDs at 1.255 MHz carrier
|
||||
* by directly programming TIM1 registers, bypassing furi_hal_infrared.
|
||||
*
|
||||
* Uses DWT cycle counter for symbol timing (no timer conflicts).
|
||||
* PP4 mode (2-bit symbols) for this POC.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/**
|
||||
* Initialize the IR transmitter.
|
||||
* Claims TIM1 for carrier, configures GPIO for IR output.
|
||||
*/
|
||||
void tagtinker_ir_init(void);
|
||||
|
||||
/**
|
||||
* Deinitialize the IR transmitter.
|
||||
* Releases TIM1, restores GPIO state.
|
||||
*/
|
||||
void tagtinker_ir_deinit(void);
|
||||
|
||||
/**
|
||||
* Transmit a frame using PP4 modulation.
|
||||
* Blocking call — returns when all repeats complete or cancelled.
|
||||
*
|
||||
* @param data frame bytes
|
||||
* @param len byte count
|
||||
* @param repeats times to repeat (0 = send once)
|
||||
* @param delay inter-repeat delay (×500µs, 10 = 5ms like ESL Blaster)
|
||||
* @return true if completed, false if cancelled/error
|
||||
*/
|
||||
bool tagtinker_ir_transmit(const uint8_t* data, size_t len, uint16_t repeats, uint8_t delay);
|
||||
|
||||
/**
|
||||
* Check if transmitting.
|
||||
*/
|
||||
bool tagtinker_ir_is_busy(void);
|
||||
|
||||
/**
|
||||
* Stop any ongoing transmission.
|
||||
*/
|
||||
void tagtinker_ir_stop(void);
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* TagTinker - ESL protocol helpers (implementation)
|
||||
*
|
||||
* Ported from furrtek/TagTinker tools_python/pr.py and img2dm.py
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "tagtinker_proto.h"
|
||||
#include "../tagtinker_app.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ── Helpers ────────────────────────────────────────────────── */
|
||||
|
||||
static void append_word(uint8_t* buf, size_t* pos, uint16_t value) {
|
||||
buf[(*pos)++] = (value >> 8) & 0xFF;
|
||||
buf[(*pos)++] = value & 0xFF;
|
||||
}
|
||||
|
||||
static size_t terminate(uint8_t* buf, size_t len) {
|
||||
uint16_t crc = tagtinker_crc16(buf, len);
|
||||
buf[len] = crc & 0xFF;
|
||||
buf[len + 1] = (crc >> 8) & 0xFF;
|
||||
return len + 2;
|
||||
}
|
||||
|
||||
static size_t raw_frame(uint8_t* buf, uint8_t proto,
|
||||
const uint8_t plid[4], uint8_t cmd) {
|
||||
buf[0] = proto;
|
||||
buf[1] = plid[3]; buf[2] = plid[2];
|
||||
buf[3] = plid[1]; buf[4] = plid[0];
|
||||
buf[5] = cmd;
|
||||
return 6;
|
||||
}
|
||||
|
||||
static size_t mcu_frame(uint8_t* buf, const uint8_t plid[4], uint8_t cmd) {
|
||||
size_t p = raw_frame(buf, TAGTINKER_PROTO_DM, plid, 0x34);
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = cmd;
|
||||
return p;
|
||||
}
|
||||
|
||||
/* ── CRC-16 (poly 0x8408, init 0x8408) ─────────────────────── */
|
||||
|
||||
uint16_t tagtinker_crc16(const uint8_t* data, size_t len) {
|
||||
uint16_t crc = 0x8408;
|
||||
for(size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for(int b = 0; b < 8; b++)
|
||||
crc = (crc & 1) ? (crc >> 1) ^ 0x8408 : crc >> 1;
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
/* ── Barcode → PLID ─────────────────────────────────────────── */
|
||||
|
||||
bool tagtinker_barcode_to_plid(const char* barcode, uint8_t plid[4]) {
|
||||
if(!barcode || strlen(barcode) != 17) return false;
|
||||
for(int i = 2; i < 12; i++)
|
||||
if(barcode[i] < '0' || barcode[i] > '9') return false;
|
||||
|
||||
uint32_t lo = 0, hi = 0;
|
||||
for(int i = 2; i < 7; i++) lo = lo * 10 + (barcode[i] - '0');
|
||||
for(int i = 7; i < 12; i++) hi = hi * 10 + (barcode[i] - '0');
|
||||
|
||||
uint32_t id = lo + (hi << 16);
|
||||
plid[0] = (id >> 8) & 0xFF;
|
||||
plid[1] = id & 0xFF;
|
||||
plid[2] = (id >> 24) & 0xFF;
|
||||
plid[3] = (id >> 16) & 0xFF;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── Frame builders ─────────────────────────────────────────── */
|
||||
|
||||
size_t tagtinker_build_broadcast_page_frame(
|
||||
uint8_t* buf, uint8_t page, bool forever, uint16_t duration) {
|
||||
|
||||
const uint8_t plid[4] = {0};
|
||||
size_t p = raw_frame(buf, TAGTINKER_PROTO_DM, plid, 0x06);
|
||||
buf[p++] = ((page & 7) << 3) | 0x01 | (forever ? 0x80 : 0x00);
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = forever ? 0x00 : ((duration >> 8) & 0xFF);
|
||||
buf[p++] = forever ? 0x00 : (duration & 0xFF);
|
||||
return terminate(buf, p);
|
||||
}
|
||||
|
||||
size_t tagtinker_build_broadcast_debug_frame(uint8_t* buf) {
|
||||
const uint8_t plid[4] = {0};
|
||||
size_t p = raw_frame(buf, TAGTINKER_PROTO_DM, plid, 0x06);
|
||||
buf[p++] = 0xF1;
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = 0x0A;
|
||||
return terminate(buf, p);
|
||||
}
|
||||
|
||||
size_t tagtinker_make_addressed_frame(
|
||||
uint8_t* buf, const uint8_t plid[4],
|
||||
const uint8_t* payload, size_t payload_len) {
|
||||
|
||||
size_t p = raw_frame(buf, TAGTINKER_PROTO_DM, plid, payload[0]);
|
||||
memcpy(&buf[p], payload + 1, payload_len - 1);
|
||||
p += payload_len - 1;
|
||||
return terminate(buf, p);
|
||||
}
|
||||
|
||||
size_t tagtinker_make_ping_frame(uint8_t* buf, const uint8_t plid[4]) {
|
||||
size_t p = raw_frame(buf, TAGTINKER_PROTO_DM, plid, 0x17);
|
||||
buf[p++] = 0x01;
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = 0x00;
|
||||
buf[p++] = 0x00;
|
||||
for(int i = 0; i < 22; i++) buf[p++] = 0x01;
|
||||
return terminate(buf, p);
|
||||
}
|
||||
|
||||
size_t tagtinker_make_refresh_frame(uint8_t* buf, const uint8_t plid[4]) {
|
||||
size_t p = mcu_frame(buf, plid, 0x01);
|
||||
for(int i = 0; i < 22; i++) buf[p++] = 0x00;
|
||||
return terminate(buf, p);
|
||||
}
|
||||
|
||||
size_t tagtinker_make_mcu_frame(
|
||||
uint8_t* buf, const uint8_t plid[4], uint8_t cmd) {
|
||||
return mcu_frame(buf, plid, cmd);
|
||||
}
|
||||
|
||||
static void record_run(uint8_t* out, size_t* pos, size_t cap, uint32_t run_count) {
|
||||
uint8_t bits[32];
|
||||
int n = 0;
|
||||
uint32_t v = run_count;
|
||||
while(v) { bits[n++] = v & 1; 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;
|
||||
}
|
||||
/* Prefix zeros (n-1 of them, skip leading 1) */
|
||||
for(int i = 1; i < n; i++)
|
||||
if(*pos < cap) out[(*pos)++] = 0;
|
||||
/* The bits themselves */
|
||||
for(int i = 0; i < n; i++)
|
||||
if(*pos < cap) out[(*pos)++] = bits[i];
|
||||
}
|
||||
|
||||
size_t tagtinker_rle_compress(
|
||||
const uint8_t* pixels, size_t count,
|
||||
uint8_t* out, size_t out_cap, uint8_t* comp_type) {
|
||||
|
||||
if(count == 0) { *comp_type = 0; return 0; }
|
||||
|
||||
size_t pos = 0;
|
||||
if(pos < out_cap) out[pos++] = pixels[0];
|
||||
|
||||
uint8_t run_pixel = pixels[0];
|
||||
uint32_t run_count = 1;
|
||||
|
||||
for(size_t i = 1; i < count; i++) {
|
||||
if(pixels[i] == run_pixel) {
|
||||
run_count++;
|
||||
} else {
|
||||
record_run(out, &pos, out_cap, run_count);
|
||||
run_pixel = pixels[i];
|
||||
run_count = 1;
|
||||
}
|
||||
}
|
||||
if(run_count > 1) record_run(out, &pos, out_cap, run_count);
|
||||
|
||||
if(pos < count) {
|
||||
*comp_type = 2; /* RLE */
|
||||
return pos;
|
||||
}
|
||||
/* Compression didn't help — use raw */
|
||||
memcpy(out, pixels, count < out_cap ? count : out_cap);
|
||||
*comp_type = 0;
|
||||
return count < out_cap ? count : out_cap;
|
||||
}
|
||||
|
||||
/* ── Image upload sequence builder ──────────────────────────── */
|
||||
|
||||
#define DATA_BYTES_PER_FRAME 20
|
||||
#define DATA_BITS_PER_FRAME (DATA_BYTES_PER_FRAME * 8)
|
||||
|
||||
void tagtinker_build_image_sequence(
|
||||
TagTinkerApp* app,
|
||||
const uint8_t plid[4],
|
||||
const uint8_t* pixels,
|
||||
uint16_t width, uint16_t height,
|
||||
uint8_t page,
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
/* Total: ping + params + N data + refresh */
|
||||
size_t total = 2 + frame_count + 1;
|
||||
|
||||
app->frame_seq_count = total;
|
||||
app->frame_sequence = malloc(sizeof(uint8_t*) * total);
|
||||
app->frame_lengths = malloc(sizeof(size_t) * total);
|
||||
app->frame_repeats = malloc(sizeof(uint16_t) * total);
|
||||
|
||||
if(!app->frame_sequence || !app->frame_lengths || !app->frame_repeats) {
|
||||
free(data_bytes);
|
||||
app->frame_seq_count = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
size_t idx = 0;
|
||||
|
||||
/* 1. Ping */
|
||||
app->frame_sequence[idx] = malloc(TAGTINKER_MAX_FRAME_SIZE);
|
||||
app->frame_lengths[idx] = tagtinker_make_ping_frame(app->frame_sequence[idx], plid);
|
||||
app->frame_repeats[idx] = wake_repeats;
|
||||
idx++;
|
||||
|
||||
/* 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_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_repeats[idx] = 3; /* 3 repeats per data frame for reliability */
|
||||
idx++;
|
||||
}
|
||||
|
||||
/* N+3. Refresh */
|
||||
app->frame_sequence[idx] = malloc(TAGTINKER_MAX_FRAME_SIZE);
|
||||
app->frame_lengths[idx] = tagtinker_make_refresh_frame(app->frame_sequence[idx], plid);
|
||||
app->frame_repeats[idx] = 1;
|
||||
|
||||
/* Copy first data frame for display in TX scene */
|
||||
if(app->frame_seq_count > 1) {
|
||||
memcpy(app->frame_buf, app->frame_sequence[1],
|
||||
app->frame_lengths[1] < TAGTINKER_MAX_FRAME_SIZE
|
||||
? app->frame_lengths[1] : TAGTINKER_MAX_FRAME_SIZE);
|
||||
app->frame_len = app->frame_lengths[1];
|
||||
}
|
||||
|
||||
free(data_bytes);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* TagTinker - ESL protocol helpers
|
||||
*
|
||||
* Frame construction, CRC, and encoding for the infrared
|
||||
* ESL protocol used by this project. Ported from furrtek/TagTinker.
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define TAGTINKER_PROTO_DM 0x85 /* Dot-matrix / graphic ESLs */
|
||||
#define TAGTINKER_PROTO_SEG 0x84 /* 7-segment ESLs */
|
||||
#define TAGTINKER_MAX_FRAME_SIZE 96
|
||||
|
||||
/* Forward declaration for TagTinkerApp (avoids circular include) */
|
||||
typedef struct TagTinkerApp TagTinkerApp;
|
||||
|
||||
/* ── CRC ────────────────────────────────────────────────────── */
|
||||
|
||||
uint16_t tagtinker_crc16(const uint8_t* data, size_t len);
|
||||
|
||||
/* ── Barcode / PLID ─────────────────────────────────────────── */
|
||||
|
||||
bool tagtinker_barcode_to_plid(const char* barcode, uint8_t plid[4]);
|
||||
|
||||
/* ── Frame builders ─────────────────────────────────────────── */
|
||||
|
||||
/* Broadcast page-change (no barcode needed). */
|
||||
size_t tagtinker_build_broadcast_page_frame(
|
||||
uint8_t* buf, uint8_t page, bool forever, uint16_t duration);
|
||||
|
||||
/* Broadcast diagnostic page. */
|
||||
size_t tagtinker_build_broadcast_debug_frame(uint8_t* buf);
|
||||
|
||||
/* Addressed DM frame: wraps raw payload with protocol + PLID + CRC. */
|
||||
size_t tagtinker_make_addressed_frame(
|
||||
uint8_t* buf, const uint8_t plid[4],
|
||||
const uint8_t* payload, size_t payload_len);
|
||||
|
||||
/* Wake-up ping (must be sent before most addressed commands). */
|
||||
size_t tagtinker_make_ping_frame(uint8_t* buf, const uint8_t plid[4]);
|
||||
|
||||
/* Display refresh request. */
|
||||
size_t tagtinker_make_refresh_frame(uint8_t* buf, const uint8_t plid[4]);
|
||||
|
||||
/* MCU-level frame (used for image upload protocol). */
|
||||
size_t tagtinker_make_mcu_frame(
|
||||
uint8_t* buf, const uint8_t plid[4], uint8_t cmd);
|
||||
|
||||
/* ── Image upload helpers ───────────────────────────────────── */
|
||||
|
||||
/* RLE-compress a pixel array (0/1 values).
|
||||
* Returns compressed bitstream length. comp_type is set to 0 (raw) or 2 (RLE). */
|
||||
size_t tagtinker_rle_compress(
|
||||
const uint8_t* pixels, size_t count,
|
||||
uint8_t* out, size_t out_cap, uint8_t* comp_type);
|
||||
|
||||
/* Build a complete image-upload frame sequence and store it in app state.
|
||||
* Allocates memory that the transmit scene frees on exit. */
|
||||
void tagtinker_build_image_sequence(
|
||||
TagTinkerApp* app,
|
||||
const uint8_t plid[4],
|
||||
const uint8_t* pixels,
|
||||
uint16_t width, uint16_t height,
|
||||
uint8_t page,
|
||||
uint16_t pos_x, uint16_t pos_y,
|
||||
uint16_t wake_repeats);
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Scene handler table
|
||||
*/
|
||||
|
||||
#include "tagtinker_scene.h"
|
||||
|
||||
void(*const tagtinker_scene_on_enter_handlers[])(void*) = {
|
||||
tagtinker_scene_main_menu_on_enter,
|
||||
tagtinker_scene_broadcast_menu_on_enter,
|
||||
tagtinker_scene_broadcast_on_enter,
|
||||
tagtinker_scene_target_menu_on_enter,
|
||||
tagtinker_scene_target_actions_on_enter,
|
||||
tagtinker_scene_barcode_input_on_enter,
|
||||
tagtinker_scene_text_input_on_enter,
|
||||
tagtinker_scene_preset_list_on_enter,
|
||||
tagtinker_scene_size_picker_on_enter,
|
||||
tagtinker_scene_image_upload_on_enter,
|
||||
tagtinker_scene_transmit_on_enter,
|
||||
tagtinker_scene_about_on_enter,
|
||||
};
|
||||
|
||||
bool(*const tagtinker_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
|
||||
tagtinker_scene_main_menu_on_event,
|
||||
tagtinker_scene_broadcast_menu_on_event,
|
||||
tagtinker_scene_broadcast_on_event,
|
||||
tagtinker_scene_target_menu_on_event,
|
||||
tagtinker_scene_target_actions_on_event,
|
||||
tagtinker_scene_barcode_input_on_event,
|
||||
tagtinker_scene_text_input_on_event,
|
||||
tagtinker_scene_preset_list_on_event,
|
||||
tagtinker_scene_size_picker_on_event,
|
||||
tagtinker_scene_image_upload_on_event,
|
||||
tagtinker_scene_transmit_on_event,
|
||||
tagtinker_scene_about_on_event,
|
||||
};
|
||||
|
||||
void(*const tagtinker_scene_on_exit_handlers[])(void*) = {
|
||||
tagtinker_scene_main_menu_on_exit,
|
||||
tagtinker_scene_broadcast_menu_on_exit,
|
||||
tagtinker_scene_broadcast_on_exit,
|
||||
tagtinker_scene_target_menu_on_exit,
|
||||
tagtinker_scene_target_actions_on_exit,
|
||||
tagtinker_scene_barcode_input_on_exit,
|
||||
tagtinker_scene_text_input_on_exit,
|
||||
tagtinker_scene_preset_list_on_exit,
|
||||
tagtinker_scene_size_picker_on_exit,
|
||||
tagtinker_scene_image_upload_on_exit,
|
||||
tagtinker_scene_transmit_on_exit,
|
||||
tagtinker_scene_about_on_exit,
|
||||
};
|
||||
|
||||
const SceneManagerHandlers tagtinker_scene_handlers = {
|
||||
.on_enter_handlers = tagtinker_scene_on_enter_handlers,
|
||||
.on_event_handlers = tagtinker_scene_on_event_handlers,
|
||||
.on_exit_handlers = tagtinker_scene_on_exit_handlers,
|
||||
.scene_num = TagTinkerSceneCount,
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* TagTinker — Scene Definitions
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/scene_manager.h>
|
||||
|
||||
typedef enum {
|
||||
TagTinkerSceneMainMenu,
|
||||
TagTinkerSceneBroadcastMenu,
|
||||
TagTinkerSceneBroadcast,
|
||||
TagTinkerSceneTargetMenu,
|
||||
TagTinkerSceneTargetActions,
|
||||
TagTinkerSceneBarcodeInput,
|
||||
TagTinkerSceneTextInput,
|
||||
TagTinkerScenePresetList,
|
||||
TagTinkerSceneSizePicker,
|
||||
TagTinkerSceneImageUpload,
|
||||
TagTinkerSceneTransmit,
|
||||
TagTinkerSceneAbout,
|
||||
TagTinkerSceneCount,
|
||||
} TagTinkerScene;
|
||||
|
||||
/* Scene handler declarations */
|
||||
void tagtinker_scene_main_menu_on_enter(void* ctx);
|
||||
bool tagtinker_scene_main_menu_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_main_menu_on_exit(void* ctx);
|
||||
|
||||
void tagtinker_scene_broadcast_menu_on_enter(void* ctx);
|
||||
bool tagtinker_scene_broadcast_menu_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_broadcast_menu_on_exit(void* ctx);
|
||||
|
||||
void tagtinker_scene_broadcast_on_enter(void* ctx);
|
||||
bool tagtinker_scene_broadcast_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_broadcast_on_exit(void* ctx);
|
||||
|
||||
void tagtinker_scene_target_menu_on_enter(void* ctx);
|
||||
bool tagtinker_scene_target_menu_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_target_menu_on_exit(void* ctx);
|
||||
|
||||
void tagtinker_scene_target_actions_on_enter(void* ctx);
|
||||
bool tagtinker_scene_target_actions_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_target_actions_on_exit(void* ctx);
|
||||
|
||||
void tagtinker_scene_barcode_input_on_enter(void* ctx);
|
||||
bool tagtinker_scene_barcode_input_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_barcode_input_on_exit(void* ctx);
|
||||
|
||||
void tagtinker_scene_text_input_on_enter(void* ctx);
|
||||
bool tagtinker_scene_text_input_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_text_input_on_exit(void* ctx);
|
||||
|
||||
void tagtinker_scene_size_picker_on_enter(void* ctx);
|
||||
bool tagtinker_scene_size_picker_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_size_picker_on_exit(void* ctx);
|
||||
|
||||
void tagtinker_scene_preset_list_on_enter(void* ctx);
|
||||
bool tagtinker_scene_preset_list_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_preset_list_on_exit(void* ctx);
|
||||
|
||||
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_transmit_on_enter(void* ctx);
|
||||
bool tagtinker_scene_transmit_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_transmit_on_exit(void* ctx);
|
||||
|
||||
void tagtinker_scene_about_on_enter(void* ctx);
|
||||
bool tagtinker_scene_about_on_event(void* ctx, SceneManagerEvent event);
|
||||
void tagtinker_scene_about_on_exit(void* ctx);
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* About — credits (state=0) or Android teaser (state=1)
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
|
||||
typedef struct {
|
||||
uint32_t mode;
|
||||
uint32_t tick;
|
||||
} AboutViewModel;
|
||||
|
||||
static void about_draw_cb(Canvas* canvas, void* _model) {
|
||||
AboutViewModel* model = _model;
|
||||
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
if(model->mode == 1) {
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(canvas, 64, 32, AlignCenter, AlignCenter, "In Progress");
|
||||
} else {
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 64, 10, AlignCenter, AlignTop, TAGTINKER_DISPLAY_NAME " v" TAGTINKER_VERSION);
|
||||
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(canvas, 64, 24, AlignCenter, AlignTop, "Ported by I12BP8");
|
||||
canvas_draw_str_aligned(canvas, 64, 34, AlignCenter, AlignTop, "Research by furrtek");
|
||||
canvas_draw_str_aligned(canvas, 64, 44, AlignCenter, AlignTop, "Owned ESLs only");
|
||||
canvas_draw_str_aligned(canvas, 64, 54, AlignCenter, AlignTop, "No store use");
|
||||
}
|
||||
}
|
||||
|
||||
void tagtinker_scene_about_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
uint32_t mode = scene_manager_get_scene_state(app->scene_manager, TagTinkerSceneAbout);
|
||||
|
||||
if(!app->about_view_allocated) {
|
||||
view_allocate_model(app->about_view, ViewModelTypeLockFree, sizeof(AboutViewModel));
|
||||
view_set_context(app->about_view, app);
|
||||
view_set_draw_callback(app->about_view, about_draw_cb);
|
||||
app->about_view_allocated = true;
|
||||
}
|
||||
|
||||
AboutViewModel* model = view_get_model(app->about_view);
|
||||
model->mode = mode;
|
||||
model->tick = 0;
|
||||
view_commit_model(app->about_view, true);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewAbout);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_about_on_event(void* ctx, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = ctx;
|
||||
if(event.type == SceneManagerEventTypeTick) {
|
||||
AboutViewModel* model = view_get_model(app->about_view);
|
||||
model->tick++;
|
||||
view_commit_model(app->about_view, true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void tagtinker_scene_about_on_exit(void* ctx) {
|
||||
UNUSED(ctx);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Barcode Input — custom numlock digit selector
|
||||
* Auto-prefixes 'N', user enters 16 digits with arrow keys.
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
|
||||
static void numlock_done(void* ctx, const char* barcode) {
|
||||
TagTinkerApp* app = ctx;
|
||||
/* Copy result to app barcode buffer */
|
||||
strncpy(app->barcode, barcode, TAGTINKER_BC_LEN);
|
||||
app->barcode[TAGTINKER_BC_LEN] = '\0';
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void tagtinker_scene_barcode_input_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
|
||||
numlock_input_reset(app->numlock);
|
||||
numlock_input_set_callback(app->numlock, numlock_done, app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewNumlock);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_barcode_input_on_event(void* ctx, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = ctx;
|
||||
if(event.type != SceneManagerEventTypeCustom) return false;
|
||||
|
||||
app->barcode_valid = tagtinker_barcode_to_plid(app->barcode, app->plid);
|
||||
|
||||
if(!app->barcode_valid) {
|
||||
FURI_LOG_W(TAGTINKER_TAG, "Invalid barcode: %s", app->barcode);
|
||||
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",
|
||||
64, 40, AlignCenter, AlignCenter);
|
||||
popup_set_timeout(app->popup, 2000);
|
||||
popup_enable_timeout(app->popup);
|
||||
popup_set_callback(app->popup, NULL);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewPopup);
|
||||
return true;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAGTINKER_TAG, "Barcode: %s -> PLID %02X%02X%02X%02X",
|
||||
app->barcode, app->plid[3], app->plid[2], app->plid[1], app->plid[0]);
|
||||
|
||||
/* Auto-save target */
|
||||
bool exists = false;
|
||||
for(uint8_t i = 0; i < app->target_count; i++) {
|
||||
if(strcmp(app->targets[i].barcode, app->barcode) == 0) {
|
||||
exists = true;
|
||||
app->selected_target = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!exists && app->target_count < TAGTINKER_MAX_TARGETS) {
|
||||
TagTinkerTarget* t = &app->targets[app->target_count];
|
||||
memcpy(t->barcode, app->barcode, TAGTINKER_BC_LEN + 1);
|
||||
memcpy(t->plid, app->plid, 4);
|
||||
char suffix[7];
|
||||
memcpy(suffix, app->barcode + TAGTINKER_BC_LEN - 6, 6);
|
||||
suffix[6] = '\0';
|
||||
snprintf(t->name, TAGTINKER_TARGET_NAME_LEN, "Tag ...%s", suffix);
|
||||
app->selected_target = app->target_count;
|
||||
app->target_count++;
|
||||
}
|
||||
|
||||
uint32_t target_scene = scene_manager_get_scene_state(
|
||||
app->scene_manager, TagTinkerSceneBarcodeInput);
|
||||
scene_manager_next_scene(app->scene_manager, target_scene);
|
||||
return true;
|
||||
}
|
||||
|
||||
void tagtinker_scene_barcode_input_on_exit(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
numlock_input_reset(app->numlock);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Broadcast page scene
|
||||
*
|
||||
* Sends either a page-select command or the diagnostic page command
|
||||
* to all DM ESLs in range.
|
||||
* No barcode needed - uses PLID 0 (broadcast).
|
||||
* Settings: page (0-7), duration (2-120s), forever toggle.
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
|
||||
enum {
|
||||
BroadcastItemPage,
|
||||
BroadcastItemDuration,
|
||||
BroadcastItemForever,
|
||||
BroadcastItemRepeats,
|
||||
BroadcastItemSpam,
|
||||
};
|
||||
|
||||
static const char* page_labels[] = {"0", "1", "2", "3", "4", "5", "6", "7"};
|
||||
static const char* forever_labels[] = {"Off", "On"};
|
||||
static const char* repeat_labels[] = {"50", "100", "200", "400", "800"};
|
||||
static const uint16_t repeat_values[] = {50, 100, 200, 400, 800};
|
||||
#define REPEAT_COUNT 5
|
||||
|
||||
static void page_changed(VariableItem* item) {
|
||||
TagTinkerApp* app = variable_item_get_context(item);
|
||||
uint8_t idx = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, page_labels[idx]);
|
||||
app->page = idx;
|
||||
}
|
||||
|
||||
static void duration_changed(VariableItem* item) {
|
||||
TagTinkerApp* app = variable_item_get_context(item);
|
||||
uint8_t idx = variable_item_get_current_value_index(item);
|
||||
uint16_t durations[] = {2, 5, 10, 15, 30, 60, 120};
|
||||
const char* dur_labels[] = {"2s", "5s", "10s", "15s", "30s", "60s", "120s"};
|
||||
variable_item_set_current_value_text(item, dur_labels[idx]);
|
||||
app->duration = durations[idx];
|
||||
}
|
||||
|
||||
static void forever_changed(VariableItem* item) {
|
||||
TagTinkerApp* app = variable_item_get_context(item);
|
||||
uint8_t idx = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, forever_labels[idx]);
|
||||
app->forever = (idx == 1);
|
||||
}
|
||||
|
||||
static void spam_changed(VariableItem* item) {
|
||||
TagTinkerApp* app = variable_item_get_context(item);
|
||||
uint8_t idx = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, forever_labels[idx]);
|
||||
app->tx_spam = (idx == 1);
|
||||
}
|
||||
|
||||
static void repeats_changed(VariableItem* item) {
|
||||
TagTinkerApp* app = variable_item_get_context(item);
|
||||
uint8_t idx = variable_item_get_current_value_index(item);
|
||||
variable_item_set_current_value_text(item, repeat_labels[idx]);
|
||||
app->repeats = repeat_values[idx];
|
||||
}
|
||||
|
||||
static void broadcast_enter_callback(void* context, uint32_t index) {
|
||||
UNUSED(index);
|
||||
TagTinkerApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void tagtinker_scene_broadcast_on_enter(void* context) {
|
||||
TagTinkerApp* app = context;
|
||||
VariableItemList* vil = app->var_item_list;
|
||||
|
||||
variable_item_list_reset(vil);
|
||||
|
||||
VariableItem* item;
|
||||
|
||||
/* Build list conditionally */
|
||||
if(app->broadcast_type == TagTinkerBroadcastFlipPage) {
|
||||
/* Page selector 0-7 */
|
||||
item = variable_item_list_add(vil, "Page", 8, page_changed, app);
|
||||
variable_item_set_current_value_index(item, app->page);
|
||||
variable_item_set_current_value_text(item, page_labels[app->page]);
|
||||
|
||||
/* Duration */
|
||||
item = variable_item_list_add(vil, "Duration", 7, duration_changed, app);
|
||||
/* Find closest index */
|
||||
uint16_t durations[] = {2, 5, 10, 15, 30, 60, 120};
|
||||
uint8_t dur_idx = 3; /* default 15s */
|
||||
for(uint8_t i = 0; i < 7; i++) {
|
||||
if(durations[i] == app->duration) { dur_idx = i; break; }
|
||||
}
|
||||
const char* dur_labels[] = {"2s", "5s", "10s", "15s", "30s", "60s", "120s"};
|
||||
variable_item_set_current_value_index(item, dur_idx);
|
||||
variable_item_set_current_value_text(item, dur_labels[dur_idx]);
|
||||
|
||||
/* Forever toggle */
|
||||
item = variable_item_list_add(vil, "Forever", 2, forever_changed, app);
|
||||
variable_item_set_current_value_index(item, app->forever ? 1 : 0);
|
||||
variable_item_set_current_value_text(item, forever_labels[app->forever ? 1 : 0]);
|
||||
}
|
||||
|
||||
/* Repeats (for both) */
|
||||
item = variable_item_list_add(vil, "Repeats", REPEAT_COUNT, repeats_changed, app);
|
||||
uint8_t rep_idx = 2; /* default 200 */
|
||||
for(uint8_t i = 0; i < REPEAT_COUNT; i++) {
|
||||
if(repeat_values[i] == app->repeats) { rep_idx = i; break; }
|
||||
}
|
||||
variable_item_set_current_value_index(item, rep_idx);
|
||||
variable_item_set_current_value_text(item, repeat_labels[rep_idx]);
|
||||
|
||||
/* Continuous repeat toggle */
|
||||
item = variable_item_list_add(vil, "Repeat", 2, spam_changed, app);
|
||||
variable_item_set_current_value_index(item, app->tx_spam ? 1 : 0);
|
||||
variable_item_set_current_value_text(item, forever_labels[app->tx_spam ? 1 : 0]);
|
||||
|
||||
/* OK press on any item → trigger transmit */
|
||||
variable_item_list_set_enter_callback(vil, broadcast_enter_callback, app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewVarItemList);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_broadcast_on_event(void* context, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(app->broadcast_type == TagTinkerBroadcastFlipPage) {
|
||||
/* Build page broadcast frame and transmit */
|
||||
app->frame_len = tagtinker_build_broadcast_page_frame(
|
||||
app->frame_buf, app->page, app->forever, app->duration);
|
||||
} else {
|
||||
/* Build debug broadcast frame */
|
||||
app->frame_len = tagtinker_build_broadcast_debug_frame(app->frame_buf);
|
||||
}
|
||||
|
||||
/* Set up single-frame TX */
|
||||
app->frame_seq_count = 0; /* Use single frame mode */
|
||||
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit);
|
||||
consumed = true;
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void tagtinker_scene_broadcast_on_exit(void* context) {
|
||||
TagTinkerApp* app = context;
|
||||
variable_item_list_reset(app->var_item_list);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Broadcast menu - page select / diagnostic page
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
|
||||
static void broadcast_menu_cb(void* ctx, uint32_t index) {
|
||||
TagTinkerApp* app = ctx;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void tagtinker_scene_broadcast_menu_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
|
||||
submenu_reset(app->submenu);
|
||||
submenu_set_header(app->submenu, "Broadcast Tag");
|
||||
|
||||
submenu_add_item(app->submenu, "Change Page", TagTinkerBroadcastFlipPage, broadcast_menu_cb, app);
|
||||
submenu_add_item(app->submenu, "Show Debug Page", TagTinkerBroadcastDebugScreen, broadcast_menu_cb, app);
|
||||
|
||||
submenu_set_selected_item(
|
||||
app->submenu,
|
||||
scene_manager_get_scene_state(app->scene_manager, TagTinkerSceneBroadcastMenu));
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewSubmenu);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_broadcast_menu_on_event(void* ctx, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = ctx;
|
||||
if(event.type != SceneManagerEventTypeCustom) return false;
|
||||
|
||||
scene_manager_set_scene_state(app->scene_manager, TagTinkerSceneBroadcastMenu, event.event);
|
||||
|
||||
switch(event.event) {
|
||||
case TagTinkerBroadcastFlipPage:
|
||||
app->broadcast_type = TagTinkerBroadcastFlipPage;
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneBroadcast);
|
||||
return true;
|
||||
case TagTinkerBroadcastDebugScreen:
|
||||
app->broadcast_type = TagTinkerBroadcastDebugScreen;
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneBroadcast);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void tagtinker_scene_broadcast_menu_on_exit(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "../tagtinker_app.h"
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <storage/storage.h>
|
||||
#include <gui/canvas.h>
|
||||
|
||||
static void show_error_dialog(TagTinkerApp* app, const char* text) {
|
||||
DialogMessage* message = dialog_message_alloc();
|
||||
dialog_message_set_header(message, "Load Error", 64, 0, AlignCenter, AlignTop);
|
||||
dialog_message_set_text(message, text, 64, 26, AlignCenter, AlignCenter);
|
||||
dialog_message_set_buttons(message, "OK", NULL, NULL);
|
||||
dialog_message_show(app->dialogs, message);
|
||||
dialog_message_free(message);
|
||||
}
|
||||
|
||||
void tagtinker_scene_image_upload_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
TagTinkerTarget* target = &app->targets[app->selected_target];
|
||||
|
||||
FuriString* file_path = furi_string_alloc();
|
||||
furi_string_set(file_path, "/ext/apps_data/tagtinker");
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
storage_simply_mkdir(storage, furi_string_get_cstr(file_path));
|
||||
|
||||
DialogsFileBrowserOptions browser_options;
|
||||
memset(&browser_options, 0, sizeof(browser_options));
|
||||
dialog_file_browser_set_basic_options(&browser_options, ".bmp", NULL);
|
||||
browser_options.hide_dot_files = true;
|
||||
|
||||
if(dialog_file_browser_show(app->dialogs, file_path, file_path, &browser_options)) {
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(storage_file_open(file, furi_string_get_cstr(file_path), FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
uint8_t header[54];
|
||||
if(storage_file_read(file, header, sizeof(header)) == sizeof(header)) {
|
||||
if(header[0] == 'B' && header[1] == 'M') {
|
||||
uint32_t data_offset = header[10] | (header[11] << 8) | (header[12] << 16) | (header[13] << 24);
|
||||
int32_t bmp_w = header[18] | (header[19] << 8) | (header[20] << 16) | (header[21] << 24);
|
||||
int32_t bmp_h = header[22] | (header[23] << 8) | (header[24] << 16) | (header[25] << 24);
|
||||
uint16_t bpp = header[28] | (header[29] << 8);
|
||||
|
||||
bool top_down = false;
|
||||
if(bmp_h < 0) {
|
||||
top_down = true;
|
||||
bmp_h = -bmp_h;
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
show_error_dialog(app, "Must be 1-bit BMP");
|
||||
}
|
||||
} else {
|
||||
show_error_dialog(app, "Invalid BMP magic");
|
||||
}
|
||||
} else {
|
||||
show_error_dialog(app, "File too small");
|
||||
}
|
||||
storage_file_close(file);
|
||||
} else {
|
||||
show_error_dialog(app, "Could not open file");
|
||||
}
|
||||
storage_file_free(file);
|
||||
} else {
|
||||
/* User cancelled browser */
|
||||
scene_manager_previous_scene(app->scene_manager);
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
furi_string_free(file_path);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_image_upload_on_event(void* ctx, SceneManagerEvent event) {
|
||||
UNUSED(ctx);
|
||||
UNUSED(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void tagtinker_scene_image_upload_on_exit(void* ctx) {
|
||||
UNUSED(ctx);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Main Menu
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
|
||||
enum {
|
||||
MainMenuBroadcast,
|
||||
MainMenuTargetESL,
|
||||
MainMenuAndroid,
|
||||
MainMenuAbout,
|
||||
};
|
||||
|
||||
static void main_menu_cb(void* ctx, uint32_t index) {
|
||||
TagTinkerApp* app = ctx;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void tagtinker_scene_main_menu_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
|
||||
submenu_reset(app->submenu);
|
||||
submenu_set_header(app->submenu, TAGTINKER_DISPLAY_NAME " v" TAGTINKER_VERSION);
|
||||
|
||||
submenu_add_item(app->submenu, "Broadcast Tag", MainMenuBroadcast, main_menu_cb, app);
|
||||
submenu_add_item(app->submenu, "Target Tag", MainMenuTargetESL, main_menu_cb, app);
|
||||
submenu_add_item(app->submenu, "Android App", MainMenuAndroid, main_menu_cb, app);
|
||||
submenu_add_item(app->submenu, "About", MainMenuAbout, main_menu_cb, app);
|
||||
|
||||
submenu_set_selected_item(
|
||||
app->submenu,
|
||||
scene_manager_get_scene_state(app->scene_manager, TagTinkerSceneMainMenu));
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewSubmenu);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_main_menu_on_event(void* ctx, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = ctx;
|
||||
if(event.type != SceneManagerEventTypeCustom) return false;
|
||||
|
||||
scene_manager_set_scene_state(app->scene_manager, TagTinkerSceneMainMenu, event.event);
|
||||
|
||||
switch(event.event) {
|
||||
case MainMenuBroadcast:
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneBroadcastMenu);
|
||||
return true;
|
||||
case MainMenuTargetESL:
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTargetMenu);
|
||||
return true;
|
||||
case MainMenuAndroid:
|
||||
/* state=1 tells About scene to show Android teaser */
|
||||
scene_manager_set_scene_state(app->scene_manager, TagTinkerSceneAbout, 1);
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneAbout);
|
||||
return true;
|
||||
case MainMenuAbout:
|
||||
scene_manager_set_scene_state(app->scene_manager, TagTinkerSceneAbout, 0);
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneAbout);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void tagtinker_scene_main_menu_on_exit(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Preset List — first screen after "Push Text".
|
||||
*
|
||||
* Shows saved presets (click = instant transmit with saved text + settings).
|
||||
* "[+] Add New Preset" goes to text input → size picker → save.
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
#include "../views/tagtinker_font.h"
|
||||
#include "../protocol/tagtinker_proto.h"
|
||||
|
||||
#define EVT_ADD_NEW 200
|
||||
#define EVT_PRESET 0
|
||||
|
||||
/* Load presets from SD card */
|
||||
static void presets_load(TagTinkerApp* app) {
|
||||
app->preset_count = 0;
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
File* file = storage_file_alloc(storage);
|
||||
|
||||
if(storage_file_open(file, APP_DATA_PATH("presets.txt"),
|
||||
FSAM_READ, FSOM_OPEN_EXISTING)) {
|
||||
char buf[512];
|
||||
uint16_t read = storage_file_read(file, buf, sizeof(buf) - 1);
|
||||
buf[read] = '\0';
|
||||
storage_file_close(file);
|
||||
|
||||
char* line = buf;
|
||||
while(line && *line && app->preset_count < TAGTINKER_MAX_PRESETS) {
|
||||
char* nl = strchr(line, '\n');
|
||||
if(nl) *nl = '\0';
|
||||
|
||||
unsigned w, h, pg, inv, clr;
|
||||
if(sscanf(line, "%u|%u|%u|%u|%u|", &w, &h, &pg, &inv, &clr) == 5) {
|
||||
char* p = line;
|
||||
int pipes = 0;
|
||||
while(*p && pipes < 5) { if(*p == '|') pipes++; p++; }
|
||||
|
||||
uint8_t idx = app->preset_count++;
|
||||
app->presets[idx].width = (uint16_t)w;
|
||||
app->presets[idx].height = (uint16_t)h;
|
||||
app->presets[idx].page = (uint8_t)pg;
|
||||
app->presets[idx].invert = (inv != 0);
|
||||
app->presets[idx].color_clear = (clr != 0);
|
||||
memset(app->presets[idx].text, 0, TAGTINKER_PRESET_TEXT_LEN);
|
||||
if(pipes == 5) {
|
||||
strncpy(app->presets[idx].text, p, TAGTINKER_PRESET_TEXT_LEN - 1);
|
||||
}
|
||||
}
|
||||
line = nl ? nl + 1 : NULL;
|
||||
}
|
||||
}
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
static void preset_list_cb(void* ctx, uint32_t index) {
|
||||
TagTinkerApp* app = ctx;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
/* Static label storage */
|
||||
static char preset_labels[TAGTINKER_MAX_PRESETS][48];
|
||||
|
||||
void tagtinker_scene_preset_list_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
|
||||
presets_load(app);
|
||||
|
||||
submenu_reset(app->submenu);
|
||||
submenu_set_header(app->submenu, "Text Presets");
|
||||
|
||||
/* Add New Preset option first */
|
||||
submenu_add_item(app->submenu, "[+] New Preset",
|
||||
EVT_ADD_NEW, preset_list_cb, app);
|
||||
|
||||
/* Saved presets */
|
||||
for(uint8_t i = 0; i < app->preset_count; i++) {
|
||||
snprintf(preset_labels[i], sizeof(preset_labels[i]),
|
||||
"%ux%u \"%s\"",
|
||||
app->presets[i].width,
|
||||
app->presets[i].height,
|
||||
app->presets[i].text);
|
||||
submenu_add_item(app->submenu, preset_labels[i],
|
||||
EVT_PRESET + i, preset_list_cb, app);
|
||||
}
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewSubmenu);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_preset_list_on_event(void* ctx, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = ctx;
|
||||
if(event.type != SceneManagerEventTypeCustom) return false;
|
||||
|
||||
if(event.event == EVT_ADD_NEW) {
|
||||
/* Clear text buffer and go to text input */
|
||||
memset(app->text_input_buf, 0, sizeof(app->text_input_buf));
|
||||
scene_manager_set_scene_state(app->scene_manager, TagTinkerSceneTextInput, 0);
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTextInput);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Preset selected — load text + settings and transmit */
|
||||
uint32_t idx = event.event - EVT_PRESET;
|
||||
if(idx < app->preset_count) {
|
||||
/* Load preset into app state */
|
||||
app->esl_width = app->presets[idx].width;
|
||||
app->esl_height = app->presets[idx].height;
|
||||
app->img_page = app->presets[idx].page;
|
||||
app->invert_text = app->presets[idx].invert;
|
||||
app->color_clear = app->presets[idx].color_clear;
|
||||
strncpy(app->text_input_buf, app->presets[idx].text,
|
||||
sizeof(app->text_input_buf) - 1);
|
||||
|
||||
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);
|
||||
app->tx_spam = false;
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void tagtinker_scene_preset_list_on_exit(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Size Picker — custom W/H (5px steps), mode, page, color, transmit
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
#include "../views/tagtinker_font.h"
|
||||
#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
|
||||
|
||||
#define W_COUNT ((MAX_W - MIN_W) / STEP_W + 1)
|
||||
#define H_COUNT ((MAX_H - MIN_H) / STEP_H + 1)
|
||||
|
||||
/* Setting indices */
|
||||
enum {
|
||||
SettingWidth,
|
||||
SettingHeight,
|
||||
SettingPage,
|
||||
SettingColor,
|
||||
SettingMode,
|
||||
SettingSave,
|
||||
SettingTransmit,
|
||||
};
|
||||
|
||||
/* ── Callbacks ── */
|
||||
|
||||
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;
|
||||
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "%u", app->esl_width);
|
||||
variable_item_set_current_value_text(item, buf);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "%u", app->esl_height);
|
||||
variable_item_set_current_value_text(item, buf);
|
||||
}
|
||||
|
||||
static void page_changed(VariableItem* item) {
|
||||
TagTinkerApp* app = variable_item_get_context(item);
|
||||
app->img_page = variable_item_get_current_value_index(item);
|
||||
|
||||
char buf[4];
|
||||
snprintf(buf, sizeof(buf), "%u", app->img_page);
|
||||
variable_item_set_current_value_text(item, buf);
|
||||
}
|
||||
|
||||
static void color_changed(VariableItem* item) {
|
||||
TagTinkerApp* app = variable_item_get_context(item);
|
||||
app->invert_text = (variable_item_get_current_value_index(item) == 1);
|
||||
|
||||
variable_item_set_current_value_text(
|
||||
item, app->invert_text ? "W on B" : "B on W");
|
||||
}
|
||||
|
||||
static void mode_changed(VariableItem* item) {
|
||||
TagTinkerApp* app = variable_item_get_context(item);
|
||||
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");
|
||||
}
|
||||
|
||||
static void save_presets_to_sd(TagTinkerApp* app) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
storage_common_mkdir(storage, APP_DATA_PATH(""));
|
||||
|
||||
File* file = storage_file_alloc(storage);
|
||||
if(storage_file_open(file, APP_DATA_PATH("presets.txt"),
|
||||
FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
||||
for(uint8_t i = 0; i < app->preset_count; i++) {
|
||||
char line[80];
|
||||
int len = snprintf(line, sizeof(line), "%u|%u|%u|%d|%d|%s\n",
|
||||
app->presets[i].width, app->presets[i].height,
|
||||
app->presets[i].page,
|
||||
app->presets[i].invert ? 1 : 0,
|
||||
app->presets[i].color_clear ? 1 : 0,
|
||||
app->presets[i].text);
|
||||
storage_file_write(file, line, (uint16_t)len);
|
||||
}
|
||||
storage_file_close(file);
|
||||
}
|
||||
storage_file_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
static void setting_cb(void* ctx, uint32_t index) {
|
||||
TagTinkerApp* app = ctx;
|
||||
|
||||
if(index == SettingSave) {
|
||||
/* Save current settings as a preset */
|
||||
if(app->preset_count < TAGTINKER_MAX_PRESETS) {
|
||||
uint8_t idx = app->preset_count++;
|
||||
app->presets[idx].width = app->esl_width;
|
||||
app->presets[idx].height = app->esl_height;
|
||||
app->presets[idx].page = app->img_page;
|
||||
app->presets[idx].invert = app->invert_text;
|
||||
app->presets[idx].color_clear = app->color_clear;
|
||||
strncpy(app->presets[idx].text, app->text_input_buf,
|
||||
TAGTINKER_PRESET_TEXT_LEN - 1);
|
||||
app->presets[idx].text[TAGTINKER_PRESET_TEXT_LEN - 1] = '\0';
|
||||
|
||||
save_presets_to_sd(app);
|
||||
notification_message(app->notifications, &sequence_success);
|
||||
FURI_LOG_I(TAGTINKER_TAG, "Preset saved: %ux%u", app->esl_width, app->esl_height);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(index != SettingTransmit) return;
|
||||
|
||||
FURI_LOG_I(TAGTINKER_TAG, "TX: %ux%u pg=%u inv=%d clr=%d",
|
||||
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);
|
||||
app->tx_spam = false;
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit);
|
||||
}
|
||||
|
||||
/* ── Scene handlers ── */
|
||||
|
||||
void tagtinker_scene_size_picker_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = 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;
|
||||
|
||||
/* Width */
|
||||
VariableItem* item_w = variable_item_list_add(
|
||||
app->var_item_list, "Width", W_COUNT, width_changed, app);
|
||||
variable_item_set_current_value_index(item_w, w_idx);
|
||||
{
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "%u", app->esl_width);
|
||||
variable_item_set_current_value_text(item_w, buf);
|
||||
}
|
||||
|
||||
/* Height */
|
||||
VariableItem* item_h = variable_item_list_add(
|
||||
app->var_item_list, "Height", H_COUNT, height_changed, app);
|
||||
variable_item_set_current_value_index(item_h, h_idx);
|
||||
{
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "%u", app->esl_height);
|
||||
variable_item_set_current_value_text(item_h, buf);
|
||||
}
|
||||
|
||||
/* Page */
|
||||
VariableItem* item_pg = variable_item_list_add(
|
||||
app->var_item_list, "Page", 8, page_changed, app);
|
||||
variable_item_set_current_value_index(item_pg, app->img_page);
|
||||
{
|
||||
char buf[4];
|
||||
snprintf(buf, sizeof(buf), "%u", app->img_page);
|
||||
variable_item_set_current_value_text(item_pg, buf);
|
||||
}
|
||||
|
||||
/* Color (text polarity) */
|
||||
VariableItem* item_col = variable_item_list_add(
|
||||
app->var_item_list, "Color", 2, color_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) */
|
||||
VariableItem* item_mode = variable_item_list_add(
|
||||
app->var_item_list, "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");
|
||||
|
||||
/* Save preset button */
|
||||
variable_item_list_add(app->var_item_list, "[*] Save Preset", 0, NULL, app);
|
||||
|
||||
/* Transmit button */
|
||||
variable_item_list_add(app->var_item_list, ">> Send to Tag <<", 0, NULL, app);
|
||||
|
||||
variable_item_list_set_enter_callback(app->var_item_list, setting_cb, app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewVarItemList);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_size_picker_on_event(void* ctx, SceneManagerEvent event) {
|
||||
UNUSED(ctx);
|
||||
UNUSED(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
void tagtinker_scene_size_picker_on_exit(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
variable_item_list_reset(app->var_item_list);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Target Actions — what to do with a selected ESL
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
|
||||
static void target_actions_cb(void* ctx, uint32_t index) {
|
||||
TagTinkerApp* app = ctx;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void tagtinker_scene_target_actions_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
|
||||
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, "LED Response Check", TagTinkerTargetPingFlash, target_actions_cb, app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewSubmenu);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_target_actions_on_event(void* ctx, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = ctx;
|
||||
if(event.type != SceneManagerEventTypeCustom) return false;
|
||||
|
||||
switch(event.event) {
|
||||
case TagTinkerTargetPushText:
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerScenePresetList);
|
||||
return true;
|
||||
case TagTinkerTargetPushImage:
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneImageUpload);
|
||||
return true;
|
||||
case TagTinkerTargetPingFlash:
|
||||
{
|
||||
TagTinkerTarget* target = &app->targets[app->selected_target];
|
||||
|
||||
/* The blink command actually requires two frames:
|
||||
* 1. A wake ping (lots of repeats)
|
||||
* 2. The green LED command payload (0x06 0xC9 0x00 0x00 0x00 0x00)
|
||||
*/
|
||||
app->frame_seq_count = 2;
|
||||
app->frame_sequence = malloc(sizeof(uint8_t*) * 2);
|
||||
app->frame_lengths = malloc(sizeof(size_t) * 2);
|
||||
app->frame_repeats = malloc(sizeof(uint16_t) * 2);
|
||||
|
||||
/* 1. Wake ping */
|
||||
app->frame_sequence[0] = malloc(TAGTINKER_MAX_FRAME_SIZE);
|
||||
app->frame_lengths[0] = tagtinker_make_ping_frame(app->frame_sequence[0], target->plid);
|
||||
app->frame_repeats[0] = 500;
|
||||
|
||||
/* 2. LED command */
|
||||
app->frame_sequence[1] = malloc(TAGTINKER_MAX_FRAME_SIZE);
|
||||
const uint8_t blink_payload[6] = {0x06, 0xC9, 0x00, 0x00, 0x00, 0x00};
|
||||
app->frame_lengths[1] = tagtinker_make_addressed_frame(
|
||||
app->frame_sequence[1], target->plid, blink_payload, 6);
|
||||
app->frame_repeats[1] = 100;
|
||||
|
||||
/* Put the first frame in the preview buffer */
|
||||
memcpy(app->frame_buf, app->frame_sequence[0], app->frame_lengths[0]);
|
||||
app->frame_len = app->frame_lengths[0];
|
||||
|
||||
app->tx_spam = false;
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTransmit);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void tagtinker_scene_target_actions_on_exit(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Saved tag menu - saved targets + add new
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
|
||||
enum {
|
||||
TargetMenuAddNew = 100,
|
||||
};
|
||||
|
||||
static void target_menu_cb(void* ctx, uint32_t index) {
|
||||
TagTinkerApp* app = ctx;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void tagtinker_scene_target_menu_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
|
||||
submenu_reset(app->submenu);
|
||||
submenu_set_header(app->submenu, "Target Tag");
|
||||
|
||||
/* Add new target */
|
||||
submenu_add_item(app->submenu, "+ Add Tag", TargetMenuAddNew, target_menu_cb, app);
|
||||
|
||||
/* List saved targets */
|
||||
for(uint8_t i = 0; i < app->target_count; i++) {
|
||||
/* Use index as event id (0..15) */
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
app->targets[i].name[0] ? app->targets[i].name : app->targets[i].barcode,
|
||||
i,
|
||||
target_menu_cb,
|
||||
app);
|
||||
}
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewSubmenu);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_target_menu_on_event(void* ctx, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = ctx;
|
||||
if(event.type != SceneManagerEventTypeCustom) return false;
|
||||
|
||||
if(event.event == TargetMenuAddNew) {
|
||||
/* Go to barcode input, then come back */
|
||||
scene_manager_set_scene_state(
|
||||
app->scene_manager, TagTinkerSceneBarcodeInput, TagTinkerSceneTargetActions);
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneBarcodeInput);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneTargetActions);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void tagtinker_scene_target_menu_on_exit(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* TagTinker — Text Input Scene
|
||||
*
|
||||
* Prompts user for a text string, then goes to Size Picker.
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
|
||||
static void text_input_done_cb(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 0);
|
||||
}
|
||||
|
||||
void tagtinker_scene_text_input_on_enter(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
bool clear = scene_manager_get_scene_state(app->scene_manager, TagTinkerSceneTextInput) == 0;
|
||||
|
||||
if(clear) {
|
||||
memset(app->text_input_buf, 0, sizeof(app->text_input_buf));
|
||||
scene_manager_set_scene_state(app->scene_manager, TagTinkerSceneTextInput, 1);
|
||||
}
|
||||
|
||||
text_input_reset(app->text_input);
|
||||
text_input_set_header_text(app->text_input, "Text to display:");
|
||||
text_input_set_result_callback(
|
||||
app->text_input,
|
||||
text_input_done_cb,
|
||||
app,
|
||||
app->text_input_buf,
|
||||
sizeof(app->text_input_buf),
|
||||
clear);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewTextInput);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_text_input_on_event(void* ctx, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = ctx;
|
||||
if(event.type != SceneManagerEventTypeCustom) return false;
|
||||
|
||||
if(strlen(app->text_input_buf) == 0) {
|
||||
scene_manager_search_and_switch_to_previous_scene(
|
||||
app->scene_manager, TagTinkerSceneTargetActions);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Configure settings (Add Preset flow) */
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneSizePicker);
|
||||
return true;
|
||||
}
|
||||
|
||||
void tagtinker_scene_text_input_on_exit(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
text_input_reset(app->text_input);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Transmit Scene
|
||||
* Displays cool animations while transmitting in a background thread.
|
||||
*/
|
||||
|
||||
#include "../tagtinker_app.h"
|
||||
#include <furi_hal.h>
|
||||
|
||||
typedef struct {
|
||||
TagTinkerApp* app;
|
||||
uint32_t tick;
|
||||
bool completed;
|
||||
bool ok;
|
||||
} TxViewModel;
|
||||
|
||||
static int32_t tx_thread_callback(void* context) {
|
||||
TagTinkerApp* app = context;
|
||||
bool ok = true;
|
||||
|
||||
tagtinker_ir_init();
|
||||
|
||||
do {
|
||||
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);
|
||||
ok = tagtinker_ir_transmit(
|
||||
app->frame_sequence[i], app->frame_lengths[i],
|
||||
app->frame_repeats[i], 10);
|
||||
if(!ok) break;
|
||||
}
|
||||
} else {
|
||||
ok = tagtinker_ir_transmit(app->frame_buf, app->frame_len, app->repeats, 10);
|
||||
}
|
||||
|
||||
if(app->tx_spam && app->tx_active) {
|
||||
furi_delay_ms(50);
|
||||
}
|
||||
} while(app->tx_spam && app->tx_active);
|
||||
|
||||
app->tx_active = false;
|
||||
/* Use event payload to securely pass result instead of querying running thread! */
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, 101 + (ok ? 0 : 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void transmit_draw_cb(Canvas* canvas, void* _model) {
|
||||
TxViewModel* model = _model;
|
||||
TagTinkerApp* app = model->app;
|
||||
uint32_t t = model->tick;
|
||||
|
||||
/* Left: Top-Down Detailed Flipper Zero Vector Model */
|
||||
int f_x = -15; // Protruding from left screen edge
|
||||
int f_y = 10;
|
||||
int f_w = 40;
|
||||
int f_h = 36;
|
||||
/* Main casing */
|
||||
canvas_draw_rframe(canvas, f_x, f_y, f_w, f_h, 3);
|
||||
/* Outer screen outline */
|
||||
canvas_draw_rframe(canvas, f_x + 10, f_y + 4, 20, 16, 2);
|
||||
/* Screen inner solid */
|
||||
canvas_draw_box(canvas, f_x + 12, f_y + 6, 16, 12);
|
||||
/* Flipper Screen highlight glow */
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_dot(canvas, f_x + 14, f_y + 8);
|
||||
canvas_draw_dot(canvas, f_x + 15, f_y + 8);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
/* D-Pad Matrix */
|
||||
canvas_draw_circle(canvas, f_x + 18, f_y + 27, 5); // Outer
|
||||
canvas_draw_circle(canvas, f_x + 18, f_y + 27, 2); // Inner button
|
||||
/* GPIO / Case Vents */
|
||||
for(int i=0; i<4; i++) {
|
||||
canvas_draw_dot(canvas, f_x + 32, f_y + 8 + i*3);
|
||||
canvas_draw_dot(canvas, f_x + 34, f_y + 8 + i*3);
|
||||
}
|
||||
/* IR Blaster window on the right tip */
|
||||
int ir_x = f_x + f_w; // 25
|
||||
int ir_y = f_y + 10;
|
||||
canvas_draw_box(canvas, ir_x, ir_y, 4, 16);
|
||||
|
||||
int center_y = f_y + 18; /* perfectly aligns with tag center */
|
||||
|
||||
if (app->tx_active) {
|
||||
/* Aggressive Pulsing Data-Stream Chevrons with bits */
|
||||
uint8_t wave_phase = (t * 2) % 15;
|
||||
for(int w=0; w<2; w++) {
|
||||
int r = wave_phase + w*18;
|
||||
if(r > 0 && r < 20) {
|
||||
int wave_x = ir_x + 4 + r;
|
||||
int wave_h = r/2 + 3;
|
||||
/* 2px thick chevron pointing right */
|
||||
canvas_draw_line(canvas, wave_x, center_y, wave_x - wave_h/2, center_y - wave_h);
|
||||
canvas_draw_line(canvas, wave_x, center_y, wave_x - wave_h/2, center_y + wave_h);
|
||||
canvas_draw_line(canvas, wave_x+1, center_y, wave_x+1 - wave_h/2, center_y - wave_h);
|
||||
canvas_draw_line(canvas, wave_x+1, center_y, wave_x+1 - wave_h/2, center_y + wave_h);
|
||||
/* Data stream particles */
|
||||
if((t + w) % 2 == 0) {
|
||||
canvas_draw_dot(canvas, wave_x + 3, center_y - wave_h/2);
|
||||
canvas_draw_dot(canvas, wave_x - 3, center_y + wave_h/2 + 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Right: Detailed ESL tag model */
|
||||
int tag_x = 52;
|
||||
int tag_y = 8;
|
||||
int tag_w = 75;
|
||||
int tag_h = 40;
|
||||
/* Outer casing bounding rect */
|
||||
canvas_draw_rframe(canvas, tag_x, tag_y, tag_w, tag_h, 2);
|
||||
/* Internal 3D bezel shadow */
|
||||
canvas_draw_rframe(canvas, tag_x+1, tag_y+1, tag_w-2, tag_h-2, 1);
|
||||
|
||||
/* True 32-bit encoded barcode representation on the rim */
|
||||
int bc_x = tag_x + 3;
|
||||
int bc_w = 8;
|
||||
int bc_y = tag_y + 4;
|
||||
uint8_t bar_pattern[] = {0b10110100, 0b11010010, 0b10011011, 0b01101010};
|
||||
for(int i=0; i<32; i++) {
|
||||
if( (bar_pattern[i/8] >> (7-(i%8))) & 1 ) {
|
||||
canvas_draw_line(canvas, bc_x, bc_y + i, bc_x + bc_w - 1, bc_y + i);
|
||||
}
|
||||
}
|
||||
|
||||
/* Screen display hardware border */
|
||||
int scr_x = tag_x + 14;
|
||||
int scr_y = tag_y + 3;
|
||||
int scr_w = tag_w - 17;
|
||||
int scr_h = tag_h - 6;
|
||||
canvas_draw_frame(canvas, scr_x - 1, scr_y - 1, scr_w + 2, scr_h + 2);
|
||||
|
||||
/* E-Paper Sweep Logic */
|
||||
int cycle = t % 40; /* 2 second loop at 20fps */
|
||||
|
||||
if (app->tx_active) {
|
||||
bool show_result = (cycle >= 20);
|
||||
|
||||
if(!show_result) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str(canvas, scr_x + 2, scr_y + 11, "LAB NOTE");
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str(canvas, scr_x + 5, scr_y + 26, "STUDY");
|
||||
} else {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, scr_x + scr_w / 2, scr_y + scr_h / 2, AlignCenter, AlignCenter, "FLIPPED ;)");
|
||||
}
|
||||
|
||||
/* Glitchy Matrix Wipe Transition */
|
||||
if(cycle >= 15 && cycle < 20) {
|
||||
int wipe_h = ((cycle - 15) * scr_h) / 5;
|
||||
canvas_draw_box(canvas, scr_x, scr_y, scr_w, wipe_h);
|
||||
/* Algorithmic leading edge burn/glitch */
|
||||
for(int xx = 0; xx < scr_w; xx += 2) {
|
||||
canvas_draw_dot(canvas, scr_x + xx, scr_y + wipe_h + ((xx+t)%4));
|
||||
}
|
||||
} else if (cycle >= 20 && cycle < 25) {
|
||||
int wipe_h = ((cycle - 20) * scr_h) / 5;
|
||||
canvas_draw_box(canvas, scr_x, scr_y + wipe_h, scr_w, scr_h - wipe_h);
|
||||
/* Algorithmic trailing edge burn/glitch */
|
||||
for(int xx = 0; xx < scr_w; xx += 2) {
|
||||
canvas_draw_dot(canvas, scr_x + xx, scr_y + wipe_h - 1 - ((xx+t)%4));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(
|
||||
canvas, scr_x + scr_w / 2, scr_y + scr_h / 2, AlignCenter, AlignCenter, "FLIPPED ;)");
|
||||
}
|
||||
|
||||
/* Submenu action hint decoupled from canvas models */
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
if(app->tx_spam) {
|
||||
canvas_draw_str_aligned(canvas, 64, 55, AlignCenter, AlignTop, "[<-] Stop Repeat");
|
||||
} else {
|
||||
canvas_draw_str_aligned(canvas, 64, 55, AlignCenter, AlignTop, app->tx_active ? "[<-] Cancel" : "[<-] Back");
|
||||
}
|
||||
}
|
||||
|
||||
void tagtinker_scene_transmit_on_enter(void* context) {
|
||||
TagTinkerApp* app = context;
|
||||
|
||||
if(!app->transmit_view_allocated) {
|
||||
view_allocate_model(app->transmit_view, ViewModelTypeLockFree, sizeof(TxViewModel));
|
||||
view_set_context(app->transmit_view, app);
|
||||
view_set_draw_callback(app->transmit_view, transmit_draw_cb);
|
||||
app->transmit_view_allocated = true;
|
||||
}
|
||||
|
||||
TxViewModel* model = view_get_model(app->transmit_view);
|
||||
model->app = app;
|
||||
model->tick = 0;
|
||||
model->completed = false;
|
||||
model->ok = true;
|
||||
view_commit_model(app->transmit_view, true);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, TagTinkerViewTransmit);
|
||||
|
||||
app->tx_active = true;
|
||||
furi_thread_set_callback(app->tx_thread, tx_thread_callback);
|
||||
furi_thread_start(app->tx_thread);
|
||||
}
|
||||
|
||||
bool tagtinker_scene_transmit_on_event(void* context, SceneManagerEvent event) {
|
||||
TagTinkerApp* app = context;
|
||||
|
||||
if(event.type == SceneManagerEventTypeBack) {
|
||||
if(app->tx_active) {
|
||||
app->tx_active = false;
|
||||
tagtinker_ir_stop();
|
||||
return true;
|
||||
} else {
|
||||
if(!scene_manager_search_and_switch_to_previous_scene(app->scene_manager, TagTinkerSceneTargetActions)) {
|
||||
if(!scene_manager_search_and_switch_to_previous_scene(app->scene_manager, TagTinkerSceneBroadcast)) {
|
||||
if(!scene_manager_search_and_switch_to_previous_scene(app->scene_manager, TagTinkerSceneBroadcastMenu)) {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, TagTinkerSceneMainMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else if(event.type == SceneManagerEventTypeTick) {
|
||||
TxViewModel* model = view_get_model(app->transmit_view);
|
||||
model->tick++;
|
||||
view_commit_model(app->transmit_view, true);
|
||||
return true;
|
||||
} else if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == 101 || event.event == 102) { /* Thread Done */
|
||||
app->tx_active = false;
|
||||
tagtinker_ir_deinit();
|
||||
TxViewModel* model = view_get_model(app->transmit_view);
|
||||
model->completed = true;
|
||||
model->ok = (event.event == 101);
|
||||
view_commit_model(app->transmit_view, true);
|
||||
notification_message(app->notifications, &sequence_success);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void tagtinker_scene_transmit_on_exit(void* context) {
|
||||
TagTinkerApp* app = context;
|
||||
|
||||
app->tx_active = false;
|
||||
tagtinker_ir_stop();
|
||||
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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 95 B |
+161
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* TagTinker - ESL Flipper Zero application
|
||||
*
|
||||
* Transmit infrared commands to supported ESL displays
|
||||
* using the built-in IR LED at 1.255 MHz carrier.
|
||||
*
|
||||
* App by I12BP8 - github.com/i12bp8
|
||||
* Research by furrtek - github.com/furrtek
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "tagtinker_app.h"
|
||||
|
||||
static bool navigation_cb(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
return scene_manager_handle_back_event(app->scene_manager);
|
||||
}
|
||||
|
||||
static void tick_cb(void* ctx) {
|
||||
TagTinkerApp* app = ctx;
|
||||
scene_manager_handle_tick_event(app->scene_manager);
|
||||
}
|
||||
|
||||
static bool custom_event_cb(void* ctx, uint32_t event) {
|
||||
TagTinkerApp* app = ctx;
|
||||
return scene_manager_handle_custom_event(app->scene_manager, event);
|
||||
}
|
||||
|
||||
extern const SceneManagerHandlers tagtinker_scene_handlers;
|
||||
|
||||
static TagTinkerApp* app_alloc(void) {
|
||||
TagTinkerApp* app = malloc(sizeof(TagTinkerApp));
|
||||
memset(app, 0, sizeof(TagTinkerApp));
|
||||
|
||||
/* Defaults */
|
||||
app->page = 0;
|
||||
app->duration = 15;
|
||||
app->repeats = 200;
|
||||
app->draw_width = 48;
|
||||
app->draw_height = 48;
|
||||
app->img_page = 1;
|
||||
app->esl_width = 200;
|
||||
app->esl_height = 80;
|
||||
app->color_clear = false;
|
||||
app->invert_text = false;
|
||||
strcpy(app->text_input_buf, "TagTinker");
|
||||
app->selected_target = -1;
|
||||
|
||||
/* Scene manager */
|
||||
app->scene_manager = scene_manager_alloc(&tagtinker_scene_handlers, app);
|
||||
|
||||
/* View dispatcher */
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||
view_dispatcher_set_navigation_event_callback(app->view_dispatcher, navigation_cb);
|
||||
view_dispatcher_set_tick_event_callback(app->view_dispatcher, tick_cb, 50);
|
||||
view_dispatcher_set_custom_event_callback(app->view_dispatcher, custom_event_cb);
|
||||
|
||||
/* GUI */
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||
|
||||
/* Notifications */
|
||||
app->notifications = furi_record_open(RECORD_NOTIFICATION);
|
||||
|
||||
/* Views */
|
||||
app->submenu = submenu_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, TagTinkerViewSubmenu, submenu_get_view(app->submenu));
|
||||
|
||||
app->var_item_list = variable_item_list_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, TagTinkerViewVarItemList,
|
||||
variable_item_list_get_view(app->var_item_list));
|
||||
|
||||
app->text_input = text_input_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, TagTinkerViewTextInput, text_input_get_view(app->text_input));
|
||||
|
||||
app->popup = popup_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, TagTinkerViewPopup, popup_get_view(app->popup));
|
||||
|
||||
app->widget = widget_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, TagTinkerViewWidget, widget_get_view(app->widget));
|
||||
|
||||
app->numlock = numlock_input_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, TagTinkerViewNumlock, numlock_input_get_view(app->numlock));
|
||||
|
||||
app->transmit_view = view_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, TagTinkerViewTransmit, app->transmit_view);
|
||||
|
||||
app->about_view = view_alloc();
|
||||
view_dispatcher_add_view(
|
||||
app->view_dispatcher, TagTinkerViewAbout, app->about_view);
|
||||
|
||||
app->dialogs = furi_record_open(RECORD_DIALOGS);
|
||||
|
||||
/* 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_priority(app->tx_thread, FuriThreadPriorityHighest);
|
||||
furi_thread_set_context(app->tx_thread, app);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewSubmenu);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewVarItemList);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewTextInput);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewPopup);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewWidget);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewNumlock);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewTransmit);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, TagTinkerViewAbout);
|
||||
|
||||
submenu_free(app->submenu);
|
||||
variable_item_list_free(app->var_item_list);
|
||||
text_input_free(app->text_input);
|
||||
popup_free(app->popup);
|
||||
widget_free(app->widget);
|
||||
numlock_input_free(app->numlock);
|
||||
|
||||
view_dispatcher_free(app->view_dispatcher);
|
||||
scene_manager_free(app->scene_manager);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
furi_record_close(RECORD_DIALOGS);
|
||||
|
||||
view_free(app->transmit_view);
|
||||
view_free(app->about_view);
|
||||
furi_thread_free(app->tx_thread);
|
||||
|
||||
free(app);
|
||||
}
|
||||
|
||||
int32_t tagtinker_app_main(void* p) {
|
||||
UNUSED(p);
|
||||
TagTinkerApp* app = app_alloc();
|
||||
scene_manager_next_scene(app->scene_manager, TagTinkerSceneMainMenu);
|
||||
view_dispatcher_run(app->view_dispatcher);
|
||||
app_free(app);
|
||||
return 0;
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* TagTinker — App State
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/view.h>
|
||||
#include <gui/scene_manager.h>
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/modules/text_input.h>
|
||||
#include <gui/modules/variable_item_list.h>
|
||||
#include <gui/modules/popup.h>
|
||||
#include <gui/modules/widget.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include <storage/storage.h>
|
||||
|
||||
#include "views/numlock_input.h"
|
||||
|
||||
#include "scenes/tagtinker_scene.h"
|
||||
#include "protocol/tagtinker_proto.h"
|
||||
#include "ir/tagtinker_ir.h"
|
||||
|
||||
#define TAGTINKER_TAG "TagTinker"
|
||||
#define TAGTINKER_DISPLAY_NAME "TagTinker"
|
||||
#define TAGTINKER_VERSION "1.1"
|
||||
#define TAGTINKER_BC_LEN 17
|
||||
#define TAGTINKER_HEX_LEN 64
|
||||
#define TAGTINKER_MAX_TARGETS 16
|
||||
#define TAGTINKER_TARGET_NAME_LEN 16
|
||||
#define TAGTINKER_MAX_PRESETS 6
|
||||
#define TAGTINKER_PRESET_TEXT_LEN 32
|
||||
|
||||
/* Views */
|
||||
typedef enum {
|
||||
TagTinkerViewSubmenu,
|
||||
TagTinkerViewVarItemList,
|
||||
TagTinkerViewTextInput,
|
||||
TagTinkerViewPopup,
|
||||
TagTinkerViewWidget,
|
||||
TagTinkerViewNumlock,
|
||||
TagTinkerViewTargetActions,
|
||||
TagTinkerViewTransmit,
|
||||
TagTinkerViewAbout,
|
||||
} TagTinkerView;
|
||||
|
||||
/* Saved ESL target */
|
||||
typedef struct {
|
||||
char name[TAGTINKER_TARGET_NAME_LEN + 1];
|
||||
char barcode[TAGTINKER_BC_LEN + 1];
|
||||
uint8_t plid[4];
|
||||
} TagTinkerTarget;
|
||||
|
||||
struct TagTinkerApp {
|
||||
/* GUI */
|
||||
Gui* gui;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
SceneManager* scene_manager;
|
||||
NotificationApp* notifications;
|
||||
DialogsApp* dialogs;
|
||||
|
||||
/* Views */
|
||||
Submenu* submenu;
|
||||
VariableItemList* var_item_list;
|
||||
TextInput* text_input;
|
||||
Popup* popup;
|
||||
Widget* widget;
|
||||
NumlockInput* numlock;
|
||||
View* target_actions_view;
|
||||
View* transmit_view;
|
||||
View* about_view;
|
||||
bool transmit_view_allocated;
|
||||
bool about_view_allocated;
|
||||
|
||||
/* TX state */
|
||||
bool tx_active;
|
||||
FuriThread* tx_thread;
|
||||
|
||||
/* Broadcast settings */
|
||||
uint8_t broadcast_type;
|
||||
uint8_t page;
|
||||
uint16_t duration;
|
||||
uint16_t repeats;
|
||||
bool forever;
|
||||
bool tx_spam;
|
||||
|
||||
/* Current target */
|
||||
char barcode[TAGTINKER_BC_LEN + 1];
|
||||
uint8_t plid[4];
|
||||
bool barcode_valid;
|
||||
int8_t selected_target; /* -1 = none */
|
||||
|
||||
/* Saved targets */
|
||||
TagTinkerTarget targets[TAGTINKER_MAX_TARGETS];
|
||||
uint8_t target_count;
|
||||
|
||||
/* Text to push */
|
||||
char text_input_buf[64];
|
||||
|
||||
/* ESL display size for current target */
|
||||
uint16_t esl_width;
|
||||
uint16_t esl_height;
|
||||
|
||||
/* Frame buffer */
|
||||
uint8_t frame_buf[TAGTINKER_MAX_FRAME_SIZE];
|
||||
size_t frame_len;
|
||||
|
||||
/* Multi-frame sequence */
|
||||
uint8_t** frame_sequence;
|
||||
size_t* frame_lengths;
|
||||
uint16_t* frame_repeats;
|
||||
size_t frame_seq_count;
|
||||
bool invert_text;
|
||||
bool color_clear;
|
||||
|
||||
/* Saved presets */
|
||||
struct {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
uint8_t page;
|
||||
bool invert;
|
||||
bool color_clear;
|
||||
char text[TAGTINKER_PRESET_TEXT_LEN];
|
||||
} presets[TAGTINKER_MAX_PRESETS];
|
||||
uint8_t preset_count;
|
||||
|
||||
/* Image settings */
|
||||
uint8_t img_page;
|
||||
uint16_t draw_width;
|
||||
uint16_t draw_height;
|
||||
|
||||
/* Indicates which mode triggered raw cmd (0=broadcast, 1=targeted) */
|
||||
uint8_t raw_mode;
|
||||
};
|
||||
|
||||
/* Main menu items */
|
||||
typedef enum {
|
||||
TagTinkerMenuBroadcast,
|
||||
TagTinkerMenuTargetESL,
|
||||
TagTinkerMenuAbout,
|
||||
} TagTinkerMainMenuItem;
|
||||
|
||||
/* Broadcast menu items */
|
||||
typedef enum {
|
||||
TagTinkerBroadcastFlipPage,
|
||||
TagTinkerBroadcastDebugScreen,
|
||||
} TagTinkerBroadcastMenuItem;
|
||||
|
||||
/* Target action items */
|
||||
typedef enum {
|
||||
TagTinkerTargetPushText,
|
||||
TagTinkerTargetPushImage,
|
||||
TagTinkerTargetPingFlash,
|
||||
} TagTinkerTargetActionItem;
|
||||
@@ -0,0 +1,392 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TagTinker Image Converter</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #09090b;
|
||||
--surface: #18181b;
|
||||
--border: #27272a;
|
||||
--primary: #FF8A00; /* Flipper Orange */
|
||||
--primary-hover: #ff9d2e;
|
||||
--text: #fafafa;
|
||||
--text-dim: #a1a1aa;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', -apple-system, sans-serif; }
|
||||
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -1px;
|
||||
color: var(--primary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
header p { color: var(--text-dim); margin-top: 0.5rem; }
|
||||
|
||||
.dropzone {
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255,255,255,0.02);
|
||||
margin-bottom: 2rem;
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropzone:hover {
|
||||
border-color: var(--primary);
|
||||
background: rgba(255,138,0,0.05);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.dropzone input {
|
||||
position: absolute;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
label { font-size: 0.875rem; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
|
||||
select, input[type="file"] {
|
||||
padding: 0.75rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
}
|
||||
select:focus { border-color: var(--primary); }
|
||||
|
||||
.preview-section {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
canvas {
|
||||
image-rendering: pixelated;
|
||||
max-width: 100%;
|
||||
background: white;
|
||||
border: 1px solid #333;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
background: var(--primary);
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
button:hover { background: var(--primary-hover); }
|
||||
button:disabled { background: var(--border); color: var(--text-dim); cursor: not-allowed; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>TagTinker Image Converter</h1>
|
||||
<p>Prepare 1-bit BMP assets for educational ESL display experiments on hardware you own.</p>
|
||||
</header>
|
||||
|
||||
<label class="dropzone" id="dropzone">
|
||||
<input type="file" id="fileInput" accept="image/*">
|
||||
<div id="dropText">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" stroke-width="2" style="margin-bottom: 1rem;">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
|
||||
</svg><br>
|
||||
<strong>Click or Drag & Drop</strong><br>
|
||||
<span style="font-size: 0.9em; color: var(--text-dim);">JPEG, PNG, WebP accepted</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>Target Resolution</label>
|
||||
<select id="resSelect">
|
||||
<option value="128x64">128x64 (Standard Small)</option>
|
||||
<option value="200x90">200x90 (Medium Tag)</option>
|
||||
<option value="250x122">250x122 (Large Tag 1)</option>
|
||||
<option value="400x300">400x300 (Large E-Paper)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label>Dithering Engine</label>
|
||||
<select id="ditherSelect">
|
||||
<option value="floyd">Floyd-Steinberg (High Detail)</option>
|
||||
<option value="atkinson">Atkinson (High Contrast)</option>
|
||||
<option value="threshold">Binary Threshold (Logos/Text)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-section" id="previewArea" style="display: none;">
|
||||
<label>Pixel Perfect Preview</label>
|
||||
<canvas id="canvasPreview"></canvas>
|
||||
</div>
|
||||
|
||||
<button id="downloadBtn" disabled>Download BMP Asset</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
const resSelect = document.getElementById('resSelect');
|
||||
const ditherSelect = document.getElementById('ditherSelect');
|
||||
const canvas = document.getElementById('canvasPreview');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const previewArea = document.getElementById('previewArea');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
|
||||
let currentImage = null;
|
||||
let lastPayloadBuffer = null;
|
||||
|
||||
// UI Handlers
|
||||
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.style.borderColor = 'var(--primary)'; });
|
||||
dropzone.addEventListener('dragleave', e => { e.preventDefault(); dropzone.style.borderColor = 'var(--border)'; });
|
||||
dropzone.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
dropzone.style.borderColor = 'var(--border)';
|
||||
if(e.dataTransfer.files.length) processFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
fileInput.addEventListener('change', e => { if(e.target.files.length) processFile(e.target.files[0]); });
|
||||
|
||||
resSelect.addEventListener('change', updatePreview);
|
||||
ditherSelect.addEventListener('change', updatePreview);
|
||||
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
if(!lastPayloadBuffer) return;
|
||||
const [w, h] = resSelect.value.split('x').map(Number);
|
||||
const blob = new Blob([lastPayloadBuffer], { type: "image/bmp" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `esl_study_${w}x${h}.bmp`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
function processFile(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
currentImage = img;
|
||||
updatePreview();
|
||||
};
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
if(!currentImage) return;
|
||||
|
||||
const [tw, th] = resSelect.value.split('x').map(Number);
|
||||
canvas.width = tw;
|
||||
canvas.height = th;
|
||||
previewArea.style.display = 'flex';
|
||||
|
||||
// Calculate scaling to 'Cover' the target resolution
|
||||
const scale = Math.max(tw / currentImage.width, th / currentImage.height);
|
||||
const sw = currentImage.width * scale;
|
||||
const sh = currentImage.height * scale;
|
||||
const offsetX = (tw - sw) / 2;
|
||||
const offsetY = (th - sh) / 2;
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, tw, th);
|
||||
ctx.drawImage(currentImage, offsetX, offsetY, sw, sh);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, tw, th);
|
||||
const data = imageData.data;
|
||||
|
||||
// Greyscale conversion
|
||||
const greyscale = new Float32Array(tw * th);
|
||||
for(let i=0; i<data.length; i+=4) {
|
||||
// Luminance
|
||||
greyscale[i/4] = 0.299*data[i] + 0.587*data[i+1] + 0.114*data[i+2];
|
||||
}
|
||||
|
||||
const method = ditherSelect.value;
|
||||
|
||||
if(method === 'threshold') {
|
||||
for(let i=0; i<greyscale.length; i++) {
|
||||
const c = greyscale[i] < 128 ? 0 : 255;
|
||||
data[i*4] = data[i*4+1] = data[i*4+2] = c;
|
||||
}
|
||||
} else if (method === 'floyd') {
|
||||
for(let y=0; y<th; y++) {
|
||||
for(let x=0; x<tw; x++) {
|
||||
const idx = y*tw + x;
|
||||
const oldPixel = greyscale[idx];
|
||||
const newPixel = oldPixel < 128 ? 0 : 255;
|
||||
|
||||
data[idx*4] = data[idx*4+1] = data[idx*4+2] = newPixel;
|
||||
const err = oldPixel - newPixel;
|
||||
|
||||
if(x+1 < tw) greyscale[idx+1] += err * 7/16;
|
||||
if(y+1 < th) {
|
||||
if(x-1 >= 0) greyscale[idx+tw-1] += err * 3/16;
|
||||
greyscale[idx+tw] += err * 5/16;
|
||||
if(x+1 < tw) greyscale[idx+tw+1] += err * 1/16;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (method === 'atkinson') {
|
||||
for(let y=0; y<th; y++) {
|
||||
for(let x=0; x<tw; x++) {
|
||||
const idx = y*tw + x;
|
||||
const oldPixel = greyscale[idx];
|
||||
const newPixel = oldPixel < 128 ? 0 : 255;
|
||||
|
||||
data[idx*4] = data[idx*4+1] = data[idx*4+2] = newPixel;
|
||||
const err = Math.floor((oldPixel - newPixel)/8);
|
||||
|
||||
if(x+1 < tw) greyscale[idx+1] += err;
|
||||
if(x+2 < tw) greyscale[idx+2] += err;
|
||||
if(y+1 < th) {
|
||||
if(x-1 >= 0) greyscale[idx+tw-1] += err;
|
||||
greyscale[idx+tw] += err;
|
||||
if(x+1 < tw) greyscale[idx+tw+1] += err;
|
||||
}
|
||||
if(y+2 < th) greyscale[idx+tw*2] += err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
generateBMP();
|
||||
downloadBtn.disabled = false;
|
||||
usbBtn.disabled = false;
|
||||
}
|
||||
|
||||
function generateBMP() {
|
||||
const [w, h] = resSelect.value.split('x').map(Number);
|
||||
const imageData = ctx.getImageData(0, 0, w, h).data;
|
||||
|
||||
const binaryPixels = new Uint8Array(w * h);
|
||||
for(let i=0; i<binaryPixels.length; i++) {
|
||||
// Determine if pixel is white (1) or black (0) for BMP
|
||||
binaryPixels[i] = imageData[i*4] > 127 ? 1 : 0;
|
||||
}
|
||||
|
||||
const rowStride = Math.floor((w + 31) / 32) * 4;
|
||||
const pixelDataSize = rowStride * h;
|
||||
const fileSize = 14 + 40 + 8 + pixelDataSize;
|
||||
|
||||
const buffer = new ArrayBuffer(fileSize);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// BMP Header
|
||||
view.setUint8(0, 0x42); // 'B'
|
||||
view.setUint8(1, 0x4D); // 'M'
|
||||
view.setUint32(2, fileSize, true);
|
||||
view.setUint32(6, 0, true);
|
||||
view.setUint32(10, 62, true); // Offset to Data
|
||||
|
||||
// DIB Header
|
||||
view.setUint32(14, 40, true);
|
||||
view.setUint32(18, w, true);
|
||||
view.setInt32(22, h, true); // Positive = Bottom-Up
|
||||
view.setUint16(26, 1, true); // Color Planes
|
||||
view.setUint16(28, 1, true); // Bits per pixel (1-bit)
|
||||
view.setUint32(30, 0, true); // BI_RGB
|
||||
view.setUint32(34, pixelDataSize, true);
|
||||
view.setUint32(38, 2835, true);
|
||||
view.setUint32(42, 2835, true);
|
||||
view.setUint32(46, 2, true);
|
||||
view.setUint32(50, 0, true);
|
||||
|
||||
// Color Table
|
||||
view.setUint32(54, 0x00000000, true); // Color 0: Black
|
||||
view.setUint32(58, 0x00FFFFFF, true); // Color 1: White
|
||||
|
||||
// Pixel Data
|
||||
const dataView = new Uint8Array(buffer, 62);
|
||||
for (let y = 0; y < h; y++) {
|
||||
// Bottom-Up
|
||||
const srcY = h - 1 - y;
|
||||
for (let x = 0; x < w; x++) {
|
||||
const px = binaryPixels[srcY * w + x];
|
||||
if (px === 1) { // Set bit if White
|
||||
const byteIndex = y * rowStride + Math.floor(x / 8);
|
||||
const bitShift = 7 - (x % 8);
|
||||
dataView[byteIndex] |= (1 << bitShift);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([buffer], { type: "image/bmp" });
|
||||
lastPayloadBuffer = buffer;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* TagTinker — Number Lock Barcode Input
|
||||
*
|
||||
* Clean centered digit entry: N + 16 digits in groups of 4.
|
||||
* UP/DOWN cycle, LEFT/RIGHT move, OK confirm, BACK cancel.
|
||||
*/
|
||||
|
||||
#include "numlock_input.h"
|
||||
#include <gui/elements.h>
|
||||
#include <furi.h>
|
||||
#include <string.h>
|
||||
|
||||
#define NUM_DIGITS 16
|
||||
#define CHAR_W 6
|
||||
#define GROUP_SIZE 4
|
||||
#define GROUP_GAP 5
|
||||
#define PREFIX_W 10
|
||||
|
||||
typedef struct {
|
||||
uint8_t digits[NUM_DIGITS];
|
||||
uint8_t cursor;
|
||||
} NumlockModel;
|
||||
|
||||
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. */
|
||||
return 4 + 8 + i * CHAR_W + groups * GROUP_GAP;
|
||||
}
|
||||
|
||||
static void numlock_draw(Canvas* canvas, void* model_v) {
|
||||
NumlockModel* m = model_v;
|
||||
canvas_clear(canvas);
|
||||
|
||||
/* 1. Header Bar — Inverted Tech Banner */
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(canvas, 0, 0, 128, 12);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(canvas, 64, 6, AlignCenter, AlignCenter, "SET BARCODE");
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
/* 2. Main Input Frame — Heavy industrial border */
|
||||
int frame_y = 19;
|
||||
int frame_h = 24;
|
||||
canvas_draw_rframe(canvas, 1, frame_y, 126, frame_h, 2);
|
||||
/* Inner shadow/border detail */
|
||||
canvas_draw_line(canvas, 2, frame_y+1, 125, frame_y+1);
|
||||
|
||||
const uint8_t baseline = 36;
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
|
||||
/* Fixed 'N' Prefix */
|
||||
canvas_draw_str(canvas, 4, baseline, "N");
|
||||
|
||||
/* 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) {
|
||||
/* Selected digit bounding block */
|
||||
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, ch);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
/* Sharp Navigator Arrows pointing at selection */
|
||||
uint8_t cx = x + CHAR_W / 2 - 1; /* Center of char */
|
||||
|
||||
/* Up pointer */
|
||||
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);
|
||||
|
||||
/* Down pointer */
|
||||
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 {
|
||||
/* Standard unselected digit */
|
||||
canvas_draw_str(canvas, x, baseline, ch);
|
||||
}
|
||||
}
|
||||
|
||||
/* 3. Footer UI with Clean Button Callouts */
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
/* D-Pad Hint Icons */
|
||||
canvas_draw_str(canvas, 2, 59, "<\x12\x13> Sel");
|
||||
canvas_draw_str(canvas, 45, 59, "^\x18\x19v Set");
|
||||
|
||||
/* Thick OK Button */
|
||||
canvas_draw_rbox(canvas, 92, 48, 34, 14, 2);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_str_aligned(canvas, 109, 55, AlignCenter, AlignCenter, "Hold OK");
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
static bool numlock_input(InputEvent* input, void* ctx) {
|
||||
NumlockInput* numlock = ctx;
|
||||
if(input->type != InputTypePress && input->type != InputTypeRepeat) return false;
|
||||
|
||||
bool consumed = false;
|
||||
|
||||
with_view_model(
|
||||
numlock->view,
|
||||
NumlockModel * m,
|
||||
{
|
||||
switch(input->key) {
|
||||
case InputKeyUp:
|
||||
m->digits[m->cursor] = (m->digits[m->cursor] + 1) % 10;
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
m->digits[m->cursor] = (m->digits[m->cursor] + 9) % 10;
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
if(m->cursor > 0) m->cursor--;
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyRight:
|
||||
if(m->cursor < NUM_DIGITS - 1) m->cursor++;
|
||||
consumed = true;
|
||||
break;
|
||||
case InputKeyOk: {
|
||||
char barcode[18];
|
||||
barcode[0] = 'N';
|
||||
for(uint8_t i = 0; i < NUM_DIGITS; i++)
|
||||
barcode[1 + i] = '0' + m->digits[i];
|
||||
barcode[17] = '\0';
|
||||
if(numlock->callback)
|
||||
numlock->callback(numlock->callback_ctx, barcode);
|
||||
consumed = true;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
consumed);
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
NumlockInput* numlock_input_alloc(void) {
|
||||
NumlockInput* numlock = malloc(sizeof(NumlockInput));
|
||||
numlock->view = view_alloc();
|
||||
view_allocate_model(numlock->view, ViewModelTypeLocking, sizeof(NumlockModel));
|
||||
view_set_draw_callback(numlock->view, numlock_draw);
|
||||
view_set_input_callback(numlock->view, numlock_input);
|
||||
view_set_context(numlock->view, numlock);
|
||||
numlock->callback = NULL;
|
||||
numlock->callback_ctx = NULL;
|
||||
|
||||
with_view_model(
|
||||
numlock->view,
|
||||
NumlockModel * m,
|
||||
{
|
||||
memset(m->digits, 0, NUM_DIGITS);
|
||||
m->digits[0] = 4;
|
||||
m->cursor = 1;
|
||||
},
|
||||
true);
|
||||
|
||||
return numlock;
|
||||
}
|
||||
|
||||
void numlock_input_free(NumlockInput* numlock) {
|
||||
view_free(numlock->view);
|
||||
free(numlock);
|
||||
}
|
||||
|
||||
View* numlock_input_get_view(NumlockInput* numlock) {
|
||||
return numlock->view;
|
||||
}
|
||||
|
||||
void numlock_input_set_callback(NumlockInput* numlock, NumlockCallback cb, void* ctx) {
|
||||
numlock->callback = cb;
|
||||
numlock->callback_ctx = ctx;
|
||||
}
|
||||
|
||||
void numlock_input_reset(NumlockInput* numlock) {
|
||||
with_view_model(
|
||||
numlock->view,
|
||||
NumlockModel * m,
|
||||
{
|
||||
memset(m->digits, 0, NUM_DIGITS);
|
||||
m->digits[0] = 4;
|
||||
m->cursor = 1;
|
||||
},
|
||||
true);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* TagTinker - number lock barcode input view
|
||||
*
|
||||
* Custom view for entering a 16-digit ESL barcode.
|
||||
* Format: N + 16 digits (auto-prefixed 'N').
|
||||
*
|
||||
* Controls:
|
||||
* UP/DOWN - cycle digit 0-9
|
||||
* LEFT/RIGHT - move cursor position
|
||||
* OK - confirm barcode
|
||||
* BACK - cancel
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gui/view.h>
|
||||
|
||||
typedef void (*NumlockCallback)(void* ctx, const char* barcode);
|
||||
|
||||
typedef struct {
|
||||
View* view;
|
||||
NumlockCallback callback;
|
||||
void* callback_ctx;
|
||||
} NumlockInput;
|
||||
|
||||
NumlockInput* numlock_input_alloc(void);
|
||||
void numlock_input_free(NumlockInput* numlock);
|
||||
View* numlock_input_get_view(NumlockInput* numlock);
|
||||
void numlock_input_set_callback(NumlockInput* numlock, NumlockCallback cb, void* ctx);
|
||||
void numlock_input_reset(NumlockInput* numlock);
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Helper to render simple 5x7 bitmap text onto the ESL image buffer.
|
||||
*/
|
||||
|
||||
#ifndef TAGTINKER_FONT_H
|
||||
#define TAGTINKER_FONT_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#define FONT_CHAR_W 5
|
||||
#define FONT_CHAR_H 7
|
||||
|
||||
/* Standard 5x7 ASCII bitmap font (characters 32-127) */
|
||||
static const uint8_t font_5x7[][5] = {
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00}, /* */
|
||||
{0x00, 0x00, 0x5f, 0x00, 0x00}, /* ! */
|
||||
{0x00, 0x07, 0x00, 0x07, 0x00}, /* " */
|
||||
{0x14, 0x7f, 0x14, 0x7f, 0x14}, /* # */
|
||||
{0x24, 0x2a, 0x7f, 0x2a, 0x12}, /* $ */
|
||||
{0x23, 0x13, 0x08, 0x64, 0x62}, /* % */
|
||||
{0x36, 0x49, 0x55, 0x22, 0x50}, /* & */
|
||||
{0x00, 0x05, 0x03, 0x00, 0x00}, /* ' */
|
||||
{0x00, 0x1c, 0x22, 0x41, 0x00}, /* ( */
|
||||
{0x00, 0x41, 0x22, 0x1c, 0x00}, /* ) */
|
||||
{0x08, 0x2a, 0x1c, 0x2a, 0x08}, /* * */
|
||||
{0x08, 0x08, 0x3e, 0x08, 0x08}, /* + */
|
||||
{0x00, 0x50, 0x30, 0x00, 0x00}, /* , */
|
||||
{0x08, 0x08, 0x08, 0x08, 0x08}, /* - */
|
||||
{0x00, 0x60, 0x60, 0x00, 0x00}, /* . */
|
||||
{0x20, 0x10, 0x08, 0x04, 0x02}, /* / */
|
||||
{0x3e, 0x51, 0x49, 0x45, 0x3e}, /* 0 */
|
||||
{0x00, 0x42, 0x7f, 0x40, 0x00}, /* 1 */
|
||||
{0x42, 0x61, 0x51, 0x49, 0x46}, /* 2 */
|
||||
{0x21, 0x41, 0x45, 0x4b, 0x31}, /* 3 */
|
||||
{0x18, 0x14, 0x12, 0x7f, 0x10}, /* 4 */
|
||||
{0x27, 0x45, 0x45, 0x45, 0x39}, /* 5 */
|
||||
{0x3c, 0x4a, 0x49, 0x49, 0x30}, /* 6 */
|
||||
{0x01, 0x71, 0x09, 0x05, 0x03}, /* 7 */
|
||||
{0x36, 0x49, 0x49, 0x49, 0x36}, /* 8 */
|
||||
{0x06, 0x49, 0x49, 0x29, 0x1e}, /* 9 */
|
||||
{0x00, 0x36, 0x36, 0x00, 0x00}, /* : */
|
||||
{0x00, 0x56, 0x36, 0x00, 0x00}, /* ; */
|
||||
{0x00, 0x08, 0x14, 0x22, 0x41}, /* < */
|
||||
{0x14, 0x14, 0x14, 0x14, 0x14}, /* = */
|
||||
{0x41, 0x22, 0x14, 0x08, 0x00}, /* > */
|
||||
{0x02, 0x01, 0x51, 0x09, 0x06}, /* ? */
|
||||
{0x32, 0x49, 0x79, 0x41, 0x3e}, /* @ */
|
||||
{0x7e, 0x11, 0x11, 0x11, 0x7e}, /* A */
|
||||
{0x7f, 0x49, 0x49, 0x49, 0x36}, /* B */
|
||||
{0x3e, 0x41, 0x41, 0x41, 0x22}, /* C */
|
||||
{0x7f, 0x41, 0x41, 0x22, 0x1c}, /* D */
|
||||
{0x7f, 0x49, 0x49, 0x49, 0x41}, /* E */
|
||||
{0x7f, 0x09, 0x09, 0x01, 0x01}, /* F */
|
||||
{0x3e, 0x41, 0x41, 0x51, 0x32}, /* G */
|
||||
{0x7f, 0x08, 0x08, 0x08, 0x7f}, /* H */
|
||||
{0x00, 0x41, 0x7f, 0x41, 0x00}, /* I */
|
||||
{0x20, 0x40, 0x41, 0x3f, 0x01}, /* J */
|
||||
{0x7f, 0x08, 0x14, 0x22, 0x41}, /* K */
|
||||
{0x7f, 0x40, 0x40, 0x40, 0x40}, /* L */
|
||||
{0x7f, 0x02, 0x04, 0x02, 0x7f}, /* M */
|
||||
{0x7f, 0x04, 0x08, 0x10, 0x7f}, /* N */
|
||||
{0x3e, 0x41, 0x41, 0x41, 0x3e}, /* O */
|
||||
{0x7f, 0x09, 0x09, 0x09, 0x06}, /* P */
|
||||
{0x3e, 0x41, 0x51, 0x21, 0x5e}, /* Q */
|
||||
{0x7f, 0x09, 0x19, 0x29, 0x46}, /* R */
|
||||
{0x46, 0x49, 0x49, 0x49, 0x31}, /* S */
|
||||
{0x01, 0x01, 0x7f, 0x01, 0x01}, /* T */
|
||||
{0x3f, 0x40, 0x40, 0x40, 0x3f}, /* U */
|
||||
{0x1f, 0x20, 0x40, 0x20, 0x1f}, /* V */
|
||||
{0x7f, 0x20, 0x18, 0x20, 0x7f}, /* W */
|
||||
{0x63, 0x14, 0x08, 0x14, 0x63}, /* X */
|
||||
{0x03, 0x04, 0x78, 0x04, 0x03}, /* Y */
|
||||
{0x61, 0x51, 0x49, 0x45, 0x43}, /* Z */
|
||||
{0x00, 0x00, 0x7f, 0x41, 0x41}, /* [ */
|
||||
{0x02, 0x04, 0x08, 0x10, 0x20}, /* \ */
|
||||
{0x41, 0x41, 0x7f, 0x00, 0x00}, /* ] */
|
||||
{0x04, 0x02, 0x01, 0x02, 0x04}, /* ^ */
|
||||
{0x40, 0x40, 0x40, 0x40, 0x40}, /* _ */
|
||||
{0x00, 0x01, 0x02, 0x04, 0x00}, /* ` */
|
||||
{0x20, 0x54, 0x54, 0x54, 0x78}, /* a */
|
||||
{0x7f, 0x48, 0x44, 0x44, 0x38}, /* b */
|
||||
{0x38, 0x44, 0x44, 0x44, 0x20}, /* c */
|
||||
{0x38, 0x44, 0x44, 0x48, 0x7f}, /* d */
|
||||
{0x38, 0x54, 0x54, 0x54, 0x18}, /* e */
|
||||
{0x08, 0x7e, 0x09, 0x01, 0x02}, /* f */
|
||||
{0x08, 0x14, 0x54, 0x54, 0x3c}, /* g */
|
||||
{0x7f, 0x08, 0x04, 0x04, 0x78}, /* h */
|
||||
{0x00, 0x44, 0x7d, 0x40, 0x00}, /* i */
|
||||
{0x20, 0x40, 0x44, 0x3d, 0x00}, /* j */
|
||||
{0x00, 0x7f, 0x10, 0x28, 0x44}, /* k */
|
||||
{0x00, 0x41, 0x7f, 0x40, 0x00}, /* l */
|
||||
{0x7c, 0x04, 0x18, 0x04, 0x78}, /* m */
|
||||
{0x7c, 0x08, 0x04, 0x04, 0x78}, /* n */
|
||||
{0x38, 0x44, 0x44, 0x44, 0x38}, /* o */
|
||||
{0x7c, 0x14, 0x14, 0x14, 0x08}, /* p */
|
||||
{0x08, 0x14, 0x14, 0x18, 0x7c}, /* q */
|
||||
{0x7c, 0x08, 0x04, 0x04, 0x08}, /* r */
|
||||
{0x48, 0x54, 0x54, 0x54, 0x20}, /* s */
|
||||
{0x04, 0x3f, 0x44, 0x40, 0x20}, /* t */
|
||||
{0x3c, 0x40, 0x40, 0x20, 0x7c}, /* u */
|
||||
{0x1c, 0x20, 0x40, 0x20, 0x1c}, /* v */
|
||||
{0x3c, 0x40, 0x30, 0x40, 0x3c}, /* w */
|
||||
{0x44, 0x28, 0x10, 0x28, 0x44}, /* x */
|
||||
{0x0c, 0x50, 0x50, 0x50, 0x3c}, /* y */
|
||||
{0x44, 0x64, 0x54, 0x4c, 0x44}, /* z */
|
||||
{0x00, 0x08, 0x36, 0x41, 0x00}, /* { */
|
||||
{0x00, 0x00, 0x7f, 0x00, 0x00}, /* | */
|
||||
{0x00, 0x41, 0x36, 0x08, 0x00}, /* } */
|
||||
{0x08, 0x04, 0x08, 0x10, 0x08}, /* ~ */
|
||||
};
|
||||
|
||||
/* Sets a pixel in a 1-byte-per-pixel buffer */
|
||||
static inline void set_pixel(uint8_t* buf, uint16_t w, uint16_t h, uint16_t x, uint16_t y, uint8_t val) {
|
||||
if(x < w && y < h) buf[y * w + x] = val;
|
||||
}
|
||||
|
||||
/*
|
||||
* Render text to fill the entire buffer.
|
||||
* bg_val/fg_val control polarity:
|
||||
* Normal: bg=1 (white), fg=0 (black)
|
||||
* Inverted: bg=0 (black), fg=1 (white)
|
||||
* Zero margin — text fills edge to edge.
|
||||
*/
|
||||
static inline void render_text_ex(uint8_t* buf, uint16_t w, uint16_t h,
|
||||
const char* text, uint8_t bg_val, uint8_t fg_val) {
|
||||
memset(buf, bg_val, w * h);
|
||||
|
||||
size_t len = strlen(text);
|
||||
if(len == 0) return;
|
||||
|
||||
uint16_t scale_w = w / (len * (FONT_CHAR_W + 1));
|
||||
uint16_t scale_h = h / FONT_CHAR_H;
|
||||
|
||||
if(scale_w < 1) scale_w = 1;
|
||||
if(scale_h < 1) scale_h = 1;
|
||||
|
||||
/* Use uniform scale = min(w,h) for proportional text + better RLE */
|
||||
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 = (w - text_w) / 2;
|
||||
uint16_t start_y = (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_pixel(buf, w, h, px, py, 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);
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user