├── .gitignore ├── LICENSE ├── audio_messages_cz.txt ├── audio_messages_de.txt ├── audio_messages_en.txt ├── audio_messages_es.txt ├── audio_messages_it.txt ├── audio_messages_nl.txt ├── docs ├── README.md ├── README_EN.md ├── usage_cheat_sheet_de.pdf ├── usage_cheat_sheet_de.png ├── usage_cheat_sheet_en.pdf └── usage_cheat_sheet_en.png ├── platformio.ini ├── tonuino.ino └── tools ├── add_lead_in_messages.py ├── create_audio_messages.py └── text_to_speech.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### OSX ### 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | 22 | # Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | # TonUINO 30 | sd-card* 31 | __pycache__ 32 | *.pyc 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /audio_messages_cz.txt: -------------------------------------------------------------------------------- 1 | 0800.mp3|Pojďme! 2 | 0801.mp3|Byla nalezena nová karta. Nechte ji prosím na krabici dokud nebude její nastavení dokončeno. Pomocí tlačítek hlasitosti vyberte složku a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 3 | 0802.mp3|Ok, jediná skladba. Pomocí tlačítek hlasitosti vyberte skladbu a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 4 | 0803.mp3|Ok. Pomocí tlačítek hlasitosti vyberte počáteční skladbu pro virtuální složku a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 5 | 0804.mp3|OK. Pomocí tlačítek hlasitosti vyberte konečnou skladbu pro virtuální složku a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 6 | 0805.mp3|Ok, karta je nastavena. Bavte se. 7 | 0806.mp3|Při nastavování karty došlo k chybě. Prosím zkuste to znovu. 8 | 0807.mp3|Nastavení karty bylo zrušeno. 9 | 0808.mp3|Jejda, moje baterie se vybila. Požádejte mámu nebo tátu, aby ji vyměnili. Teď se vypínám, ahoj. 10 | 0809.mp3|Reset byl dokončen. 11 | 0810.mp3|Zadejte prosím PIN. 12 | 0811.mp3|Zadání PINu bylo zrušeno. 13 | 0820.mp3|Ok, pomocí tlačítek hlasitosti vyberte režim přehrávání a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 14 | 0821.mp3|Režim příběhů. Přehraje jednu náhodnou skladbu ze složky. 15 | 0822.mp3|Režim alba. Přehraje celou složku. 16 | 0823.mp3|Párty režim. Náhodně přehraje celou složku. 17 | 0824.mp3|Režim singl. Přehraje jednu konkrétní skladbu ze složky. 18 | 0825.mp3|Režim audio knihy. Přehraje celou složku a pamatuje si, kde jsme minule skončili. 19 | 0826.mp3|Virtuální složka, režim příběhů. Přehraje jednu náhodnou skladbu mezi počáteční a konečnou skladbou. 20 | 0827.mp3|Virtuální složka, režim alba. Přehraje všechny skladby mezi počáteční a konečnou skladbou. 21 | 0828.mp3|Virtuální složka, párty režim. Náhodně přehraje skladby mezi počáteční a konečnou skladbou. 22 | 0900.mp3|Rodičovské menu. Pomocí tlačítek hlasitosti vyberte možnost a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 23 | 0901.mp3|Předvolba uložena. 24 | 0902.mp3|Audio kniha je opět na začátku. 25 | 0903.mp3|Předvolby byly vymazány. 26 | 0904.mp3|Rodičovské menu ukončeno. 27 | 0910.mp3|Vymazat kartu. 28 | 0911.mp3|Nastavit počáteční hlasitost po spuštění. 29 | 0912.mp3|Nastavit maximální hlasitost. 30 | 0913.mp3|Nastavit hlasitost nabídky. 31 | 0914.mp3|Nastavit ekvalizér. 32 | 0915.mp3|Naučit se infračervené dálkové ovládání. Potřebujete dálkový ovladač s nejméně sedmi klávesami. 33 | 0916.mp3|Nastavit časovač vypnutí. 34 | 0917.mp3|Nastavit audio knihu na začátek. 35 | 0918.mp3|Vymazat předvolby. 36 | 0919.mp3|Vypnout. 37 | 0920.mp3|Položte prosím kartu na krabici a počkejte na potvrzení. 38 | 0921.mp3|Karta vymazána. 39 | 0922.mp3|Vymazání karty se nezdařilo. Zkuste to prosím znovu. 40 | 0923.mp3|Mazání karty bylo zrušeno. 41 | 0930.mp3|Pomocí tlačítek hlasitosti nastavte počáteční hlasitost po spuštění a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 42 | 0931.mp3|Pomocí tlačítek hlasitosti vyberte maximální hlasitost a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 43 | 0932.mp3|Pomocí tlačítek hlasitosti vyberte hlasitost nabídky a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 44 | 0940.mp3|Pomocí tlačítek hlasitosti vyberte ekvalizér a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 45 | 0941.mp3|Standard 46 | 0942.mp3|Pop 47 | 0943.mp3|Rock 48 | 0944.mp3|Jazz 49 | 0945.mp3|Klasika 50 | 0946.mp3|Basy 51 | 0950.mp3|Tato funkce vyžaduje infračervený přijímač, který je poté nutné povolit v kódu programu. 52 | 0951.mp3|Stiskněte tlačítko pro zvýšení hlasitosti na dálkovém ovladači. 53 | 0952.mp3|Stiskněte tlačítko pro snížení hlasitosti na dálkovém ovladači. 54 | 0953.mp3|Stiskněte tlačítko předchozí skladby na dálkovém ovladači. 55 | 0954.mp3|Stiskněte tlačítko následující skladby na dálkovém ovladači. 56 | 0955.mp3|Stiskněte prostřední tlačítko na dálkovém ovladači. 57 | 0956.mp3|Stiskněte tlačítko nabídky na dálkovém ovladači. 58 | 0957.mp3|Na dálkovém ovládání stiskněte tlačítko přehrávání/pozastavení. 59 | 0960.mp3|Pomocí tlačítek hlasitosti nastavte časovač vypnutí a potvrďte tlačítkem pauza. Chcete-li akci zrušit, podržte tlačítko pauza. 60 | 0961.mp3|5 minut. 61 | 0962.mp3|10 minut. 62 | 0963.mp3|15 minut. 63 | 0964.mp3|20 minut. 64 | 0965.mp3|30 minut. 65 | 0966.mp3|60 minut. 66 | 0967.mp3|Deaktivovat časovač vypnutí. 67 | -------------------------------------------------------------------------------- /audio_messages_de.txt: -------------------------------------------------------------------------------- 1 | 0800.mp3|Los gehts! 2 | 0801.mp3|Eine neue Karte wurde erkannt. Bitte lasse die Karte auf der Box liegen, bis die Karte konfiguriert ist. Du kannst jetzt mit den Lautstärketasten einen Ordner auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 3 | 0802.mp3|Ok, Lieblingsfolge. Du kannst jetzt mit den Lautstärketasten eine Lieblingsfolge auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 4 | 0803.mp3|Ok. Du kannst jetzt mit den Lautstärketasten eine Startdatei für den virtuellen Ordner auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 5 | 0804.mp3|Ok. Du kannst jetzt mit den Lautstärketasten eine Enddatei für den virtuellen Ordner auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 6 | 0805.mp3|Ok, ich habe die Karte konfiguriert. Viel Spass damit. 7 | 0806.mp3|Beim konfigurieren der Karte ist leider ein Fehler aufgetreten. Bitte versuche es noch einmal. 8 | 0807.mp3|Karte konfigurieren abgebrochen. 9 | 0808.mp3|Oh je, meine Batterie ist alle! Bitte Mama oder Papa, daß sie die Batterie tauschen. Ich schalte mich jetzt ab, bis bald. 10 | 0809.mp3|Reset durchgeführt. 11 | 0810.mp3|Bitte PIN eingeben. 12 | 0811.mp3|PIN Eingabe abgebrochen. 13 | 0820.mp3|Ok. Du kannst jetzt mit den Lautstärketasten den Wiedergabemodus auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 14 | 0821.mp3|Hörspielmodus. Einen zufälligen Titel aus dem Ordner wiedergeben. 15 | 0822.mp3|Albummodus. Den kompletten Ordner wiedergeben. 16 | 0823.mp3|Partymodus. Den Ordner zufällig wiedergeben. 17 | 0824.mp3|Lieblingsfolge. Einen bestimmten Titel aus dem Ordner wiedergeben. 18 | 0825.mp3|Hörbuchmodus. Den kompletten Ordner wiedergeben und den Fortschritt speichern. 19 | 0826.mp3|Virtueller Ordner, Hörspielmodus. Einen zufälligen Titel zwischen Start und Enddatei wiedergeben. 20 | 0827.mp3|Virtueller Ordner, Albummodus. Alle Titel zwischen Start und Enddatei komplett wiedergeben. 21 | 0828.mp3|Virtueller Ordner, Partymodus. Alle Titel zwischen Start und Enddatei zufällig wiedergeben. 22 | 0900.mp3|Elternmenü. Du kannst jetzt mit den Lautstärketasten eine Option auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 23 | 0901.mp3|Einstellung gespeichert. 24 | 0902.mp3|Fortschritte vom Hörbuchmodus zurückgesetzt. 25 | 0903.mp3|Einstellungen zurückgesetzt. 26 | 0904.mp3|Elternmenü beendet. 27 | 0910.mp3|Karte löschen. 28 | 0911.mp3|Startlautstärke festlegen. 29 | 0912.mp3|Maximallautstärke festlegen. 30 | 0913.mp3|Menülautstärke festlegen. 31 | 0914.mp3|Equalizer festlegen. 32 | 0915.mp3|Fernbedienung anlernen. Du benötigst eine Fernbedienung mit mindestens 7 Tasten. 33 | 0916.mp3|Shutdown Timer festlegen. 34 | 0917.mp3|Fortschritte vom Hörbuchmodus zurücksetzen. 35 | 0918.mp3|Einstellungen zurücksetzen. 36 | 0919.mp3|Box ausschalten. 37 | 0920.mp3|Bitte lege die zu löschende Karte auf die Box und warte auf die Bestätigung. 38 | 0921.mp3|Ok, ich habe die Karte gelöscht. 39 | 0922.mp3|Beim löschen der Karte ist leider ein Fehler aufgetreten. Bitte versuche es noch einmal. 40 | 0923.mp3|Karte löschen abgebrochen. 41 | 0930.mp3|Mit den Lautstärketasten die Startlautstärke auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 42 | 0931.mp3|Mit den Lautstärketasten die Maximallautstärke auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 43 | 0932.mp3|Mit den Lautstärketasten die Menülautstärke auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 44 | 0940.mp3|Mit den Lautstärketasten den Equalizer auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 45 | 0941.mp3|Normal 46 | 0942.mp3|Pop 47 | 0943.mp3|Rock 48 | 0944.mp3|Jazz 49 | 0945.mp3|Classic 50 | 0946.mp3|Bass 51 | 0950.mp3|Diese Funktion benötigt einen Infrarotempfänger, welcher dann im Programmcode aktiviert werden muss. 52 | 0951.mp3|Drücke auf der Fernbedienung die Lautstärke erhöhen Taste. 53 | 0952.mp3|Drücke auf der Fernbedienung die Lautstärke verringern Taste. 54 | 0953.mp3|Drücke auf der Fernbedienung die vorheriger Titel Taste. 55 | 0954.mp3|Drücke auf der Fernbedienung die nächster Titel Taste. 56 | 0955.mp3|Drücke auf der Fernbedienung die Center Taste. 57 | 0956.mp3|Drücke auf der Fernbedienung die Menü Taste. 58 | 0957.mp3|Drücke auf der Fernbedienung die Play/Pause Taste. 59 | 0960.mp3|Mit den Lautstärketasten den Shutdown Timer auswählen und mit der Pausetaste bestätigen. Durch gedrückt halten der Pausetaste kannst du abbrechen. 60 | 0961.mp3|5 Minuten. 61 | 0962.mp3|10 Minuten. 62 | 0963.mp3|15 Minuten. 63 | 0964.mp3|20 Minuten. 64 | 0965.mp3|30 Minuten. 65 | 0966.mp3|60 Minuten. 66 | 0967.mp3|Shutdown Timer deaktivieren. 67 | -------------------------------------------------------------------------------- /audio_messages_en.txt: -------------------------------------------------------------------------------- 1 | 0800.mp3|Lets go! 2 | 0801.mp3|A new card was found, please leave it on the box until its setup. Use the volume keys to pick a folder and confirm with the pause key. Hold the pause key to cancel. 3 | 0802.mp3|Ok, single track. Use the volume keys to pick a track and confirm with the pause key. Hold the pause key to cancel. 4 | 0803.mp3|Ok. Use the volume keys to pick a start track for the virtual folder and confirm with the pause key. Hold the pause key to cancel. 5 | 0804.mp3|Ok. Use the volume keys to pick an end track for the virtual folder and confirm with the pause key. Hold the pause key to cancel. 6 | 0805.mp3|Ok, the card is setup. Have fun. 7 | 0806.mp3|An error occurred while setting up the card. Please try again. 8 | 0807.mp3|Card setup canceled. 9 | 0808.mp3|Oops my batteries died. Please ask mom or dad to swap them. Im powering down now, bye. 10 | 0809.mp3|Reset completed. 11 | 0810.mp3|Please enter PIN. 12 | 0811.mp3|PIN entry canceled. 13 | 0820.mp3|Ok, use the volume keys to pick a playback mode and confirm with the pause key. Hold the pause key to cancel. 14 | 0821.mp3|Story mode. Play one random track from the folder. 15 | 0822.mp3|Album mode. Play the complete folder. 16 | 0823.mp3|Party mode. Randomly play the complete folder. 17 | 0824.mp3|Single mode. Play one particular track from the folder. 18 | 0825.mp3|Audiobook mode. Play the complete folder and track the progress. 19 | 0826.mp3|Virtual folder, story mode. Play one random track between start and end track. 20 | 0827.mp3|Virtual folder, album mode. Play all tracks between start and end track. 21 | 0828.mp3|Virtual folder, party mode. Randomly play the tracks between start track and end track. 22 | 0900.mp3|Parents menu. Use the volume keys to pick an option and confirm with the pause key. Hold the pause key to cancel. 23 | 0901.mp3|Preference saved. 24 | 0902.mp3|Audiobook progress reset completed. 25 | 0903.mp3|Preferences reset completed. 26 | 0904.mp3|Parents menu terminated. 27 | 0910.mp3|Erase card. 28 | 0911.mp3|Setup startup volume. 29 | 0912.mp3|Setup maximum volume. 30 | 0913.mp3|Setup menu volume. 31 | 0914.mp3|Setup equalizer. 32 | 0915.mp3|Learn infrared remote. You need a remote with at least 7 keys. 33 | 0916.mp3|Setup shutdown timer. 34 | 0917.mp3|Reset audiobook progress. 35 | 0918.mp3|Reset preferences. 36 | 0919.mp3|Shutdown box. 37 | 0920.mp3|Please put the card on the box and wait for the confirmation. 38 | 0921.mp3|Ok, card erased. 39 | 0922.mp3|Erasing the card failed. Please try again. 40 | 0923.mp3|Erasing the card canceled. 41 | 0930.mp3|Use the volume keys to pick the startup volume and confirm with the pause key. Hold the pause key to cancel. 42 | 0931.mp3|Use the volume keys to pick the maximum volume and confirm with the pause key. Hold the pause key to cancel. 43 | 0932.mp3|Use the volume keys to pick the menu volume and confirm with the pause key. Hold the pause key to cancel. 44 | 0940.mp3|Use the volume keys to pick the equalizer and confirm with the pause key. Hold the pause key to cancel. 45 | 0941.mp3|Normal 46 | 0942.mp3|Pop 47 | 0943.mp3|Rock 48 | 0944.mp3|Jazz 49 | 0945.mp3|Classic 50 | 0946.mp3|Base 51 | 0950.mp3|This feature needs an infrared receiver, which then needs to be enabled in the program code. 52 | 0951.mp3|Press the volume up key on the remote. 53 | 0952.mp3|Press the volume down key on the remote. 54 | 0953.mp3|Press the previous track key on the remote. 55 | 0954.mp3|Press the next track key on the remote. 56 | 0955.mp3|Press the center key on the remote. 57 | 0956.mp3|Press the menu key on the remote. 58 | 0957.mp3|Press the play/pause key on the remote. 59 | 0960.mp3|Use the volume keys to pick the shutdown timer and confirm with the pause key. Hold the pause key to cancel. 60 | 0961.mp3|5 minutes. 61 | 0962.mp3|10 minutes. 62 | 0963.mp3|15 minutes. 63 | 0964.mp3|20 minutes. 64 | 0965.mp3|30 minutes. 65 | 0966.mp3|60 minutes. 66 | 0967.mp3|Deactivate shutdown timer. 67 | -------------------------------------------------------------------------------- /audio_messages_es.txt: -------------------------------------------------------------------------------- 1 | 0800.mp3|Hola! 2 | 0801.mp3|Acabo de detectar una tarjeta nueva, por favor déjala encima hasta que acabe de configurarla. Usa los botones de volumen para escojer una carpeta y confirma con el botón de pausa. Mantén presionado el botón de pausa para cancelar. 3 | 0802.mp3|Vale, solo una vez. Usa los botones de volumen para escojer una pista y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 4 | 0803.mp3|Vale. Usa los botones de volumen para escojer la primera pista de la carpeta virtual y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 5 | 0804.mp3|Vale. Usa los botones de volumen para escojer la última pista de la carpeta virtual y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 6 | 0805.mp3|Vale, la tarjeta esta configurada. Diviértete. 7 | 0806.mp3|Ha ocurrido un error al configurar la tarjeta. Por favor inténtalo de nuevo. 8 | 0807.mp3|Configuración de tarjeta cancelada. 9 | 0808.mp3|Uy, me he quedado sin batería. Díle a mama o papa que las cambie. Me voy a apagar ahora, adios. 10 | 0809.mp3|Reset completado. 11 | 0810.mp3|Por favor intruduce PIN. 12 | 0811.mp3|Esperando al PIN cancelado. 13 | 0820.mp3|Vale, usa los botones de volumen para escojer el modo de reproducción y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 14 | 0821.mp3|Modo historia. Reproduce una pista aleatoria de la carpeta. 15 | 0822.mp3|Modo Album. Reproduce la carpeta entera. 16 | 0823.mp3|Modo fiesta. Reproduce una carpeta completa al aleatoria. 17 | 0824.mp3|Modo simple. Reproduce solo una pista de la carpeta. 18 | 0825.mp3|Modo audiolibro. Reproduce la carpeta entera acordándome del progreso. 19 | 0826.mp3|Carpeta virtual, modo historia. Reproduce una pista aleatoria entre la primera y la última. 20 | 0827.mp3|Carpeta virtual, modo album. Reproduce todas las pistas entre la primera y la última. 21 | 0828.mp3|Carpeta virtual, modo fiesta. Reproduce de modo aleatorio todas las pistas entre la primera y la última. 22 | 0900.mp3|Menu para los padres. Usa los botones de volumen para escojer una opción y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 23 | 0901.mp3|Ajustes guardados. 24 | 0902.mp3|Progreso del audiolibro borrado. 25 | 0903.mp3|Ajustes borrados. 26 | 0904.mp3|Saliendo del menu de padres. 27 | 0910.mp3|Borra la tarjeta. 28 | 0911.mp3|Ajusta el volúmen inicial. 29 | 0912.mp3|Ajusta el volúmen máximo. 30 | 0913.mp3|Ajusta el volúmen de mi voz. 31 | 0914.mp3|Escoje el preset del ecualizador. 32 | 0915.mp3|Aprende el mando a distancia por infrarrojos. Necesitas un mando con almenos 7 botones. 33 | 0916.mp3|Ajusta el tiempo para auto apagarse. 34 | 0917.mp3|Resetea el progreso de un audiolibro. 35 | 0918.mp3|Resetea los ajustes. 36 | 0919.mp3|Apaga el sistema. 37 | 0920.mp3|Por favor pon la tarjeta encima y espera un mensaje afirmativo. 38 | 0921.mp3|Vale, tarjeta borrada. 39 | 0922.mp3|Ha habido un problema borrando la tarjeta. Por favor inténtalo de nuevo. 40 | 0923.mp3|Borrado de tarjeta cancelado. 41 | 0930.mp3|Usa los botones de volumen para escojer el volúmen inicial y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 42 | 0931.mp3|Usa los botones de volumen para escojer el volúmen máximo y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 43 | 0932.mp3|Usa los botones de volumen para escojer el volúmen de mi voz y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 44 | 0940.mp3|Usa los botones de volumen para ajustar el ecualizador y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 45 | 0941.mp3|Normal 46 | 0942.mp3|Pop 47 | 0943.mp3|Rock 48 | 0944.mp3|Jazz 49 | 0945.mp3|Clasica 50 | 0946.mp3|Bass 51 | 0950.mp3|Esta función necesita un mando a distancia, el cual necesita ser habilitado en el código del programa. 52 | 0951.mp3|Presiona en el mando el botón de más volúmen. 53 | 0952.mp3|Presiona en el mando el botón de menos volúmen. 54 | 0953.mp3|Presiona en el mando el botón de pista anterior. 55 | 0954.mp3|Presiona en el mando el botón de pista siguiente. 56 | 0955.mp3|Presiona en el mando el botón central. 57 | 0956.mp3|Presiona en el mando el botón menu. 58 | 0957.mp3|Presiona en el mando el botón de play/pausa. 59 | 0960.mp3|Usa los botones de volumen para escojer el timepo de autoapagado y confirma con el botón de pausa. Para cancelar, mantén presionado el botón de pausa. 60 | 0961.mp3|5 minutos. 61 | 0962.mp3|10 minutos. 62 | 0963.mp3|15 minutos. 63 | 0964.mp3|20 minutos. 64 | 0965.mp3|30 minutos. 65 | 0966.mp3|60 minutos. 66 | 0967.mp3|Desactiva el auto apagado. 67 | -------------------------------------------------------------------------------- /audio_messages_it.txt: -------------------------------------------------------------------------------- 1 | 0800.mp3|Iniziamo! 2 | 0801.mp3|È stata rilevata una nuova carta: lasciala sul box fino al termine della sua configurazione. Usa i pulsanti volume per selezionare una cartella e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per cancellare. 3 | 0802.mp3|Ok. Traccia singola. Usa i pulsanti volume per selezionare una traccia e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per annullare. 4 | 0803.mp3|Ok. Usa i pulsanti volume per selezionare una traccia iniziale dalla cartella virtuale e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per annullare. 5 | 0804.mp3|Ok. Usa i pulsanti volume per selezionare una traccia finale dalla cartella virtuale e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per annullare. 6 | 0805.mp3|Ok, la carta è configurata. Buon divertimento! 7 | 0806.mp3|Si è verificato un errore durante la configurazione della carta. Riprova. 8 | 0807.mp3|La configurazione della carta è stata annullata. 9 | 0808.mp3|Oooooops le mie batterie sono scariche, chiedi a mamma o babbo di ricaricarmi. Vado a nanna ora, ciao. 10 | 0809.mp3|Reset completato. 11 | 0810.mp3|Inserisci il PIN. 12 | 0811.mp3|Inserimento PIN annullato. 13 | 0820.mp3|Ok. Usa i pulsanti volume per selezionare una modalità di riproduzione e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per annullare. 14 | 0821.mp3|Modalità Story. Riproduce una traccia casuale dalla cartella. 15 | 0822.mp3|Modalità Album. Riproduce l'intera cartella. 16 | 0823.mp3|Modalità Party. Riproduce l'intera cartella in modalità casuale. 17 | 0824.mp3|Modalità Single. Riproduce una traccia specifica dalla cartella. 18 | 0825.mp3|Modalità Audiobook. Riproduce l'intera cartella tenendo traccia del progresso. 19 | 0826.mp3|Cartella virtuale, modalità Story. Riproduce una traccia casuale compresa tra quella iniziale e quella finale. 20 | 0827.mp3|Cartella virtuale, modalità Album. Riproduce tutte le tracce comprese tra quella iniziale e quella finale. 21 | 0828.mp3|Cartella virtuale, modalità Party. Riproduce, in modo casuale, tutte le tracce comprese tra quella iniziale e quella finale. 22 | 0900.mp3|Menu principale. Usa i pulsanti volume per selezionare un'opzione e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per cancellare. 23 | 0901.mp3|Impostazioni salvate. 24 | 0902.mp3|Reset del progresso Audiobook completato. 25 | 0903.mp3|Reset delle impostazioni completato. 26 | 0904.mp3|Uscita dal menu principale. 27 | 0910.mp3|Cancella la carta. 28 | 0911.mp3|Imposta volume iniziale. 29 | 0912.mp3|Imposta volume massimo. 30 | 0913.mp3|Imposta volume menu. 31 | 0914.mp3|Imposta equalizzatore. 32 | 0915.mp3|Telecomando infrarossi. Hai bisogno di un telecomando con almeno 7 tasti. 33 | 0916.mp3|Imposta un timer di spegnimento. 34 | 0917.mp3|Reset del progresso Audiobook. 35 | 0918.mp3|Reset delle impostazioni. 36 | 0919.mp3|Spegnimento box. 37 | 0920.mp3|Appoggia una carta sul box e attendi la conferma. 38 | 0921.mp3|Ok. Carta cancellata. 39 | 0922.mp3|Cancellazione della carta fallita. Riprova. 40 | 0923.mp3|Cancellazione della carta completata. 41 | 0930.mp3|Usa i pulsanti volume per selezionare il volume iniziale e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per annullare. 42 | 0931.mp3|Usa i pulsanti volume per selezionare il volume massimo e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per annullare. 43 | 0932.mp3|Usa i pulsanti volume per selezionare il volume del menu e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per annullare. 44 | 0940.mp3|Usa i pulsanti volume per selezionare l'equalizzatore e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per annullare. 45 | 0941.mp3|Normale 46 | 0942.mp3|Pop 47 | 0943.mp3|Rock 48 | 0944.mp3|Jazz 49 | 0945.mp3|Classica 50 | 0946.mp3|Base 51 | 0950.mp3|Questa funzione necessita di un telecomando infrarossi, che deve a sua volta essere abilitato nel codice del programma. 52 | 0951.mp3|Premi il tasto volume su del telecomando. 53 | 0952.mp3|Premi il tasto volume giù del telecomando. 54 | 0953.mp3|Premi il tasto traccia precedente del telecomando. 55 | 0954.mp3|Premi il tasto traccia successiva del telecomando. 56 | 0955.mp3|Premi il tasto centrale del telecomando. 57 | 0956.mp3|Premi il tasto menu del telecomando. 58 | 0957.mp3|Premi il tasto play/pausa del telecomando. 59 | 0960.mp3|Usa i pulsanti volume per selezionare il timer di spegnimento e conferma con il pulsante pausa. Tieni premuto il pulsante pausa per annullare. 60 | 0961.mp3|5 minuti. 61 | 0962.mp3|10 minuti. 62 | 0963.mp3|15 minuti. 63 | 0964.mp3|20 minuti. 64 | 0965.mp3|30 minuti. 65 | 0966.mp3|60 minuti. 66 | 0967.mp3|Disattiva il timer di spegnimento. 67 | -------------------------------------------------------------------------------- /audio_messages_nl.txt: -------------------------------------------------------------------------------- 1 | 0800.mp3|Daar gaan we! 2 | 0801.mp3|Een nieuwe kaart gevonden. Laat de kaart op de doos liggen, tot hij is geconfigureerd. Je kunt nu met de volume knoppen de map uitkiezen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 3 | 0802.mp3|Ok, lievelings volgorde. Je kunt nu met de volume toetsen een lievelingsvolgorde kiezen, en met de pause knop bevestigen. 4 | 0803.mp3|Ok. Virtuele map, je kunt nu met de volume toetsen een start plek kiezen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 5 | 0804.mp3|Ok. Je kunt nu met de volume toetsen een eind plek kiezen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 6 | 0805.mp3|Ok, de kaart is geconfigureerd, veel plezier ermee! 7 | 0806.mp3|Bij het configureren van de kaart is iets mis gegaan, probeer het opnieuw. 8 | 0807.mp3|Kaart configureren is afgebroken. 9 | 0808.mp3|Oh nee, mijn batterij is bijna op, vraag aan papa of mama om me op te laden, nu zet ik mezelf uit. 10 | 0809.mp3|Reset gelukt. 11 | 0810.mp3|Voer de PIN code in 12 | 0811.mp3|PIN code invoeren afgebroken. 13 | 0820.mp3|Ok. Je kunt nu de weergavemodus instellen met de volume toetsen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 14 | 0821.mp3|Verhaal modus. Ik kies een verhaaltje uit. 15 | 0822.mp3|Album modus. Ik speel het helemaal af. 16 | 0823.mp3|Feest. Ik speel alles lekker door elkaar. 17 | 0824.mp3|Lievelings liedje. Ik speel nu jouw lievelingsliedje af. 18 | 0825.mp3|Luisterboek modus. Ik speel alle hoofdstukken af. 19 | 0826.mp3|Virtuele map, in verhaal modus. Ik kies een verhaaltje uit. 20 | 0827.mp3|Virtuele map, album modus. Ik speel het helemaal af. 21 | 0828.mp3|Virtuele map, in feest modus. Ik speel alles lekker door elkaar. 22 | 0900.mp3|Papa en mama menu. Je kunt nu met de volume toetsen een optie kiezen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 23 | 0901.mp3|Instelling opgeslagen. 24 | 0902.mp3|Luisterboek voortgang gewist. 25 | 0903.mp3|Instellingen gewist. 26 | 0904.mp3|Ouder menu gestopt. 27 | 0910.mp3|Kaartje wissen. 28 | 0911.mp3|Start volume opslaan. 29 | 0912.mp3|Maximaal volume opslaan. 30 | 0913.mp3|Menu volume opslaan. 31 | 0914.mp3|Equalizer opslaan. 32 | 0915.mp3|Afstandsbediening leren. Je hebt een afstandsbediening nodig met tenminste 7 knoppen. 33 | 0916.mp3|Shutdown Timer opslaan. 34 | 0917.mp3|Wis luisterboek voortgang. 35 | 0918.mp3|Wis instellingen. 36 | 0919.mp3|Box uitzetten. 37 | 0920.mp3|Leg het kaartje wat je wilt wissen op de doos, en wacht op bevestiging. 38 | 0921.mp3|Ok, het kaartje is gewist. 39 | 0922.mp3|Bij het wissen van het kaartje is iets mis gegaan, probeer het opnieuw. 40 | 0923.mp3|Kaartje wissen afgebroken. 41 | 0930.mp3|Je kunt nu het startvolume kiezen met de volume toetsen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 42 | 0931.mp3|Je kunt nu het maximale volume kiezen met de volume toetsen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 43 | 0932.mp3|Je kunt nu het menu volume kiezen met de volume toetsen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 44 | 0940.mp3|Je kunt nu de equalizer instelling kiezen met de volume toetsen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 45 | 0941.mp3|Normaal 46 | 0942.mp3|Pop 47 | 0943.mp3|Rock 48 | 0944.mp3|Jazz 49 | 0945.mp3|Classic 50 | 0946.mp3|Bass 51 | 0950.mp3|Deze functie heeft een infrarood ontvanger nodig. 52 | 0951.mp3|Druk op de volume hoger knop op de afstandsbediening. Volume hoger knop. 53 | 0952.mp3|Druk op de volume lager knop op de afstandsbediening. Volume lager knop. 54 | 0953.mp3|Druk op de vorige titel knop op de afstandsbediening. Vorige titel knop. 55 | 0954.mp3|Druk op de volgende titel knop op de afstandsbediening. Volgende titel knop. 56 | 0955.mp3|Druk op de bevestiging knop op de afstandsbediening. Bevestiging knop. 57 | 0956.mp3|Druk op de menu knop op de afstandsbediening. Menu knop. 58 | 0957.mp3|Druk op de play/pauze knop op de afstandsbediening. Play/pause knop. 59 | 0960.mp3|Je kunt nu de shutdown timer tijd kiezen met de volume toetsen, en met de pause knop bevestigen. Je kunt afbreken door lang op de pause knop te drukken. 60 | 0961.mp3|5 Minuten. 61 | 0962.mp3|10 Minuten. 62 | 0963.mp3|15 Minuten. 63 | 0964.mp3|20 Minuten. 64 | 0965.mp3|30 Minuten. 65 | 0966.mp3|60 Minuten. 66 | 0967.mp3|Shutdown Timer deactiveren. 67 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | [For english, please click here!](README_EN.md) 2 | 3 | Alternative TonUINO Firmware 4 | ============================ 5 | 6 | Dies ist meine alternative Firmware für das wundervolle [TonUINO](https://www.voss.earth/tonuino/) Projekt. Ziel ist hier nicht unbedingt 100%ige Funktionsgleichheit mit der original Firmware. Es wurden vielmehr für meinen Zweck interessante Funktionen hinzugefügt und desweiteren auch einige Punkte (erstmal?) ausgelassen. Im großen und ganzen macht es einfach Spaß, sich mit der TonUINO Plattform auszutoben, ich bin deswegen auch viel in der [TonUINO Community](https://discourse.voss.earth/) aktiv. Schaut doch einfach mal vorbei - ihr findet dort viele Bauvorschläge, Hacks und Informationen rund um den TonUINO. 7 | 8 | **Die Firmware wird "as-is" zur Verfügung gestellt. Wenn jemand die Firmware in seinem TonUINO einsetzt, freue ich mich natürlich darüber. Ich kann allerdings keinen Support bieten.** 9 | 10 | ## Inhalt 11 | - [Funktionsübersicht](https://github.com/seisfeld/TonUINO#funktionsübersicht) 12 | - [Tastenbelegung](https://github.com/seisfeld/TonUINO#tastenbelegung) 13 | - [PIN Code](https://github.com/seisfeld/TonUINO#pin-code) 14 | - [Ordnerstruktur auf der SD Karte](https://github.com/seisfeld/TonUINO#ordnerstruktur-auf-der-sd-karte) 15 | - [Audio Meldungen](https://github.com/seisfeld/TonUINO#audio-meldungen) 16 | - [Audio Meldungen herunterladen](https://github.com/seisfeld/TonUINO#audio-meldungen-herunterladen) 17 | - [Audio Meldungen selbst erzeugen](https://github.com/seisfeld/TonUINO#audio-meldungen-selbst-erzeugen) 18 | - [Audio Meldungen mit say und ffmpeg erzeugen](https://github.com/seisfeld/TonUINO#audio-meldungen-mit-say-und-ffmpeg-erzeugen) 19 | - [Audio Meldungen mit Amazon Polly erzeugen](https://github.com/seisfeld/TonUINO#audio-meldungen-mit-amazon-polly-erzeugen) 20 | - [Audio Meldungen mit dem Cloud text-to-speech Service von Google erzeugen](https://github.com/seisfeld/TonUINO#audio-meldungen-mit-dem-cloud-text-to-speech-service-von-google-erzeugen) 21 | - [Hilfe und weitere Optionen](https://github.com/seisfeld/TonUINO#hilfe-und-weitere-optionen) 22 | - [Titelansagen in MP3-Dateien einfügen](https://github.com/seisfeld/TonUINO#titelansagen-in-mp3-dateien-einfügen) 23 | - [Funktionsweise](https://github.com/seisfeld/TonUINO#funktionsweise) 24 | - [Hilfe und weitere Optionen](https://github.com/seisfeld/TonUINO#hilfe-und-weitere-optionen-1) 25 | - [Lizenz](https://github.com/seisfeld/TonUINO#lizenz) 26 | 27 | ## Funktionsübersicht 28 | 29 | - Standard Abspielmodi: Hörspiel, Album, Party, Lieblingsfolge und Hörbuch. 30 | - Erweiterte Abspielmodi: Virtuelle Ordner für die Modi Hörspiel, Album und Party. 31 | - Nächster/Vorheriger Titel sowohl in den Standard Abspielmodi Album, Party und Hörbuch - als auch bei der Nutzung von virtuellen Ordnern in den Modi Album und Party. 32 | - Der aktuell laufende Titel kann dauerhaft wiederholt werden. 33 | - Speichert die Ordnerverknüpfungen, Abspielmodi etc. auf den NFC Tags/Karten. 34 | - Unterstützung für MIFARE Classic (Mini, 1K & 4K) Tags/Karten. 35 | - Unterstützung für MIFARE Ultralight / Ultralight C Tags/Karten. 36 | - Unterstützung für NTAG213/215/216 Tags/Karten. 37 | - Debugausgabe auf der seriellen Konsole. 38 | - Einstellungen werden im EEPROM gespeichert. 39 | - Konfigurationsdialoge (NFC Tags/Karten anlernen/löschen, Elternmenü etc.) können abgebrochen werden. 40 | - NFC Tags/Karten können wieder komplett gelöscht werden. 41 | - Elternmenü um NFC Tags/Karten zu löschen und um Einstellungen wie Startlautstärke, Maximallautstärke, Menülautstärke, Equalizer und Abschalttimer (benötigt eine externe Schaltung oder eine passende Powerbank) vorzunehmen. Dort kann TonUINO auch von Hand abgeschaltet werden und es lassen sich der Hörbuchfortschritt und die Einstellugen zurücksetzen. 42 | - **Optional:** PIN Code um Elternfunktionen zu schützen. 43 | - **Optional:** Umstellbar auf 5 Tasten Bedienung. 44 | - **Optional:** Fernbedienbar über eine Infrarotfernbedienung (diese muss mindestens 7 Tasten haben), welche über das Elternmenü angelernt werden kann. Über die Fernbedienung ist es dann auch möglich die Tasten von TonUINO zu sperren. 45 | - **Optional:** Unterstützung einer Status LED. 46 | - **Optional:** Unterstützung von WS281x LED(s) als Status LED(s). 47 | - **Optional:** Unterspannungsabschaltung für z.B. die [CubieKid Platine](https://www.thingiverse.com/thing:3148200). 48 | - **Optional:** Unterstützung des [Pololu Power Switch (LV)](https://www.pololu.com/product/2808). 49 | 50 | ## Tastenbelegung 51 | 52 | ![Tastenbelegung](usage_cheat_sheet_de.png) 53 | 54 | ## PIN Code 55 | 56 | Der (optional einschaltbare) PIN Code um die Elternfunktionen abzusichern lautet standard mässig 57 | 58 | - `play/pause, vol-, vol+, play/pause` 59 | 60 | und kann im Sketch vor dem kompilieren geändert werden. 61 | 62 | ## Ordnerstruktur auf der SD Karte 63 | 64 | Die Ordner auf der SD Karte, in denen eure MP3-Dateien abgelegt werden, müssen **01 bis 99** heissen - also **zweistellig** sein. Die Dateien in den Ordnern müssen mit einer **dreistelligen** Nummer beginnen - **001 bis 255** - können aber in der Regel [1] weitere Zeichen enthalten. Erlaubt wäre demnach `001.mp3` oder auch `001Lieblingslied.mp3`. 65 | 66 | Es hat sich bewährt, die gesammte Ordnerstruktur auf dem Comupter vorzubereiten und dann in einem Rutsch auf die SD Karte zu kopieren. So wird sichergestellt, daß alle Dateien auch in der richtigen Reihenfolge sind. 67 | 68 | [1] Nicht alle DFPlayer Mini Module akzeptieren im Dateinamen weitere Zeichen hinter der dreistelligen Nummer. Hier geht dann leider nur `001.mp3`, `002.mp3` usw. 69 | 70 | ## Audio Meldungen 71 | 72 | TonUINO funktioniert nur korrekt, wenn ein **zur Firmware passendes** Set an Audio Meldungen auf der SD Karte vorhanden ist. Dies sind die Ordner **advert** und **mp3**. 73 | 74 | ### Audio Meldungen herunterladen 75 | 76 | Die Audio Meldungen sind mit Amazon Polly generiert worden und können in verschiedenen Sprachen heruntergeladen werden: 77 | 78 | - Deutsch: [audio-messages-polly-de.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-de.zip) 79 | - Englisch: [audio-messages-polly-en.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-en.zip) 80 | - Niederländisch: [audio-messages-polly-nl.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-nl.zip) 81 | - Spanisch: [audio-messages-polly-es.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-es.zip) 82 | - Tschechisch: Kein Download verfügbar. 83 | - Italienisch: [audio-messages-polly-it.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-it.zip) 84 | 85 | Die `.zip` Datei entpacken und die Ordner **advert** und **mp3** auf die SD Karte kopieren. Fertig. 86 | 87 | ### Audio Meldungen selbst erzeugen 88 | 89 | Das passende Set an Audio Meldungen lässt sich auch jederzeit mit dem beigelegten Python Skript `create_audio_messages.py` erzeugen. Hier sind dann auch weitere text-to-speech Engines möglich wenn ihr möchtet (siehe unten). Das Skript kann sowohl **deutsche**, **englische**, **niederländische**, **spanische**, **tschechische** als auch **italienische** Audio Meldungen erzeugen (unterstützt wird ebenfalls **französisch**, es liegt dafür allerdings momentan keine Quelldatei bei). Es ist unter macOS, Linux und Windows getested, und benötigt **Python 3** und ggf. **ffmpeg**. 90 | 91 | Unter Linux kann Python 3 mit dem zur Distribution gehörenden Paketmanager installiert werden. macOS Nutzer benutzen z.B. [Homebrew](https://brew.sh): `brew install python` und für Windows kann es [hier heruntergeladen werden](https://www.python.org/downloads/windows/). 92 | 93 | Das Skript unerstützt dabei die Nutzung von drei text-to-speech Engines: 94 | 95 | - Lokal - nur unter macOS - mit den Tools `say` und `ffmpeg`. Sofern man einen Mac hat ist dieser Weg schnell, einfach und dauerhaft kostenlos. 96 | - Über das Internet mit Amazon Polly, dem text-to-speech Service von Amazon. 97 | - Über das Internet mit Hilfe des Cloud text-to-speech Service von Google. 98 | 99 | Für die Anzahl der benötigten Meldungen ist die Nutzung der Services von Amazon ([Preise](https://aws.amazon.com/de/polly/pricing/)) bzw. Google ([Preise](https://cloud.google.com/text-to-speech/pricing)) erstmal kostenlos. Für beide Services muss man allerdings erst einen Account anlegen, ist dann das Freikontingent irgendwann einmal aufgebraucht fallen Kosten im Bereich von ein paar Cent an. 100 | 101 | #### Audio Meldungen mit `say` und `ffmpeg` erzeugen 102 | 103 | Neben dem Tool `say` (ist Teil von macOS) wird hier noch `ffmpeg` benötigt. 104 | 105 | 1. `ffmpeg` installieren, z.B. via [Homebrew](https://brew.sh): `brew install ffmpeg` 106 | 2. In den Ordner wechseln wo ihr die `.zip` Datei von GitHub entpackt, bzw. das Repository gecloned habt. 107 | 3. `python3 tools/create_audio_messages.py --use-say` ausführen. 108 | 4. Kopiert nun den Inhalt des Ordners **sd-card** auf die SD Karte. Fertig. 109 | 110 | #### Audio Meldungen mit Amazon Polly erzeugen 111 | 112 | 1. Auf der [AWS](https://aws.amazon.com/) Webseite einen Account anlegen und Access Keys erzeugen. 113 | 2. Das Tool `aws` [installieren](https://docs.aws.amazon.com/de_de/cli/latest/userguide/cli-chap-install.html) (Windows / Linux), macOS z.B. via [Homebrew](https://brew.sh): `brew install awscli`. 114 | 3. Das Tool `aws` [konfigurieren](https://docs.aws.amazon.com/de_de/cli/latest/userguide/cli-chap-configure.html). 115 | 4. In den Ordner wechseln wo ihr die `.zip` Datei von GitHub entpackt, bzw. das Repository gecloned habt. 116 | 5. `python3 tools/create_audio_messages.py --use-amazon` ausführen. 117 | 6. Kopiert nun den Inhalt des Ordners **sd-card** auf die SD Karte. Fertig. 118 | 119 | #### Audio Meldungen mit dem Cloud text-to-speech Service von Google erzeugen 120 | 121 | 1. Auf Googles [Cloud text-to-speech](https://cloud.google.com/text-to-speech/) Webseite einen Account anlegen und einen API-Key erzeugen. 122 | 2. In den Ordner wechseln wo ihr die `.zip` Datei von GitHub entpackt, bzw. das Repository gecloned habt. 123 | 3. `python3 tools/create_audio_messages.py --use-google-key=ABCD` ausführen. 124 | 4. Kopiert nun den Inhalt des Ordners **sd-card** auf die SD Karte. Fertig. 125 | 126 | #### Hilfe und weitere Optionen 127 | 128 | Das Python Skript hat noch einige weitere Funktionen. Eine Übersicht gibt: 129 | 130 | - `python3 tools/create_audio_messages.py --help` 131 | 132 | ## Titelansagen in MP3-Dateien einfügen 133 | 134 | Im Hörspielmodus gibt es das Problem, daß man beim Auflegen der Karte nicht weiß, welche Folge abgespielt wird. Spielt man z.B. *Benjamin Blümchen* ab, dann kommt immer zuerst der Titelsong, der sich bei allen Folgen gleich anhört. 135 | 136 | Das Python Skript `add_lead_in_messages.py` fügt der MP3-Datei eine Titelansage wie z.B. *Benjamin Blümchen im Urlaub* hinzu. Wenn man eine andere Folge hören möchte, kann man dann einfach nochmal die *Benjamin Blümchen* Karte auflegen. Es ist unter macOS getested, sollte aber mit minimalem Aufwand auch unter Windows / Linux laufen - wenn alle Abhängigkeiten erfüllt werden. 137 | 138 | ### Funktionsweise 139 | 140 | Angenommen man hat einen Ordner mit folgendem Inhalt: 141 | 142 | ``` 143 | +- 04_Benjamin Blümchen 144 | +- Benjamin Blümchen hat Geburtstag.mp3 145 | +- Benjamin Blümchen im Urlaub.mp3 146 | +- Benjamin Blümchen als Pilot.mp3 147 | ``` 148 | 149 | Dann kann man mit folgendem Aufruf MP3-Dateien mit Ansagen generieren (Beispiel): 150 | 151 | python3 tools/add_lead_in_messages.py -i '04_Benjamin Blümchen' -o /Volumes/TonUINO/04 --google-key=ABCD --add-numbering 152 | 153 | Was dann passiert: 154 | 155 | - Es werden neue MP3-Dateien mit den Ansagen erzeugt. Man kann dabei auch optional direkt auf die SD-Karte schreiben (wie im Beispiel). 156 | - Die Original-Dateien werden dabei nicht geändert. 157 | - Die MP3-Dateien werden nicht neu enkodiert (also kein Qualitätsverlust). 158 | - Auf Wunsch werden die MP3-Dateien kompatibel zum DFPlayer Mini numeriert. Also z.B. `001_Benjamin Blümchen hat Geburtstag.mp3` (Parameter `--add-numbering`) 159 | - Das Skript unerstützt dabei die Nutzung von drei text-to-speech Engines: 160 | - Lokal - nur unter macOS - mit den Tools `say` und `ffmpeg`. (Parameter `--use-say`) 161 | - Über das Internet mit Amazon Polly, dem text-to-speech Service von Amazon. (Parameter `--use-amazon`) 162 | - Über das Internet mit Hilfe des Cloud text-to-speech Service von Google. (Parameter `--google-key=ABCD`) 163 | 164 | Das Ergebnis sieht dann so aus: 165 | 166 | ``` 167 | +- /Volumes/TonUINO/04 168 | +- 001_Benjamin Blümchen hat Geburtstag.mp3 169 | +- 002_Benjamin Blümchen im Urlaub.mp3 170 | +- 003_Benjamin Blümchen als Pilot.mp3 171 | ``` 172 | 173 | #### Hilfe und weitere Optionen 174 | 175 | Das Python Skript hat noch einige weitere Funktionen. Eine Übersicht gibt: 176 | 177 | - `python3 tools/add_lead_in_messages.py --help` 178 | 179 | ## Lizenz 180 | 181 | GPL v3. Siehe [LICENSE](../LICENSE.md). 182 | -------------------------------------------------------------------------------- /docs/README_EN.md: -------------------------------------------------------------------------------- 1 | Alternative TonUINO Firmware 2 | ============================ 3 | 4 | This is my alternative firmware for the wonderful [TonUINO](https://www.voss.earth/tonuino/) project. The goal of this firmware is not to implement 100% the same features as the original firmware. I more or less just added what fits my use case and i also removed (for now?) some stuff i currently don't really need. Overall it's just fun to play around with TonUINO, which is also the reason why i am very active in the [TonUINO Community](https://discourse.voss.earth/). Make sure you drop by, there is lots of stuff to discover. 5 | 6 | **The firmware is provided "as-is". I'm happy if all this is useful for anyone else, but can't offer support.** 7 | 8 | ## Table Of Contents 9 | - [Features](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#features) 10 | - [Button Cheat Sheet](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#button-cheat-sheet) 11 | - [PIN Code](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#pin-code) 12 | - [SD Card Folder Structure](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#sd-card-folder-structure) 13 | - [Audio Messages](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#audio-messages) 14 | - [Download the audio messages](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#download-the-audio-messages) 15 | - [Create the audio messages yourself](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#create-the-audio-messages-yourself) 16 | - [Create audio messages using say and ffmpeg](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#create-audio-messages-using-say-and-ffmpeg) 17 | - [Create audio messages using Amazon Polly](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#create-audio-messages-using-amazon-polly) 18 | - [Create audio messages using Googles text-to-speech service](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#create-audio-messages-using-googles-text-to-speech-service) 19 | - [Help and additional options](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#help-and-additional-options) 20 | - [Add Lead-In Messages To mp3 Files](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#add-lead-in-messages-to-mp3-files) 21 | - [How it works](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#how-it-works) 22 | - [Help and additional options](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#help-and-additional-options-1) 23 | - [License](https://github.com/seisfeld/TonUINO/blob/master/docs/README_EN.md#license) 24 | 25 | ## Features 26 | 27 | - Standard playback modes: Story, album, party, single, story book. 28 | - Advanced playback modes: Virtual folders for the modes story, album and party. 29 | - Next/previous track in standard modes album, party and story book mode - as well as when using virtual folders in album and party mode. 30 | - The currently playing track can be repeated indefinitely. 31 | - Saves playback modes etc. directly to the NFC tags/cards. 32 | - Supports MIFARE Classic (Mini, 1K & 4K) tags/cards. 33 | - Supports MIFARE Ultralight / Ultralight C tags/cards. 34 | - Supports NTAG213/215/216 tags/cards. 35 | - Debug output to the serial console. 36 | - Preferences are stored in EEPROM. 37 | - Setup dialogues (setup/erase NFC tags/cards, parents menu etc.) can be aborted. 38 | - NFC tags/cards can be erased. 39 | - Parents menu to erase NFC tags/cards and to change preferences like startup volume, maximum volume, menu volume, equalizer and shutdown timer (requires an external circuit or compatible power bank). You can also manually trigger the shutdown there and reset the story book progress and preferences. 40 | - **Optional:** PIN to protect parental functions. 41 | - **Optional:** 5 Buttons. 42 | - **Optional:** IR remote control (incl. box lock). The remote (which needs at least 7 keys) can be learned in using the parents menu. 43 | - **Optional:** Vanilla status LED. 44 | - **Optional:** WS281x status LED(s). 45 | - **Optional:** Low voltage shutdown i.e. for the [CubieKid PCB](https://www.thingiverse.com/thing:3148200). 46 | - **Optional:** [Pololu Power Switch (LV)](https://www.pololu.com/product/2808) support. 47 | 48 | ## Button Cheat Sheet 49 | 50 | ![Tastenbelegung](usage_cheat_sheet_en.png) 51 | 52 | ## PIN Code 53 | 54 | The (optional) PIN Code to secure the parental functions is by default 55 | 56 | - `play/pause, vol-, vol+, play/pause` 57 | 58 | and can be changed in the sketch before compile time. 59 | 60 | ## SD Card Folder Structure 61 | 62 | The folders on the SD card, that will hold your mp3 files, need to be named **01 bis 99** - that is **two digits**. The mp3 files in these folders need to start with a **three digit zero padded number** like **001 to 255**, but may [1] contain more characters afterwards. Allowed would be `001.mp3` or `001MyTune.mp3`. 63 | 64 | It has been proven benefitial to prepare the whole folder structure on the computer and then copy everything to the SD card in one go. That way it's made sure the order of the files is correct. 65 | 66 | [1] Not all derivates of the DFPlayer Mini module accept characters after the three digit number. In this case only `001.mp3`, `002.mp3` etc. would be allowed. 67 | 68 | ## Audio Messages 69 | 70 | TonUINO only functions correctly, when there is the correct (**as in matches the firmware**) set of audio messages on the SD card. These are the folders **advert** and **mp3**. 71 | 72 | ### Download the audio messages 73 | 74 | The audio messages have been generated with Amazon Polly and can be downloaded in several different languages: 75 | 76 | - German: [audio-messages-polly-de.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-de.zip) 77 | - English: [audio-messages-polly-en.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-en.zip) 78 | - Dutch: [audio-messages-polly-nl.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-nl.zip) 79 | - Spanish: [audio-messages-polly-es.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-es.zip) 80 | - Czech: No download available. 81 | - Italian: [audio-messages-polly-it.zip](https://seisfeld.github.io/tonuino/audio-messages-polly-it.zip) 82 | 83 | Extract the `.zip` file and copy the folders **advert** and **mp3** to the SD Card. Done. 84 | 85 | ### Create the audio messages yourself 86 | 87 | If you want to, you can as well create the matching set of audio messages yourself, using the `create_audio_messages.py` python script from this repo. This way then also offers different text-to-speech engines if you like (see below). The script can create **german**, **english**, **dutch**, **spanish**, **czech** and **italian** audio messages (**french** is also supported, but there is currently no source file included). It is tested on macOS, Linux and Windows, and requires `Python 3` and `ffmpeg`. 88 | 89 | Install Python 3 using your favorite package manager on Linux, macOS users can use [Homebrew](https://brew.sh): `brew install python` and for Windows it can be [downloaded here](https://www.python.org/downloads/windows/). 90 | 91 | The script is able utilize three text-to-speech engines: 92 | 93 | - Locally - on macOS only - using the tools `say` and `ffmpeg`. If you have a Mac this method is fast, simple and free of charge. 94 | - Over the internet using Amazon Polly, the text-to-speech service of Amazon. 95 | - Over the internet using Googles Cloud text-to-speech service. 96 | 97 | The amount of messages you need to create, is covered by the free tiers of the respective services - Amazon ([pricing](https://aws.amazon.com/de/polly/pricing/)) or Google ([pricing](https://cloud.google.com/text-to-speech/pricing)). You need to create an account for both and once the free tier is used up, the costs are just a few cents. 98 | 99 | #### Create audio messages using `say` and `ffmpeg` 100 | 101 | In addition to `say` (part of macOS) you also need `ffmpeg`. 102 | 103 | 1. Install `ffmpeg`, i.e. via [Homebrew](https://brew.sh): `brew install ffmpeg` 104 | 2. Change into the folder where you unzipped the `.zip` from GitHub or where you cloned the repo to. 105 | 3. Run `python3 tools/create_audio_messages.py --use-say --lang=en`. 106 | 4. Copy the contents of the folder **sd-card** to the SD Card. Done. 107 | 108 | #### Create audio messages using Amazon Polly 109 | 110 | 1. Go to the [AWS](https://aws.amazon.com/) website, create an account and the respective access keys. 111 | 2. [Install](https://docs.aws.amazon.com/en_us/cli/latest/userguide/cli-chap-install.html) (Windows / Linux) the `aws` command line tool. On macOS i.e. via [Homebrew](https://brew.sh): `brew install awscli`. 112 | 3. [Configure](https://docs.aws.amazon.com/en_us/cli/latest/userguide/cli-chap-configure.html) the the `aws` command line tool. 113 | 4. Change into the folder where you unzipped the `.zip` from GitHub or where you cloned the repo to. 114 | 5. Run `python3 tools/create_audio_messages.py --use-amazon --lang=en`. 115 | 6. Copy the contents of the folder **sd-card** to the SD Card. Done. 116 | 117 | #### Create audio messages using Googles text-to-speech service 118 | 119 | 1. Go to Googles [Cloud text-to-speech](https://cloud.google.com/text-to-speech/) website, create an account and API key. 120 | 2. Change into the folder where you unzipped the `.zip` from GitHub or where you cloned the repo to. 121 | 4. Run `python3 tools/create_audio_messages.py --use-google-key=ABCD --lang=en`. 122 | 5. Copy the contents of the folder **sd-card** to the SD Card. Done. 123 | 124 | #### Help and additional options 125 | 126 | The python script offers additional options, run the following command to get an overview: 127 | 128 | - `python3 tools/create_audio_messages.py --help` 129 | 130 | ## Add Lead-In Messages To mp3 Files 131 | 132 | In story mode there is the problem that when playing a card, one does not know which episode is played. If you play for example *Benjamin the Elephant*, then you'll always hear the title song first, which sounds the same in all episodes. 133 | 134 | The python script `add_lead_in_messages.py` adds a lead-in message to the mp3 file, such as *Benjamin the Elephant on vacation*. If you want to hear a different episode, then you can just show the *Benjamin the Elephant* card again. It is tested on macOS, but you should be able to run it on Windows / Linux with minimal effort - given you resolve the dependencies. 135 | 136 | ### How it works 137 | 138 | Suppose you have a folder with the following content: 139 | 140 | ``` 141 | +- 04_Benjamin the Elephant 142 | +- Benjamin the Elephant has his birthday.mp3 143 | +- Benjamin the Elephant on vacation.mp3 144 | +- Benjamin the Elephant as a pilot.mp3 145 | ``` 146 | 147 | Then you can use the following command to generate mp3 files with lead-in messages (example): 148 | 149 | python3 tools/add_lead_in_messages.py -i '04_Benjamin the Elephant' -o /Volumes/TonUINO/04 --google-key=ABCD --add-numbering 150 | 151 | What happened: 152 | 153 | - New mp3 files are generated including the lead-in messages. You can also write directly to the SD card (as in the example). 154 | - The original mp3 files are not touched. 155 | - The mp3 files are not re-encoded (so no quality loss). 156 | - Optionally, the mp3 files are numbered to be compatible with DFPlayer Mini. For example `001_Benjamin the Elephant has his birthday.mp3` (parameter `--add-numbering`) 157 | - The script is able utilize three text-to-speech engines: 158 | - Locally - on macOS only - using the tools `say` and `ffmpeg`. (parameter `--use-say`) 159 | - Over the internet using Amazon Polly, the text-to-speech service of Amazon. (parameter `--use-amazon`) 160 | - Over the internet using Googles Cloud text-to-speech service. (parameter `--google-key=ABCD`) 161 | 162 | The result looks like this: 163 | 164 | ``` 165 | +- /Volumes/TonUINO/04 166 | +- 001_Benjamin the Elephant has his birthday.mp3 167 | +- 002_Benjamin the Elephant on vacation.mp3 168 | +- 003_Benjamin the Elephant as a pilot.mp3 169 | ``` 170 | 171 | ### Help and additional options 172 | 173 | The python script offers additional options, run the following command to get an overview: 174 | 175 | - `python3 tools/add_lead_in_messages.py --help` 176 | 177 | ## License 178 | 179 | GPL v3. See [LICENSE](../LICENSE.md). -------------------------------------------------------------------------------- /docs/usage_cheat_sheet_de.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seisfeld/TonUINO/c773bb73971ba571a917b7db7385f14b6351d612/docs/usage_cheat_sheet_de.pdf -------------------------------------------------------------------------------- /docs/usage_cheat_sheet_de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seisfeld/TonUINO/c773bb73971ba571a917b7db7385f14b6351d612/docs/usage_cheat_sheet_de.png -------------------------------------------------------------------------------- /docs/usage_cheat_sheet_en.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seisfeld/TonUINO/c773bb73971ba571a917b7db7385f14b6351d612/docs/usage_cheat_sheet_en.pdf -------------------------------------------------------------------------------- /docs/usage_cheat_sheet_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seisfeld/TonUINO/c773bb73971ba571a917b7db7385f14b6351d612/docs/usage_cheat_sheet_en.png -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | src_dir = . 3 | 4 | [env:nanoatmega328] 5 | platform = atmelavr 6 | board = nanoatmega328 7 | framework = arduino 8 | monitor_speed = 9600 9 | 10 | lib_deps = 11 | miguelbalboa/MFRC522 12 | makuna/DFPlayer Mini Mp3 by Makuna @ 1.0.7 13 | bxparks/AceButton 14 | https://github.com/z3t0/Arduino-IRremote 15 | https://github.com/cpldcpu/light_ws2812 16 | https://github.com/Yveaux/Arduino_Vcc 17 | 18 | build_flags = 19 | ; uncomment the below line to enable five button support 20 | ; -D FIVEBUTTONS 21 | 22 | ; uncomment the below line to enable ir remote support 23 | ; -D IRREMOTE 24 | 25 | ; uncomment the below line to enable pin code support 26 | ; -D PINCODE 27 | 28 | ; uncomment ONE OF THE BELOW TWO LINES to enable status led support 29 | ; the first enables support for a vanilla led 30 | ; the second enables support for ws281x led(s) 31 | ; -D STATUSLED 32 | ; -D STATUSLEDRGB 33 | 34 | ; uncomment the below line to enable low voltage shutdown support 35 | ; -D LOWVOLTAGE 36 | 37 | ; uncomment the below line to flip the shutdown pin logic 38 | ; -D POLOLUSWITCH 39 | -------------------------------------------------------------------------------- /tonuino.ino: -------------------------------------------------------------------------------- 1 | /* 2 | basic button actions: 3 | ===================== 4 | 5 | Button B0 (by default pin A0, middle button on the original TonUINO): play/pause 6 | Button B1 (by default pin A1, right button on the original TonUINO): volume up 7 | Button B2 (by default pin A2, left button on the original TonUINO): volume down 8 | Button B3 (by default pin A3, optional): previous track 9 | Button B4 (by default pin A4, optional): next track 10 | 11 | additional button actions: 12 | ========================== 13 | 14 | During idle: 15 | ------------ 16 | Hold B0 for 5 seconds - Enter parents menu 17 | 18 | During playback: 19 | ---------------- 20 | Hold B0 for 5 seconds - Reset progress to track 1 (story book mode) 21 | Hold B0 for 5 seconds - Single track repeat (all modes, except story book mode) 22 | Hold B1 for 2 seconds - Skip to the next track ((v)album, (v)party and story book mode) 23 | Hold B2 for 2 seconds - Skip to the previous track ((v)album, (v)party and story book mode) 24 | 25 | During parents menu: 26 | -------------------- 27 | Click B0 - Confirm selection 28 | Click B1 - Next option 29 | Click B2 - Previous option 30 | Click B3 - Jump 10 options backwards 31 | Click B4 - Jump 10 options forward 32 | Double click B0 - Announce current option 33 | Hold B0 for 2 seconds - Cancel parents menu or any submenu 34 | Hold B1 for 2 seconds - Jump 10 options forward 35 | Hold B2 for 2 seconds - Jump 10 options backwards 36 | 37 | During nfc tag setup mode: 38 | -------------------------- 39 | Click B0 - Confirm selection 40 | Click B1 - Next folder, mode or track 41 | Click B2 - Previous folder, mode or track 42 | Click B3 - Jump 10 folders or tracks backwards 43 | Click B4 - Jump 10 folders or tracks forward 44 | Double click B0 - Announce current folder, mode or track number 45 | Hold B0 for 2 seconds - Cancel nfc tag setup mode 46 | Hold B1 for 2 seconds - Jump 10 folders or tracks forward 47 | Hold B2 for 2 seconds - Jump 10 folders or tracks backwards 48 | 49 | During power up: 50 | ---------------- 51 | 52 | Hold B0 + B1 + B2 - Erase all eeprom contents (resets stored progress and preferences) 53 | 54 | ir remote: 55 | ========== 56 | 57 | If an ir receiver (like TSOP38238 or similar) is connected to pin 5, you can also use 58 | an ir remote to remote control TonUINO. The remote needs at least seven buttons to be 59 | able to support all functions. This feature can be enabled by uncommenting the 60 | '#define IRREMOTE' below. 61 | 62 | Down below you can hard code (multiple sets of) code mappings for remotes which will 63 | always be recognized. In addition to these hard coded remotes, you can learn in one 64 | additional remote using the parents menu, which is then stored in EEPROM. 65 | 66 | There is one function, currently only available with the ir remote: Box lock. 67 | When TonUINO is locked, the buttons on TonUINO as well as the nfc reader are disabled 68 | until TonUINO is unlocked again. Playback continues while TonUINO is locked and 69 | you are still able to control TonUINO using the ir remote (great for parents). 70 | 71 | During idle: 72 | ------------ 73 | center - toggle box lock 74 | menu - enter parents menu 75 | 76 | During playback: 77 | ---------------- 78 | center - toggle box lock 79 | play/pause - toggle playback 80 | up / down - volume up / down 81 | left / right - previous / next track ((v)album, (v)party and story book mode) 82 | menu - reset progress to track 1 (story book mode) 83 | menu - single track repeat (all modes, except story book mode) 84 | 85 | During parents menu: 86 | -------------------- 87 | center - announce current option 88 | play/pause - confirm selection 89 | up / down - next / previous option 90 | left / right - jump 10 options backwards / forward 91 | menu - cancel 92 | 93 | During nfc tag setup mode: 94 | -------------------------- 95 | center - announce current folder, mode or track number 96 | play/pause - confirm selection 97 | up / down - next / previous folder, mode or track 98 | left / right - jump 10 folders or tracks backwards / forward 99 | menu - cancel 100 | 101 | pin code: 102 | ========= 103 | 104 | The complete erase of the eeprom contents (hold vol-, play/pause, vol+ during startup) 105 | as well as the parents menu can be secured with a pin code of variable length. It is 106 | defined (and can be changed) in the configuration section below, the default pin code is: 107 | 108 | ===> play/pause, vol-, vol+, play/pause <=== 109 | 110 | Once the pin code entry is triggered, you have (by default) 10s to enter the pin code 111 | before it times out. It repeats when entered incorrectly. See status led section 112 | for information about visual feedback. This feature can be enabled by uncommenting 113 | the '#define PINCODE' below. 114 | 115 | status led(s): 116 | ============== 117 | 118 | There are two options for a status led (which are mutually exclusive!): 119 | 120 | 1) Connect a vanilla led to pin 6 and uncomment the '#define STATUSLED' below. 121 | TonUINO will signal various status information, for example: 122 | 123 | - Pulse slowly when TonUINO is idle. 124 | - Solid when TonUINO is playing a title. 125 | - Blink every 500ms when interactive in menus etc. 126 | - Blink every 100ms if the LOWVOLTAGE feature is active and the battery is low. 127 | - Burst 4 times when TonUINO is locked and 8 times when unlocked. 128 | - Burst 4 times when repeat is activated and 8 times when deactivated. 129 | 130 | There are some more signals, they try to be intuitive. You'll see. 131 | 132 | 2) Connect one (or even several) ws281x rgb led(s) to pin 6 and uncomment 133 | the '#define STATUSLEDRGB' below. TonUINO will signal various status information 134 | in different patterns and colors, for example: 135 | 136 | - Pulse slowly green when TonUINO is idle. 137 | - Solid green when TonUINO is playing a title. 138 | - Blink yellow every 500ms when interactive in menus etc. 139 | - Blink red every 100ms if the LOWVOLTAGE feature is active and the battery is low. 140 | - Burst 4 times red when TonUINO is locked and 8 times green when unlocked. 141 | - Burst 4 times white when repeat is activated and 8 times white when deactivated. 142 | - Burst 8 times magenta when the track in story book mode is reset to 1. 143 | 144 | There are some more signals, they try to be intuitive. You'll see. 145 | 146 | For the vanilla led basically any 5V led will do, just don't forget to put an appropriate 147 | resistor in series. For the ws281x led(s) you have several options. Stripes, single 148 | neo pixels etc. The author did test the fuctionality with an 'addressable Through-Hole 5mm RGB LED' 149 | from Pololu [1]. Please make sure you put a capacitor of at least 10 uF between the ground 150 | and power lines of the led and consider adding a 100 to 1k ohm resistor between pin 6 151 | and the led's data in. In general make sure you have enough current available (especially if 152 | you plan more than one led - each takes up to 60mA!) and don't source the current for the led(s) 153 | from the arduino power rail! Consult your ws281x led vendors documentation for guidance! 154 | 155 | The ammount of ws281x led(s) as well as the max brightness can be set in the configuration 156 | section below. The defaults are: One led and 20% brightness. 157 | 158 | [1] https://www.pololu.com/product/2535 159 | 160 | cubiekid: 161 | ========= 162 | 163 | If you happen to have a CubieKid case and the CubieKid circuit board, this firmware 164 | supports both shutdown methods. The inactivity shutdown timer is enabled by default, 165 | the shutdown due to low battery voltage (which can be configured in the shutdown 166 | section below) can be enabled by uncommenting the '#define LOWVOLTAGE' below. 167 | 168 | The CubieKid case as well as the CubieKid circuit board, have been designed and developed 169 | by Jens Hackel aka DB3JHF and can be found here: https://www.thingiverse.com/thing:3148200 170 | 171 | pololu switch: 172 | ============== 173 | 174 | If you want to use a pololu switch with this firmware the shutdown pin logic needs 175 | to be flipped from HIGH (on) -> LOW (off) to LOW (on) -> HIGH (off). This can be done 176 | by uncommenting the '#define POLOLUSWITCH' below. 177 | 178 | data stored on the nfc tags: 179 | ============================ 180 | 181 | On MIFARE Classic (Mini, 1K & 4K) tags: 182 | --------------------------------------- 183 | 184 | Up to 16 bytes of data are stored in sector 1 / block 4, of which the first 9 bytes 185 | are currently in use: 186 | 187 | 13 37 B3 47 01 02 04 10 19 00 00 00 00 00 00 00 188 | ----------- -- -- -- -- -- 189 | | | | | | | 190 | | | | | | +- end track (0x01-0xFF - in vstory (0x06), valbum (0x07) and vparty (0x08) modes) 191 | | | | | +- single/start track (0x01-0xFF - in single (0x04), vstory (0x06), valbum (0x07) and vparty (0x08) modes) 192 | | | | +- playback mode (0x01-0x08) 193 | | | +- folder (0x01-0x63) 194 | | +- version (currently always 0x01) 195 | +- magic cookie to recognize that a card belongs to TonUINO (by default 0x13 0x37 0xb3 0x47) 196 | 197 | On MIFARE Ultralight / Ultralight C and NTAG213/215/216 tags: 198 | ------------------------------------------------------------- 199 | 200 | Up to 16 bytes of data are stored in pages 8-11, of which the first 9 bytes 201 | are currently in use: 202 | 203 | 8 13 37 B3 47 - magic cookie to recognize that a card belongs to TonUINO (by default 0x13 0x37 0xb3 0x47) 204 | 9 01 02 04 10 - version, folder, playback mode, single track / start strack 205 | 10 19 00 00 00 - end track 206 | 11 00 00 00 00 207 | 208 | additional non standard libraries used in this firmware: 209 | ======================================================== 210 | 211 | MFRC522.h - https://github.com/miguelbalboa/rfid 212 | DFMiniMp3.h - https://github.com/Makuna/DFMiniMp3 213 | AceButton.h - https://github.com/bxparks/AceButton 214 | IRremote.h - https://github.com/z3t0/Arduino-IRremote 215 | WS2812.h - https://github.com/cpldcpu/light_ws2812 216 | Vcc.h - https://github.com/Yveaux/Arduino_Vcc 217 | */ 218 | 219 | // uncomment the below line to enable five button support 220 | // #define FIVEBUTTONS 221 | 222 | // uncomment the below line to enable ir remote support 223 | // #define IRREMOTE 224 | 225 | // uncomment the below line to enable pin code support 226 | // #define PINCODE 227 | 228 | // uncomment ONE OF THE BELOW TWO LINES to enable status led support 229 | // the first enables support for a vanilla led 230 | // the second enables support for ws281x led(s) 231 | // #define STATUSLED 232 | // #define STATUSLEDRGB 233 | 234 | // uncomment the below line to enable low voltage shutdown support 235 | // #define LOWVOLTAGE 236 | 237 | // uncomment the below line to flip the shutdown pin logic 238 | // #define POLOLUSWITCH 239 | 240 | // include required libraries 241 | #include 242 | #include 243 | #include 244 | #include 245 | #include 246 | #include 247 | #include 248 | using namespace ace_button; 249 | 250 | // include additional library if ir remote support is enabled 251 | #if defined IRREMOTE 252 | #include 253 | #endif 254 | 255 | // include additional library if ws281x status led support is enabled 256 | #if defined STATUSLED ^ defined STATUSLEDRGB 257 | #if defined STATUSLEDRGB 258 | #include 259 | #endif 260 | #endif 261 | 262 | // include additional library if low voltage shutdown support is enabled 263 | #if defined LOWVOLTAGE 264 | #include 265 | #endif 266 | 267 | // playback modes 268 | enum {NOMODE, STORY, ALBUM, PARTY, SINGLE, STORYBOOK, VSTORY, VALBUM, VPARTY}; 269 | 270 | // button actions 271 | enum {NOP, 272 | B0P, B1P, B2P, B3P, B4P, 273 | B0H, B1H, B2H, B3H, B4H, 274 | B0D, B1D, B2D, B3D, B4D, 275 | IRU, IRD, IRL, IRR, IRC, IRM, IRP 276 | }; 277 | 278 | // button modes 279 | enum {INIT, PLAY, PAUSE, PIN, CONFIG}; 280 | 281 | // shutdown timer actions 282 | enum {START, STOP, CHECK, SHUTDOWN}; 283 | 284 | // preference actions 285 | enum {READ, WRITE, MIGRATE, RESET, RESET_PROGRESS}; 286 | 287 | // status led actions 288 | enum {OFF, SOLID, PULSE, BLINK, BURST2, BURST4, BURST8}; 289 | 290 | // define general configuration constants 291 | const uint8_t mp3SerialRxPin = 2; // mp3 serial rx, wired to tx pin of DFPlayer Mini 292 | const uint8_t mp3SerialTxPin = 3; // mp3 serial tx, wired to rx pin of DFPlayer Mini 293 | const uint8_t mp3BusyPin = 4; // reports play state of DFPlayer Mini (LOW = playing) 294 | #if defined IRREMOTE 295 | const uint8_t irReceiverPin = 5; // pin used for the ir receiver 296 | #endif 297 | #if defined STATUSLED ^ defined STATUSLEDRGB 298 | const uint8_t statusLedPin = 6; // pin used for vanilla status led or ws281x status led(s) 299 | const uint8_t statusLedCount = 1; // number of ws281x status led(s) 300 | const uint8_t statusLedMaxBrightness = 20; // max brightness of ws281x status led(s) (in percent) 301 | #endif 302 | const uint8_t shutdownPin = 7; // pin used to shutdown the system 303 | const uint8_t nfcResetPin = 9; // used for spi communication to nfc module 304 | const uint8_t nfcSlaveSelectPin = 10; // used for spi communication to nfc module 305 | const uint8_t button0Pin = A0; // middle button 306 | const uint8_t button1Pin = A1; // right button 307 | const uint8_t button2Pin = A2; // left button 308 | #if defined FIVEBUTTONS 309 | const uint8_t button3Pin = A3; // optional 4th button 310 | const uint8_t button4Pin = A4; // optional 5th button 311 | #endif 312 | const uint16_t buttonClickDelay = 1000; // time during which a button press is still a click (in milliseconds) 313 | const uint16_t buttonShortLongPressDelay = 2000; // time after which a button press is considered a long press (in milliseconds) 314 | const uint16_t buttonLongLongPressDelay = 5000; // longer long press delay for special cases, i.e. to trigger the parents menu (in milliseconds) 315 | const uint32_t debugConsoleSpeed = 9600; // speed for the debug console 316 | 317 | // define magic cookie (by default 0x13 0x37 0xb3 0x47) 318 | const uint8_t magicCookieHex[4] = {0x13, 0x37, 0xb3, 0x47}; 319 | 320 | #if defined PINCODE 321 | // define pin code, allowed enums for pinCode[]: B0P, B1P, B2P (plus B3P & B4P if FIVEBUTTONS is enabled) 322 | const uint8_t pinCode[] = {B0P, B2P, B1P, B0P}; // for example play/pause, vol-, vol+, play/pause 323 | const uint8_t pinCodeLength = sizeof(pinCode); 324 | const uint8_t pinCodeIrToButtonMapping[] = {B1P, B2P, B3P, B4P, NOP, IRM, B0P}; 325 | const uint64_t enterPinCodeTimeout = 10000; // time to enter the pin code (in milliseconds) 326 | #endif 327 | 328 | // default values for preferences 329 | const uint8_t preferenceVersion = 1; 330 | const uint8_t mp3StartVolumeDefault = 15; 331 | const uint8_t mp3MaxVolumeDefault = 25; 332 | const uint8_t mp3MenuVolumeDefault = 15; 333 | const uint8_t mp3EqualizerDefault = 1; 334 | const uint8_t shutdownMinutesDefault = 10; 335 | const uint16_t irRemoteUserCodesDefault[7] = {}; 336 | 337 | /* 338 | define hard coded (sets of) code mappings for ir remotes. 339 | one remote per line with the following order of codes for: up, down, left, right, center, menu, play/pause 340 | 341 | when adding multiple remotes, the array needs to look like this (watch the 'commas'!): 342 | 343 | const uint16_t irRemoteCodes[][7] = { 344 | {...}, 345 | {...}, 346 | {...} 347 | }; 348 | */ 349 | const uint16_t irRemoteCodes[][7] = { 350 | {0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000} // example, replace 0x0000 with respective codes as per above 351 | }; 352 | const uint8_t irRemoteCount = sizeof(irRemoteCodes) / 14; 353 | const uint8_t irRemoteCodeCount = sizeof(irRemoteCodes) / (2 * irRemoteCount); 354 | 355 | #if defined LOWVOLTAGE 356 | // define constants for shutdown feature 357 | const float shutdownMinVoltage = 4.4; // minimum expected voltage level (in volts) 358 | const float shutdownWarnVoltage = 4.8; // warning voltage level (in volts) 359 | const float shutdownMaxVoltage = 5.0; // maximum expected voltage level (in volts) 360 | const float shutdownVoltageCorrection = 1.0 / 1.0; // voltage measured by multimeter divided by reported voltage 361 | #endif 362 | 363 | // define strings 364 | const char *playbackModeName[] = {" ", "story", "album", "party", "single", "storybook", "vstory", "valbum", "vparty"}; 365 | const char *nfcStatusMessage[] = {" ", "read", "write", "ok", "failed"}; 366 | const char *mp3EqualizerName[] = {" ", "normal", "pop", "rock", "jazz", "classic", "bass"}; 367 | 368 | // this struct stores nfc tag data 369 | struct nfcTagStruct { 370 | uint32_t cookie = 0; 371 | uint8_t version = 0; 372 | uint8_t folder = 0; 373 | uint8_t mode = 0; 374 | uint8_t multiPurposeData1 = 0; 375 | uint8_t multiPurposeData2 = 0; 376 | }; 377 | 378 | // this struct stores playback state 379 | struct playbackStruct { 380 | bool isLocked = false; 381 | bool isPlaying = false; 382 | bool isFresh = true; 383 | bool isRepeat = false; 384 | bool playListMode = false; 385 | uint8_t mp3CurrentVolume = 0; 386 | uint8_t folderStartTrack = 0; 387 | uint8_t folderEndTrack = 0; 388 | uint8_t playList[255] = {}; 389 | uint8_t playListItem = 0; 390 | uint8_t playListItemCount = 0; 391 | nfcTagStruct currentTag; 392 | }; 393 | 394 | // this struct stores preferences 395 | struct preferenceStruct { 396 | uint32_t cookie = 0; 397 | uint8_t version = 0; 398 | uint8_t mp3StartVolume = 0; 399 | uint8_t mp3MaxVolume = 0; 400 | uint8_t mp3MenuVolume = 0; 401 | uint8_t mp3Equalizer = 0; 402 | uint8_t shutdownMinutes = 0; 403 | uint16_t irRemoteUserCodes[7] = {}; 404 | }; 405 | 406 | // global variables 407 | uint8_t inputEvent = NOP; 408 | uint32_t magicCookie = 0; 409 | uint32_t preferenceCookie = 0; 410 | playbackStruct playback; 411 | preferenceStruct preference; 412 | 413 | // ################################################################################################################################################################ 414 | // ############################################################### no configuration below this line ############################################################### 415 | // ################################################################################################################################################################ 416 | 417 | // declare functions 418 | void checkForInput(); 419 | void translateButtonInput(AceButton *button, uint8_t eventType, uint8_t /* buttonState */); 420 | void switchButtonConfiguration(uint8_t buttonMode); 421 | void waitPlaybackToFinish(uint8_t red, uint8_t green, uint8_t blue, uint16_t statusLedUpdateInterval); 422 | void printModeFolderTrack(bool cr); 423 | void playNextTrack(uint16_t globalTrack, bool directionForward, bool triggeredManually); 424 | uint8_t readNfcTagData(); 425 | uint8_t writeNfcTagData(uint8_t nfcTagWriteBuffer[], uint8_t nfcTagWriteBufferSize); 426 | void printNfcTagData(uint8_t dataBuffer[], uint8_t dataBufferSize, bool cr); 427 | void printNfcTagType(MFRC522::PICC_Type nfcTagType); 428 | void shutdownTimer(uint8_t timerAction); 429 | void preferences(uint8_t preferenceAction); 430 | uint8_t prompt(uint8_t promptOptions, uint16_t promptHeading, uint16_t promptOffset, uint8_t promptCurrent, uint8_t promptFolder, bool promptPreview, bool promptChangeVolume); 431 | void parentsMenu(); 432 | #if defined PINCODE 433 | bool enterPinCode(); 434 | #endif 435 | #if defined STATUSLED ^ defined STATUSLEDRGB 436 | void statusLedUpdate(uint8_t statusLedAction, uint8_t red, uint8_t green, uint8_t blue, uint16_t statusLedUpdateInterval); 437 | void statusLedUpdateHal(uint8_t red, uint8_t green, uint8_t blue, int16_t brightness); 438 | #endif 439 | 440 | class Mp3Notify; // forward declare the notify class, just the name 441 | 442 | SoftwareSerial mp3Serial(mp3SerialRxPin, mp3SerialTxPin); // create SoftwareSerial instance 443 | typedef DFMiniMp3 DfMp3; // define a type using serial and the notify class 444 | DfMp3 mp3(mp3Serial); // create DfMp3 instance 445 | MFRC522 mfrc522(nfcSlaveSelectPin, nfcResetPin); // create MFRC522 instance 446 | ButtonConfig button0Config; // create ButtonConfig instance 447 | ButtonConfig button1Config; // create ButtonConfig instance 448 | ButtonConfig button2Config; // create ButtonConfig instance 449 | AceButton button0(&button0Config); // create AceButton instance 450 | AceButton button1(&button1Config); // create AceButton instance 451 | AceButton button2(&button2Config); // create AceButton instance 452 | #if defined FIVEBUTTONS 453 | ButtonConfig button3Config; // create ButtonConfig instance 454 | ButtonConfig button4Config; // create ButtonConfig instance 455 | AceButton button3(&button3Config); // create AceButton instance 456 | AceButton button4(&button4Config); // create AceButton instance 457 | #endif 458 | 459 | #if defined STATUSLED ^ defined STATUSLEDRGB 460 | #if defined STATUSLEDRGB 461 | WS2812 rgbLed(statusLedCount); // create WS2812 instance 462 | #endif 463 | #endif 464 | 465 | #if defined LOWVOLTAGE 466 | Vcc shutdownVoltage(shutdownVoltageCorrection); // create Vcc instance 467 | #endif 468 | 469 | // used by DFPlayer Mini library during callbacks 470 | class Mp3Notify { 471 | public: 472 | static void OnError([[maybe_unused]] DfMp3& mp3, uint16_t returnValue) { 473 | switch (returnValue) { 474 | case DfMp3_Error_Busy: { 475 | Serial.print(F("busy")); 476 | break; 477 | } 478 | case DfMp3_Error_Sleeping: { 479 | Serial.print(F("sleep")); 480 | break; 481 | } 482 | case DfMp3_Error_SerialWrongStack: { 483 | Serial.print(F("serial stack")); 484 | break; 485 | } 486 | case DfMp3_Error_CheckSumNotMatch: { 487 | Serial.print(F("checksum")); 488 | break; 489 | } 490 | case DfMp3_Error_FileIndexOut: { 491 | Serial.print(F("file index")); 492 | break; 493 | } 494 | case DfMp3_Error_FileMismatch: { 495 | Serial.print(F("file mismatch")); 496 | break; 497 | } 498 | case DfMp3_Error_Advertise: { 499 | Serial.print(F("advertise")); 500 | break; 501 | } 502 | case DfMp3_Error_RxTimeout: { 503 | Serial.print(F("rx timeout")); 504 | break; 505 | } 506 | case DfMp3_Error_PacketSize: { 507 | Serial.print(F("packet size")); 508 | break; 509 | } 510 | case DfMp3_Error_PacketHeader: { 511 | Serial.print(F("packet header")); 512 | break; 513 | } 514 | case DfMp3_Error_PacketChecksum: { 515 | Serial.print(F("packet checksum")); 516 | break; 517 | } 518 | case DfMp3_Error_General: { 519 | Serial.print(F("general")); 520 | break; 521 | } 522 | default: { 523 | Serial.print(F("unknown")); 524 | break; 525 | } 526 | } 527 | Serial.println(F(" error")); 528 | } 529 | static void PrintlnSourceAction(DfMp3_PlaySources source, const char* action) { 530 | if (source & DfMp3_PlaySources_Sd) Serial.print("sd "); 531 | if (source & DfMp3_PlaySources_Usb) Serial.print("usb "); 532 | if (source & DfMp3_PlaySources_Flash) Serial.print("flash "); 533 | Serial.println(action); 534 | } 535 | static void OnPlayFinished([[maybe_unused]] DfMp3& mp3, [[maybe_unused]] DfMp3_PlaySources source, uint16_t returnValue) { 536 | playNextTrack(returnValue, true, false); 537 | } 538 | static void OnPlaySourceOnline([[maybe_unused]] DfMp3& mp3, DfMp3_PlaySources source) { 539 | PrintlnSourceAction(source, "online"); 540 | } 541 | static void OnPlaySourceInserted([[maybe_unused]] DfMp3& mp3, DfMp3_PlaySources source) { 542 | PrintlnSourceAction(source, "in"); 543 | } 544 | static void OnPlaySourceRemoved([[maybe_unused]] DfMp3& mp3, DfMp3_PlaySources source) { 545 | PrintlnSourceAction(source, "out"); 546 | } 547 | }; 548 | 549 | void setup() { 550 | // things we need to do immediately on startup 551 | pinMode(shutdownPin, OUTPUT); 552 | #if defined POLOLUSWITCH 553 | digitalWrite(shutdownPin, LOW); 554 | #else 555 | digitalWrite(shutdownPin, HIGH); 556 | #endif 557 | magicCookie = (uint32_t)magicCookieHex[0] << 24; 558 | magicCookie += (uint32_t)magicCookieHex[1] << 16; 559 | magicCookie += (uint32_t)magicCookieHex[2] << 8; 560 | magicCookie += (uint32_t)magicCookieHex[3]; 561 | preferenceCookie = (uint32_t)magicCookieHex[2] << 24; 562 | preferenceCookie += (uint32_t)magicCookieHex[3] << 16; 563 | preferenceCookie += (uint32_t)magicCookieHex[0] << 8; 564 | preferenceCookie += (uint32_t)magicCookieHex[1]; 565 | 566 | // start normal operation 567 | Serial.begin(debugConsoleSpeed); 568 | Serial.println(F("\n\nTonUINO JUKEBOX")); 569 | Serial.println(F("by Thorsten Voß")); 570 | Serial.println(F("Stephan Eisfeld")); 571 | Serial.println(F("and many others")); 572 | Serial.println(F("---------------")); 573 | Serial.println(F("flashed")); 574 | Serial.print(F(" ")); 575 | Serial.println(__DATE__); 576 | Serial.print(F(" ")); 577 | Serial.println(__TIME__); 578 | 579 | preferences(READ); 580 | 581 | Serial.println(F("init nfc")); 582 | SPI.begin(); 583 | mfrc522.PCD_Init(); 584 | mfrc522.PCD_DumpVersionToSerial(); 585 | 586 | Serial.println(F("init mp3")); 587 | mp3.begin(); 588 | delay(2000); 589 | Serial.print(F(" start ")); 590 | Serial.println(preference.mp3StartVolume); 591 | mp3.setVolume(playback.mp3CurrentVolume = preference.mp3StartVolume); 592 | Serial.print(F(" max ")); 593 | Serial.println(preference.mp3MaxVolume); 594 | Serial.print(F(" menu ")); 595 | Serial.println(preference.mp3MenuVolume); 596 | Serial.print(F(" eq ")); 597 | Serial.println(mp3EqualizerName[preference.mp3Equalizer]); 598 | mp3.setEq((DfMp3_Eq)(preference.mp3Equalizer - 1)); 599 | Serial.print(F(" files ")); 600 | Serial.println(mp3.getTotalTrackCount(DfMp3_PlaySource_Sd)); 601 | pinMode(mp3BusyPin, INPUT); 602 | 603 | Serial.print(F("init")); 604 | pinMode(button0Pin, INPUT_PULLUP); 605 | pinMode(button1Pin, INPUT_PULLUP); 606 | pinMode(button2Pin, INPUT_PULLUP); 607 | button0.init(button0Pin, HIGH, 0); 608 | button1.init(button1Pin, HIGH, 1); 609 | button2.init(button2Pin, HIGH, 2); 610 | #if defined FIVEBUTTONS 611 | pinMode(button3Pin, INPUT_PULLUP); 612 | pinMode(button4Pin, INPUT_PULLUP); 613 | button3.init(button3Pin, HIGH, 3); 614 | button4.init(button4Pin, HIGH, 4); 615 | Serial.print(F(" 5")); 616 | #else 617 | Serial.print(F(" 3")); 618 | #endif 619 | Serial.println(F(" buttons")); 620 | switchButtonConfiguration(INIT); 621 | 622 | Serial.print(F("init ")); 623 | Serial.print(preference.shutdownMinutes); 624 | Serial.println(F("m timer")); 625 | shutdownTimer(START); 626 | 627 | #if defined IRREMOTE 628 | Serial.println(F("init ir")); 629 | IrReceiver.begin(irReceiverPin, DISABLE_LED_FEEDBACK); 630 | #endif 631 | 632 | #if defined STATUSLED ^ defined STATUSLEDRGB 633 | #if defined STATUSLED 634 | Serial.println(F("init led")); 635 | pinMode(statusLedPin, OUTPUT); 636 | #endif 637 | #if defined STATUSLEDRGB 638 | Serial.println(F("init ws281x")); 639 | rgbLed.setOutput(statusLedPin); 640 | rgbLed.setColorOrderRGB(); 641 | //rgbLed.setColorOrderBRG(); 642 | //rgbLed.setColorOrderGRB(); 643 | #endif 644 | statusLedUpdate(SOLID, 0, 0, 0, 0); 645 | #endif 646 | 647 | #if defined LOWVOLTAGE 648 | Serial.println(F("init lvm")); 649 | Serial.print(F(" ex-")); 650 | Serial.print(shutdownMaxVoltage); 651 | Serial.print(F("V")); 652 | Serial.print(F(" wa-")); 653 | Serial.print(shutdownWarnVoltage); 654 | Serial.print(F("V")); 655 | Serial.print(F(" sh-")); 656 | Serial.print(shutdownMinVoltage); 657 | Serial.print(F("V")); 658 | Serial.print(F(" cu-")); 659 | Serial.print(shutdownVoltage.Read_Volts()); 660 | Serial.print(F("V (")); 661 | Serial.print(shutdownVoltage.Read_Perc(shutdownMinVoltage, shutdownMaxVoltage)); 662 | Serial.println(F("%)")); 663 | #endif 664 | 665 | // hold down all three buttons while powering up: erase the eeprom contents 666 | if (button0.isPressedRaw() && button1.isPressedRaw() && button2.isPressedRaw()) { 667 | #if defined PINCODE 668 | if (enterPinCode()) { 669 | #endif 670 | Serial.println(F("init eeprom")); 671 | for (uint16_t i = 0; i < EEPROM.length(); i++) { 672 | EEPROM.update(i, 0); 673 | } 674 | preferences(RESET); 675 | mp3.setVolume(playback.mp3CurrentVolume = preference.mp3StartVolume); 676 | mp3.setEq((DfMp3_Eq)(preference.mp3Equalizer - 1)); 677 | shutdownTimer(START); 678 | mp3.playMp3FolderTrack(809); 679 | waitPlaybackToFinish(0, 255, 0, 100); 680 | #if defined PINCODE 681 | } 682 | #endif 683 | } 684 | 685 | switchButtonConfiguration(PAUSE); 686 | mp3.playMp3FolderTrack(800); 687 | Serial.println(F("ready")); 688 | } 689 | 690 | void loop() { 691 | playback.isPlaying = !digitalRead(mp3BusyPin); 692 | checkForInput(); 693 | shutdownTimer(CHECK); 694 | 695 | #if defined LOWVOLTAGE 696 | // if low voltage level is reached, store progress and shutdown 697 | if (shutdownVoltage.Read_Volts() <= shutdownMinVoltage) { 698 | if (playback.currentTag.mode == STORYBOOK) EEPROM.update(playback.currentTag.folder, playback.playList[playback.playListItem - 1]); 699 | mp3.playMp3FolderTrack(808); 700 | waitPlaybackToFinish(255, 0, 0, 100); 701 | shutdownTimer(SHUTDOWN); 702 | } 703 | else if (shutdownVoltage.Read_Volts() <= shutdownWarnVoltage) { 704 | #if defined STATUSLED ^ defined STATUSLEDRGB 705 | statusLedUpdate(BLINK, 255, 0, 0, 100); 706 | #endif 707 | } 708 | else { 709 | #if defined STATUSLED ^ defined STATUSLEDRGB 710 | if (playback.isPlaying) statusLedUpdate(SOLID, 0, 255, 0, 100); 711 | else statusLedUpdate(PULSE, 0, 255, 0, 100); 712 | #endif 713 | } 714 | #else 715 | #if defined STATUSLED ^ defined STATUSLEDRGB 716 | if (playback.isPlaying) statusLedUpdate(SOLID, 0, 255, 0, 100); 717 | else statusLedUpdate(PULSE, 0, 255, 0, 100); 718 | #endif 719 | #endif 720 | 721 | // ################################################################################ 722 | // # main code block, if nfc tag is detected and TonUINO is not locked do something 723 | if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial() && !playback.isLocked) { 724 | // if the current playback mode is story book mode, only while playing: store the current progress 725 | if (playback.currentTag.mode == STORYBOOK && playback.isPlaying) { 726 | Serial.print(F("save ")); 727 | printModeFolderTrack(true); 728 | EEPROM.update(playback.currentTag.folder, playback.playList[playback.playListItem - 1]); 729 | } 730 | uint8_t readNfcTagStatus = readNfcTagData(); 731 | // ############################## 732 | // # nfc tag is successfully read 733 | if (readNfcTagStatus == 1) { 734 | // ############################################################################# 735 | // # nfc tag has our magic cookie on it, use data from nfc tag to start playback 736 | if (playback.currentTag.cookie == magicCookie) { 737 | switchButtonConfiguration(PLAY); 738 | shutdownTimer(STOP); 739 | 740 | randomSeed(micros()); 741 | 742 | // prepare boundaries for playback 743 | switch (playback.currentTag.mode) { 744 | case STORY: {} 745 | case ALBUM: {} 746 | case PARTY: {} 747 | case SINGLE: {} 748 | case STORYBOOK: { 749 | playback.folderStartTrack = 1; 750 | playback.folderEndTrack = mp3.getFolderTrackCount(playback.currentTag.folder); 751 | break; 752 | } 753 | case VSTORY: {} 754 | case VALBUM: {} 755 | case VPARTY: { 756 | playback.folderStartTrack = playback.currentTag.multiPurposeData1; 757 | playback.folderEndTrack = playback.currentTag.multiPurposeData2; 758 | break; 759 | } 760 | default: { 761 | break; 762 | } 763 | } 764 | 765 | // prepare playlist for playback 766 | for (uint8_t i = 0; i < 255; i++) playback.playList[i] = playback.folderStartTrack + i <= playback.folderEndTrack ? playback.folderStartTrack + i : 0; 767 | playback.playListItemCount = playback.folderEndTrack - playback.folderStartTrack + 1; 768 | 769 | // prepare first track for playback 770 | switch (playback.currentTag.mode) { 771 | case VSTORY: {} 772 | case STORY: { 773 | playback.playListItem = random(1, playback.playListItemCount + 1); 774 | break; 775 | } 776 | case VALBUM: {} 777 | case ALBUM: { 778 | playback.playListItem = 1; 779 | break; 780 | } 781 | case VPARTY: {} 782 | case PARTY: { 783 | playback.playListItem = 1; 784 | // shuffle playlist 785 | for (uint8_t i = 0; i < playback.playListItemCount; i++) { 786 | uint8_t j = random(0, playback.playListItemCount); 787 | uint8_t temp = playback.playList[i]; 788 | playback.playList[i] = playback.playList[j]; 789 | playback.playList[j] = temp; 790 | } 791 | break; 792 | } 793 | case SINGLE: { 794 | playback.playListItem = playback.currentTag.multiPurposeData1; 795 | break; 796 | } 797 | case STORYBOOK: { 798 | uint8_t storedTrack = EEPROM.read(playback.currentTag.folder); 799 | // don't resume from eeprom, play from the beginning 800 | if (storedTrack == 0 || storedTrack > playback.folderEndTrack) playback.playListItem = 1; 801 | // resume from eeprom 802 | else { 803 | playback.playListItem = storedTrack; 804 | Serial.print(F("resume ")); 805 | } 806 | break; 807 | } 808 | default: { 809 | break; 810 | } 811 | } 812 | 813 | playback.isFresh = true; 814 | playback.isRepeat = false; 815 | playback.playListMode = true; 816 | printModeFolderTrack(true); 817 | mp3.playFolderTrack(playback.currentTag.folder, playback.playList[playback.playListItem - 1]); 818 | } 819 | // # end - nfc tag has our magic cookie on it 820 | // ########################################## 821 | 822 | // ##################################################################################### 823 | // # nfc tag does not have our magic cookie on it, start setup to configure this nfc tag 824 | else if (playback.currentTag.cookie == 0) { 825 | nfcTagStruct newTag; 826 | playback.playListMode = false; 827 | 828 | // set volume to menu volume 829 | mp3.setVolume(preference.mp3MenuVolume); 830 | 831 | switchButtonConfiguration(CONFIG); 832 | shutdownTimer(STOP); 833 | 834 | while (true) { 835 | Serial.println(F("setup tag")); 836 | Serial.println(F("folder")); 837 | newTag.folder = prompt(99, 801, 0, 0, 0, true, false); 838 | if (newTag.folder == 0) { 839 | mp3.playMp3FolderTrack(807); 840 | waitPlaybackToFinish(255, 0, 0, 100); 841 | break; 842 | } 843 | Serial.println(F("mode")); 844 | newTag.mode = prompt(8, 820, 820, 0, 0, false, false); 845 | if (newTag.mode == 0) { 846 | mp3.playMp3FolderTrack(807); 847 | waitPlaybackToFinish(255, 0, 0, 100); 848 | break; 849 | } 850 | else if (newTag.mode == SINGLE) { 851 | Serial.println(F("singletrack")); 852 | newTag.multiPurposeData1 = prompt(mp3.getFolderTrackCount(newTag.folder), 802, 0, 0, newTag.folder, true, false); 853 | newTag.multiPurposeData2 = 0; 854 | if (newTag.multiPurposeData1 == 0) { 855 | mp3.playMp3FolderTrack(807); 856 | waitPlaybackToFinish(255, 0, 0, 100); 857 | break; 858 | } 859 | } 860 | else if (newTag.mode == VSTORY || newTag.mode == VALBUM || newTag.mode == VPARTY) { 861 | Serial.println(F("starttrack")); 862 | newTag.multiPurposeData1 = prompt(mp3.getFolderTrackCount(newTag.folder), 803, 0, 0, newTag.folder, true, false); 863 | if (newTag.multiPurposeData1 == 0) { 864 | mp3.playMp3FolderTrack(807); 865 | waitPlaybackToFinish(255, 0, 0, 100); 866 | break; 867 | } 868 | Serial.println(F("endtrack")); 869 | newTag.multiPurposeData2 = prompt(mp3.getFolderTrackCount(newTag.folder), 804, 0, newTag.multiPurposeData1, newTag.folder, true, false); 870 | newTag.multiPurposeData2 = max(newTag.multiPurposeData1, newTag.multiPurposeData2); 871 | if (newTag.multiPurposeData2 == 0) { 872 | mp3.playMp3FolderTrack(807); 873 | waitPlaybackToFinish(255, 0, 0, 100); 874 | break; 875 | } 876 | } 877 | uint8_t bytesToWrite[] = {magicCookieHex[0], // 1st byte of magic cookie (by default 0x13) 878 | magicCookieHex[1], // 2nd byte of magic cookie (by default 0x37) 879 | magicCookieHex[2], // 3rd byte of magic cookie (by default 0xb3) 880 | magicCookieHex[3], // 4th byte of magic cookie (by default 0x47) 881 | 0x01, // version 1 882 | newTag.folder, // the folder selected by the user 883 | newTag.mode, // the playback mode selected by the user 884 | newTag.multiPurposeData1, // multi purpose data (ie. single track for mode 4 and start of vfolder) 885 | newTag.multiPurposeData2, // multi purpose data (ie. end of vfolder, depending on mode) 886 | 0x00, 0x00, 0x00, // reserved for future use 887 | 0x00, 0x00, 0x00, 0x00 // reserved for future use 888 | }; 889 | uint8_t writeNfcTagStatus = writeNfcTagData(bytesToWrite, sizeof(bytesToWrite)); 890 | if (writeNfcTagStatus == 1) { 891 | mp3.playMp3FolderTrack(805); 892 | waitPlaybackToFinish(0, 255, 0, 100); 893 | } 894 | else { 895 | mp3.playMp3FolderTrack(806); 896 | waitPlaybackToFinish(255, 0, 0, 100); 897 | } 898 | break; 899 | } 900 | mfrc522.PICC_HaltA(); 901 | mfrc522.PCD_StopCrypto1(); 902 | 903 | // restore playback volume, can't be higher than maximum volume 904 | mp3.setVolume(playback.mp3CurrentVolume = min(playback.mp3CurrentVolume, preference.mp3MaxVolume)); 905 | 906 | switchButtonConfiguration(PAUSE); 907 | shutdownTimer(START); 908 | inputEvent = NOP; 909 | } 910 | // # end - nfc tag does not have our magic cookie on it 911 | // #################################################### 912 | } 913 | // # end - nfc tag is successfully read 914 | // #################################### 915 | } 916 | // # end - main code block 917 | // ####################### 918 | 919 | // ################################################################################## 920 | // # handle button and ir remote events during playback or while waiting for nfc tags 921 | // ir remote center: toggle box lock 922 | if (inputEvent == IRC) { 923 | if ((playback.isLocked = !playback.isLocked)) { 924 | Serial.println(F("lock")); 925 | #if defined STATUSLED ^ defined STATUSLEDRGB 926 | statusLedUpdate(BURST4, 255, 0, 0, 0); 927 | #endif 928 | } 929 | else { 930 | Serial.println(F("unlock")); 931 | #if defined STATUSLED ^ defined STATUSLEDRGB 932 | statusLedUpdate(BURST8, 0, 255, 0, 0); 933 | #endif 934 | } 935 | } 936 | // button 0 (middle) press or ir remote play/pause: toggle playback 937 | else if ((inputEvent == B0P && !playback.isLocked) || inputEvent == IRP) { 938 | if (playback.isPlaying) { 939 | switchButtonConfiguration(PAUSE); 940 | shutdownTimer(START); 941 | Serial.println(F("pause")); 942 | mp3.pause(); 943 | // if the current playback mode is story book mode: store the current progress 944 | if (playback.currentTag.mode == STORYBOOK) { 945 | Serial.print(F("save ")); 946 | printModeFolderTrack(true); 947 | EEPROM.update(playback.currentTag.folder, playback.playList[playback.playListItem - 1]); 948 | } 949 | } 950 | else { 951 | if (playback.playListMode) { 952 | switchButtonConfiguration(PLAY); 953 | shutdownTimer(STOP); 954 | Serial.println(F("play")); 955 | mp3.start(); 956 | } 957 | } 958 | } 959 | // button 1 (right) press or ir remote up while playing: increase volume 960 | else if (((inputEvent == B1P && !playback.isLocked) || inputEvent == IRU) && playback.isPlaying) { 961 | if (playback.mp3CurrentVolume < preference.mp3MaxVolume) { 962 | mp3.setVolume(++playback.mp3CurrentVolume); 963 | Serial.print(F("volume ")); 964 | Serial.println(playback.mp3CurrentVolume); 965 | } 966 | #if defined STATUSLED 967 | else statusLedUpdate(BURST2, 255, 0, 0, 0); 968 | #endif 969 | } 970 | // button 2 (left) press or ir remote down while playing: decrease volume 971 | else if (((inputEvent == B2P && !playback.isLocked) || inputEvent == IRD) && playback.isPlaying) { 972 | if (playback.mp3CurrentVolume > 1) { 973 | mp3.setVolume(--playback.mp3CurrentVolume); 974 | Serial.print(F("volume ")); 975 | Serial.println(playback.mp3CurrentVolume); 976 | } 977 | #if defined STATUSLED 978 | else statusLedUpdate(BURST2, 255, 0, 0, 0); 979 | #endif 980 | } 981 | // button 1 (right) hold for 2 sec or button 5 press or ir remote right, only during (v)album, (v)party and story book mode while playing: next track 982 | else if ((((inputEvent == B1H || inputEvent == B4P) && !playback.isLocked) || inputEvent == IRR) && (playback.currentTag.mode == ALBUM || playback.currentTag.mode == PARTY || playback.currentTag.mode == STORYBOOK || playback.currentTag.mode == VALBUM || playback.currentTag.mode == VPARTY) && playback.isPlaying) { 983 | Serial.println(F("next")); 984 | playNextTrack(0, true, true); 985 | } 986 | // button 2 (left) hold for 2 sec or button 4 press or ir remote left, only during (v)album, (v)party and story book mode while playing: previous track 987 | else if ((((inputEvent == B2H || inputEvent == B3P) && !playback.isLocked) || inputEvent == IRL) && (playback.currentTag.mode == ALBUM || playback.currentTag.mode == PARTY || playback.currentTag.mode == STORYBOOK || playback.currentTag.mode == VALBUM || playback.currentTag.mode == VPARTY) && playback.isPlaying) { 988 | Serial.println(F("prev")); 989 | playNextTrack(0, false, true); 990 | } 991 | // button 0 (middle) hold for 5 sec or ir remote menu, only during (v)story, (v)album, (v)party and single mode while playing: toggle single track repeat 992 | else if (((inputEvent == B0H && !playback.isLocked) || inputEvent == IRM) && (playback.currentTag.mode == STORY || playback.currentTag.mode == ALBUM || playback.currentTag.mode == PARTY || playback.currentTag.mode == SINGLE || playback.currentTag.mode == VSTORY || playback.currentTag.mode == VALBUM || playback.currentTag.mode == VPARTY) && playback.isPlaying) { 993 | Serial.print(F("repeat ")); 994 | if ((playback.isRepeat = !playback.isRepeat)) { 995 | Serial.println(F("on")); 996 | #if defined STATUSLED ^ defined STATUSLEDRGB 997 | statusLedUpdate(BURST4, 255, 255, 255, 0); 998 | #endif 999 | } 1000 | else { 1001 | Serial.println(F("off")); 1002 | #if defined STATUSLED ^ defined STATUSLEDRGB 1003 | statusLedUpdate(BURST8, 255, 255, 255, 0); 1004 | #endif 1005 | } 1006 | } 1007 | // button 0 (middle) hold for 5 sec or ir remote menu, only during story book mode while playing: reset progress 1008 | else if (((inputEvent == B0H && !playback.isLocked) || inputEvent == IRM) && playback.currentTag.mode == STORYBOOK && playback.isPlaying) { 1009 | playback.playListItem = 1; 1010 | Serial.print(F("reset ")); 1011 | printModeFolderTrack(true); 1012 | EEPROM.update(playback.currentTag.folder, 0); 1013 | mp3.playFolderTrack(playback.currentTag.folder, playback.playList[playback.playListItem - 1]); 1014 | #if defined STATUSLED ^ defined STATUSLEDRGB 1015 | statusLedUpdate(BURST8, 255, 0, 255, 0); 1016 | #endif 1017 | } 1018 | // button 0 (middle) hold for 5 sec or ir remote menu while not playing: parents menu 1019 | else if (((inputEvent == B0H && !playback.isLocked) || inputEvent == IRM) && !playback.isPlaying) { 1020 | parentsMenu(); 1021 | Serial.println(F("ready")); 1022 | } 1023 | // # end - handle button or ir remote events during playback or while waiting for nfc tags 1024 | // ####################################################################################### 1025 | 1026 | mp3.loop(); 1027 | } 1028 | 1029 | // ################################################################################################################################################################ 1030 | // ################################################################ functions are below this line! ################################################################ 1031 | // ################################################################################################################################################################ 1032 | 1033 | // checks all input sources (and populates the global inputEvent variable for ir events) 1034 | void checkForInput() { 1035 | // clear inputEvent 1036 | inputEvent = NOP; 1037 | 1038 | // check all buttons 1039 | button0.check(); 1040 | button1.check(); 1041 | button2.check(); 1042 | #if defined FIVEBUTTONS 1043 | button3.check(); 1044 | button4.check(); 1045 | #endif 1046 | 1047 | #if defined IRREMOTE 1048 | uint8_t irRemoteEvent = NOP; 1049 | uint16_t irRemoteCode = 0; 1050 | static uint64_t irRemoteOldMillis; 1051 | 1052 | // poll ir receiver, has precedence over (overwrites) physical buttons 1053 | if (IrReceiver.decode()) { 1054 | // process only codes which don't have the repeat flag set 1055 | if (!(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)) { 1056 | irRemoteCode = IrReceiver.decodedIRData.command; 1057 | for (uint8_t i = 0; i < irRemoteCount; i++) { 1058 | for (uint8_t j = 0; j < irRemoteCodeCount; j++) { 1059 | //if we have a match, temporally populate irRemoteEvent and break 1060 | if (irRemoteCode == irRemoteCodes[i][j] || irRemoteCode == preference.irRemoteUserCodes[j]) { 1061 | // 16 is used as an offset in the button action enum list - 17 is the first ir action 1062 | irRemoteEvent = 16 + j; 1063 | break; 1064 | } 1065 | } 1066 | // if the inner loop had a match, populate inputEvent and break 1067 | // ir remote key presses are debounced by 250ms 1068 | if (millis() - irRemoteOldMillis >= 250) { 1069 | irRemoteOldMillis = millis(); 1070 | inputEvent = irRemoteEvent; 1071 | break; 1072 | } 1073 | } 1074 | } 1075 | IrReceiver.resume(); 1076 | } 1077 | #endif 1078 | } 1079 | 1080 | // translates the various button events into enums and populates the global inputEvent variable 1081 | void translateButtonInput(AceButton *button, uint8_t eventType, uint8_t /* buttonState */) { 1082 | switch (button->getId()) { 1083 | // button 0 (middle) 1084 | case 0: { 1085 | switch (eventType) { 1086 | case AceButton::kEventClicked: { 1087 | inputEvent = B0P; 1088 | break; 1089 | } 1090 | case AceButton::kEventLongPressed: { 1091 | inputEvent = B0H; 1092 | break; 1093 | } 1094 | case AceButton::kEventDoubleClicked: { 1095 | inputEvent = B0D; 1096 | break; 1097 | } 1098 | default: { 1099 | break; 1100 | } 1101 | } 1102 | break; 1103 | } 1104 | // button 1 (right) 1105 | case 1: { 1106 | switch (eventType) { 1107 | case AceButton::kEventClicked: { 1108 | inputEvent = B1P; 1109 | break; 1110 | } 1111 | case AceButton::kEventLongPressed: { 1112 | inputEvent = B1H; 1113 | break; 1114 | } 1115 | default: { 1116 | break; 1117 | } 1118 | } 1119 | break; 1120 | } 1121 | // button 2 (left) 1122 | case 2: { 1123 | switch (eventType) { 1124 | case AceButton::kEventClicked: { 1125 | inputEvent = B2P; 1126 | break; 1127 | } 1128 | case AceButton::kEventLongPressed: { 1129 | inputEvent = B2H; 1130 | break; 1131 | } 1132 | default: { 1133 | break; 1134 | } 1135 | } 1136 | break; 1137 | } 1138 | #if defined FIVEBUTTONS 1139 | // optional 4th button 1140 | case 3: { 1141 | switch (eventType) { 1142 | case AceButton::kEventClicked: { 1143 | inputEvent = B3P; 1144 | break; 1145 | } 1146 | default: { 1147 | break; 1148 | } 1149 | } 1150 | break; 1151 | } 1152 | // optional 5th button 1153 | case 4: { 1154 | switch (eventType) { 1155 | case AceButton::kEventClicked: { 1156 | inputEvent = B4P; 1157 | break; 1158 | } 1159 | default: { 1160 | break; 1161 | } 1162 | } 1163 | break; 1164 | } 1165 | #endif 1166 | default: { 1167 | break; 1168 | } 1169 | } 1170 | } 1171 | 1172 | // switches button configuration dependig on the state that TonUINO is in 1173 | void switchButtonConfiguration(uint8_t buttonMode) { 1174 | switch (buttonMode) { 1175 | case INIT: { 1176 | // button 0 (middle) 1177 | button0Config.setEventHandler(translateButtonInput); 1178 | button0Config.setFeature(ButtonConfig::kFeatureClick); 1179 | button0Config.setFeature(ButtonConfig::kFeatureSuppressAfterClick); 1180 | button0Config.setClickDelay(buttonClickDelay); 1181 | button0Config.setFeature(ButtonConfig::kFeatureLongPress); 1182 | button0Config.setFeature(ButtonConfig::kFeatureSuppressAfterLongPress); 1183 | button0Config.setLongPressDelay(buttonShortLongPressDelay); 1184 | // button 1 (right) 1185 | button1Config.setEventHandler(translateButtonInput); 1186 | button1Config.setFeature(ButtonConfig::kFeatureClick); 1187 | button1Config.setFeature(ButtonConfig::kFeatureSuppressAfterClick); 1188 | button1Config.setClickDelay(buttonClickDelay); 1189 | #if not defined FIVEBUTTONS 1190 | // only enable long press on button 1 (right) when in 3 button mode 1191 | button1Config.setFeature(ButtonConfig::kFeatureLongPress); 1192 | button1Config.setFeature(ButtonConfig::kFeatureSuppressAfterLongPress); 1193 | button1Config.setLongPressDelay(buttonShortLongPressDelay); 1194 | #endif 1195 | // button 2 (left) 1196 | button2Config.setEventHandler(translateButtonInput); 1197 | button2Config.setFeature(ButtonConfig::kFeatureClick); 1198 | button2Config.setFeature(ButtonConfig::kFeatureSuppressAfterClick); 1199 | button2Config.setClickDelay(buttonClickDelay); 1200 | #if not defined FIVEBUTTONS 1201 | // only enable long press on button 2 (left) when in 3 button mode 1202 | button2Config.setFeature(ButtonConfig::kFeatureLongPress); 1203 | button2Config.setFeature(ButtonConfig::kFeatureSuppressAfterLongPress); 1204 | button2Config.setLongPressDelay(buttonShortLongPressDelay); 1205 | #endif 1206 | #if defined FIVEBUTTONS 1207 | // optional 4th button 1208 | button3Config.setEventHandler(translateButtonInput); 1209 | button3Config.setFeature(ButtonConfig::kFeatureClick); 1210 | button3Config.setFeature(ButtonConfig::kFeatureSuppressAfterClick); 1211 | button3Config.setClickDelay(buttonClickDelay); 1212 | // optional 5th button 1213 | button4Config.setEventHandler(translateButtonInput); 1214 | button4Config.setFeature(ButtonConfig::kFeatureClick); 1215 | button4Config.setFeature(ButtonConfig::kFeatureSuppressAfterClick); 1216 | button4Config.setClickDelay(buttonClickDelay); 1217 | #endif 1218 | break; 1219 | } 1220 | case PLAY: { 1221 | // button 0 (middle) 1222 | button0Config.clearFeature(ButtonConfig::kFeatureDoubleClick); 1223 | button0Config.clearFeature(ButtonConfig::kFeatureSuppressClickBeforeDoubleClick); 1224 | button0Config.clearFeature(ButtonConfig::kFeatureSuppressAfterDoubleClick); 1225 | button0Config.setLongPressDelay(buttonLongLongPressDelay); 1226 | break; 1227 | } 1228 | case PAUSE: { 1229 | // button 0 (middle) 1230 | button0Config.clearFeature(ButtonConfig::kFeatureDoubleClick); 1231 | button0Config.clearFeature(ButtonConfig::kFeatureSuppressClickBeforeDoubleClick); 1232 | button0Config.clearFeature(ButtonConfig::kFeatureSuppressAfterDoubleClick); 1233 | button0Config.setLongPressDelay(buttonLongLongPressDelay); 1234 | break; 1235 | } 1236 | case PIN: { 1237 | // button 0 (middle) 1238 | button0Config.clearFeature(ButtonConfig::kFeatureDoubleClick); 1239 | button0Config.clearFeature(ButtonConfig::kFeatureSuppressClickBeforeDoubleClick); 1240 | button0Config.clearFeature(ButtonConfig::kFeatureSuppressAfterDoubleClick); 1241 | button0Config.setLongPressDelay(buttonShortLongPressDelay); 1242 | break; 1243 | } 1244 | case CONFIG: { 1245 | // button 0 (middle) 1246 | button0Config.setFeature(ButtonConfig::kFeatureDoubleClick); 1247 | button0Config.setFeature(ButtonConfig::kFeatureSuppressClickBeforeDoubleClick); 1248 | button0Config.setFeature(ButtonConfig::kFeatureSuppressAfterDoubleClick); 1249 | button0Config.setLongPressDelay(buttonShortLongPressDelay); 1250 | break; 1251 | } 1252 | default: { 1253 | break; 1254 | } 1255 | } 1256 | } 1257 | 1258 | // waits for current playing track to finish 1259 | void waitPlaybackToFinish([[maybe_unused]] uint8_t red, [[maybe_unused]] uint8_t green, [[maybe_unused]] uint8_t blue, [[maybe_unused]] uint16_t statusLedUpdateInterval) { 1260 | uint64_t waitPlaybackToStartMillis = millis(); 1261 | 1262 | delay(500); 1263 | while (digitalRead(mp3BusyPin)) { 1264 | if (millis() - waitPlaybackToStartMillis >= 10000) break; 1265 | #if defined STATUSLED ^ defined STATUSLEDRGB 1266 | statusLedUpdate(BLINK, red, green, blue, statusLedUpdateInterval); 1267 | #endif 1268 | } 1269 | while (!digitalRead(mp3BusyPin)) { 1270 | #if defined STATUSLED ^ defined STATUSLEDRGB 1271 | statusLedUpdate(BLINK, red, green, blue, statusLedUpdateInterval); 1272 | #endif 1273 | mp3.loop(); 1274 | } 1275 | } 1276 | 1277 | // prints current mode, folder and track information 1278 | void printModeFolderTrack(bool cr) { 1279 | Serial.print(playbackModeName[playback.currentTag.mode]); 1280 | Serial.print(F("-")); 1281 | Serial.print(playback.currentTag.folder); 1282 | Serial.print(F("-")); 1283 | Serial.print(playback.playListItem); 1284 | Serial.print(F("/")); 1285 | Serial.print(playback.playListItemCount); 1286 | if (playback.currentTag.mode == PARTY) { 1287 | Serial.print(F("-(")); 1288 | Serial.print(playback.playList[playback.playListItem - 1]); 1289 | Serial.print(F(")")); 1290 | } 1291 | else if (playback.currentTag.mode == VSTORY || playback.currentTag.mode == VALBUM || playback.currentTag.mode == VPARTY) { 1292 | Serial.print(F("-(")); 1293 | Serial.print(playback.folderStartTrack); 1294 | Serial.print(F("->")); 1295 | Serial.print(playback.folderEndTrack); 1296 | Serial.print(F("|")); 1297 | Serial.print(playback.playList[playback.playListItem - 1]); 1298 | Serial.print(F(")")); 1299 | } 1300 | if (cr) Serial.println(); 1301 | } 1302 | 1303 | // plays next track depending on the current playback mode 1304 | void playNextTrack(uint16_t globalTrack, bool directionForward, bool triggeredManually) { 1305 | static uint16_t lastCallTrack = 0; 1306 | 1307 | // we only advance to a new track when in playlist mode, not during interactive prompt playback (ie. during configuration of a new nfc tag) 1308 | if (!playback.playListMode) return; 1309 | 1310 | //delay 100ms to be on the safe side with the serial communication 1311 | delay(100); 1312 | 1313 | // story mode (1): play one random track in folder 1314 | // single mode (4): play one single track in folder 1315 | // vstory mode (6): play one random track in virtual folder 1316 | // there is no next track in story, single and vstory mode, stop playback 1317 | if (playback.currentTag.mode == STORY || playback.currentTag.mode == SINGLE || playback.currentTag.mode == VSTORY) { 1318 | if (playback.isRepeat) { 1319 | lastCallTrack = 0; 1320 | printModeFolderTrack(true); 1321 | mp3.playFolderTrack(playback.currentTag.folder, playback.playList[playback.playListItem - 1]); 1322 | } 1323 | else { 1324 | playback.playListMode = false; 1325 | switchButtonConfiguration(PAUSE); 1326 | shutdownTimer(START); 1327 | Serial.print(playbackModeName[playback.currentTag.mode]); 1328 | Serial.println(F("-stop")); 1329 | mp3.stop(); 1330 | } 1331 | } 1332 | 1333 | // album mode (2): play the complete folder 1334 | // party mode (3): shuffle the complete folder 1335 | // story book mode (5): play the complete folder and track progress 1336 | // valbum mode (7): play the complete virtual folder 1337 | // vparty mode (8): shuffle the complete virtual folder 1338 | // advance to the next or previous track, stop if the end of the folder is reached 1339 | if (playback.currentTag.mode == ALBUM || playback.currentTag.mode == PARTY || playback.currentTag.mode == STORYBOOK || playback.currentTag.mode == VALBUM || playback.currentTag.mode == VPARTY) { 1340 | 1341 | // **workaround for some DFPlayer mini modules that make two callbacks in a row when finishing a track** 1342 | // reset lastCallTrack to avoid lockup when playback was just started 1343 | if (playback.isFresh) { 1344 | playback.isFresh = false; 1345 | lastCallTrack = 0; 1346 | } 1347 | // check if we automatically get called with the same track number twice in a row, if yes return immediately 1348 | if (lastCallTrack == globalTrack && !triggeredManually) return; 1349 | else lastCallTrack = globalTrack; 1350 | 1351 | // play next track? 1352 | if (directionForward) { 1353 | // single track repeat is on, repeat current track 1354 | if (playback.isRepeat && !triggeredManually) { 1355 | lastCallTrack = 0; 1356 | printModeFolderTrack(true); 1357 | mp3.playFolderTrack(playback.currentTag.folder, playback.playList[playback.playListItem - 1]); 1358 | } 1359 | // there are more tracks after the current one, play next track 1360 | else if (playback.playListItem < playback.playListItemCount) { 1361 | playback.playListItem++; 1362 | printModeFolderTrack(true); 1363 | mp3.playFolderTrack(playback.currentTag.folder, playback.playList[playback.playListItem - 1]); 1364 | } 1365 | // there are no more tracks after the current one 1366 | else { 1367 | // if not triggered manually, stop playback (and reset progress) 1368 | if (!triggeredManually) { 1369 | playback.playListMode = false; 1370 | switchButtonConfiguration(PAUSE); 1371 | shutdownTimer(START); 1372 | Serial.print(playbackModeName[playback.currentTag.mode]); 1373 | Serial.print(F("-stop")); 1374 | if (playback.currentTag.mode == STORYBOOK) { 1375 | Serial.print(F("-reset")); 1376 | EEPROM.update(playback.currentTag.folder, 0); 1377 | } 1378 | Serial.println(); 1379 | mp3.stop(); 1380 | } 1381 | #if defined STATUSLED 1382 | else statusLedUpdate(BURST2, 255, 0, 0, 0); 1383 | #endif 1384 | } 1385 | } 1386 | // play previous track? 1387 | else { 1388 | // there are more tracks before the current one, play the previous track 1389 | if (playback.playListItem > 1) { 1390 | playback.playListItem--; 1391 | printModeFolderTrack(true); 1392 | mp3.playFolderTrack(playback.currentTag.folder, playback.playList[playback.playListItem - 1]); 1393 | } 1394 | #if defined STATUSLED 1395 | else statusLedUpdate(BURST2, 255, 0, 0, 0); 1396 | #endif 1397 | } 1398 | } 1399 | } 1400 | 1401 | // reads data from nfc tag 1402 | uint8_t readNfcTagData() { 1403 | uint8_t nfcTagReadBuffer[16] = {}; 1404 | uint8_t piccReadBuffer[18] = {}; 1405 | uint8_t piccReadBufferSize = sizeof(piccReadBuffer); 1406 | bool nfcTagReadSuccess = false; 1407 | MFRC522::StatusCode piccStatus; 1408 | MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak); 1409 | 1410 | // decide which code path to take depending on picc type 1411 | if (piccType == MFRC522::PICC_TYPE_MIFARE_MINI || piccType == MFRC522::PICC_TYPE_MIFARE_1K || piccType == MFRC522::PICC_TYPE_MIFARE_4K) { 1412 | uint8_t classicBlock = 4; 1413 | uint8_t classicTrailerBlock = 7; 1414 | MFRC522::MIFARE_Key classicKey; 1415 | for (uint8_t i = 0; i < 6; i++) classicKey.keyByte[i] = 0xFF; 1416 | 1417 | // check if we can authenticate with classicKey 1418 | piccStatus = (MFRC522::StatusCode)mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, classicTrailerBlock, &classicKey, &mfrc522.uid); 1419 | if (piccStatus == MFRC522::STATUS_OK) { 1420 | // read 16 bytes from nfc tag (by default sector 1 / block 4) 1421 | piccStatus = (MFRC522::StatusCode)mfrc522.MIFARE_Read(classicBlock, piccReadBuffer, &piccReadBufferSize); 1422 | if (piccStatus == MFRC522::STATUS_OK) { 1423 | nfcTagReadSuccess = true; 1424 | memcpy(nfcTagReadBuffer, piccReadBuffer, sizeof(nfcTagReadBuffer)); 1425 | } 1426 | else Serial.println(mfrc522.GetStatusCodeName(piccStatus)); 1427 | } 1428 | else Serial.println(mfrc522.GetStatusCodeName(piccStatus)); 1429 | } 1430 | else if (piccType == MFRC522::PICC_TYPE_MIFARE_UL) { 1431 | uint8_t ultralightStartPage = 8; 1432 | uint8_t ultralightACK[2] = {}; 1433 | MFRC522::MIFARE_Key ultralightKey; 1434 | for (uint8_t i = 0; i < 4; i++) ultralightKey.keyByte[i] = 0xFF; 1435 | 1436 | // check if we can authenticate with ultralightKey 1437 | piccStatus = (MFRC522::StatusCode)mfrc522.PCD_NTAG216_AUTH(ultralightKey.keyByte, ultralightACK); 1438 | if (piccStatus == MFRC522::STATUS_OK) { 1439 | // read 16 bytes from nfc tag (by default pages 8-11) 1440 | for (uint8_t ultralightPage = ultralightStartPage; ultralightPage < ultralightStartPage + 4; ultralightPage++) { 1441 | piccStatus = (MFRC522::StatusCode)mfrc522.MIFARE_Read(ultralightPage, piccReadBuffer, &piccReadBufferSize); 1442 | if (piccStatus == MFRC522::STATUS_OK) { 1443 | nfcTagReadSuccess = true; 1444 | memcpy(nfcTagReadBuffer + ((ultralightPage * 4) - (ultralightStartPage * 4)), piccReadBuffer, 4); 1445 | } 1446 | else { 1447 | nfcTagReadSuccess = false; 1448 | Serial.println(mfrc522.GetStatusCodeName(piccStatus)); 1449 | break; 1450 | } 1451 | } 1452 | } 1453 | else Serial.println(mfrc522.GetStatusCodeName(piccStatus)); 1454 | } 1455 | // picc type is not supported 1456 | else { 1457 | mfrc522.PICC_HaltA(); 1458 | mfrc522.PCD_StopCrypto1(); 1459 | return 0; 1460 | } 1461 | 1462 | Serial.print(nfcStatusMessage[1]); 1463 | Serial.print(nfcStatusMessage[0]); 1464 | printNfcTagType(piccType); 1465 | // read was successfull 1466 | if (nfcTagReadSuccess) { 1467 | // log data to the console 1468 | Serial.print(nfcStatusMessage[3]); 1469 | printNfcTagData(nfcTagReadBuffer, sizeof(nfcTagReadBuffer), true); 1470 | 1471 | // convert 4 byte magic cookie to 32bit decimal for easier handling 1472 | uint32_t tempMagicCookie = 0; 1473 | tempMagicCookie = (uint32_t)nfcTagReadBuffer[0] << 24; 1474 | tempMagicCookie += (uint32_t)nfcTagReadBuffer[1] << 16; 1475 | tempMagicCookie += (uint32_t)nfcTagReadBuffer[2] << 8; 1476 | tempMagicCookie += (uint32_t)nfcTagReadBuffer[3]; 1477 | 1478 | // if cookie is not blank, update ncfTag object with data read from nfc tag 1479 | if (tempMagicCookie != 0) { 1480 | playback.currentTag.cookie = tempMagicCookie; 1481 | playback.currentTag.version = nfcTagReadBuffer[4]; 1482 | playback.currentTag.folder = nfcTagReadBuffer[5]; 1483 | playback.currentTag.mode = nfcTagReadBuffer[6]; 1484 | playback.currentTag.multiPurposeData1 = nfcTagReadBuffer[7]; 1485 | playback.currentTag.multiPurposeData2 = nfcTagReadBuffer[8]; 1486 | mfrc522.PICC_HaltA(); 1487 | mfrc522.PCD_StopCrypto1(); 1488 | } 1489 | // if magic cookie is blank, clear ncfTag object 1490 | else { 1491 | playback.currentTag.cookie = 0; 1492 | playback.currentTag.version = 0; 1493 | playback.currentTag.folder = 0; 1494 | playback.currentTag.mode = 0; 1495 | playback.currentTag.multiPurposeData1 = 0; 1496 | playback.currentTag.multiPurposeData2 = 0; 1497 | } 1498 | return 1; 1499 | } 1500 | // read was not successfull 1501 | else { 1502 | Serial.println(nfcStatusMessage[4]); 1503 | mfrc522.PICC_HaltA(); 1504 | mfrc522.PCD_StopCrypto1(); 1505 | return 0; 1506 | } 1507 | } 1508 | 1509 | // writes data to nfc tag 1510 | uint8_t writeNfcTagData(uint8_t nfcTagWriteBuffer[], uint8_t nfcTagWriteBufferSize) { 1511 | uint8_t piccWriteBuffer[16] = {}; 1512 | bool nfcTagWriteSuccess = false; 1513 | MFRC522::StatusCode piccStatus; 1514 | MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak); 1515 | 1516 | // decide which code path to take depending on picc type 1517 | if (piccType == MFRC522::PICC_TYPE_MIFARE_MINI || piccType == MFRC522::PICC_TYPE_MIFARE_1K || piccType == MFRC522::PICC_TYPE_MIFARE_4K) { 1518 | uint8_t classicBlock = 4; 1519 | uint8_t classicTrailerBlock = 7; 1520 | MFRC522::MIFARE_Key classicKey; 1521 | for (uint8_t i = 0; i < 6; i++) classicKey.keyByte[i] = 0xFF; 1522 | 1523 | // check if we can authenticate with classicKey 1524 | piccStatus = (MFRC522::StatusCode)mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, classicTrailerBlock, &classicKey, &mfrc522.uid); 1525 | if (piccStatus == MFRC522::STATUS_OK) { 1526 | // write 16 bytes to nfc tag (by default sector 1 / block 4) 1527 | memcpy(piccWriteBuffer, nfcTagWriteBuffer, sizeof(piccWriteBuffer)); 1528 | piccStatus = (MFRC522::StatusCode)mfrc522.MIFARE_Write(classicBlock, piccWriteBuffer, sizeof(piccWriteBuffer)); 1529 | if (piccStatus == MFRC522::STATUS_OK) nfcTagWriteSuccess = true; 1530 | else Serial.println(mfrc522.GetStatusCodeName(piccStatus)); 1531 | } 1532 | else Serial.println(mfrc522.GetStatusCodeName(piccStatus)); 1533 | } 1534 | else if (piccType == MFRC522::PICC_TYPE_MIFARE_UL) { 1535 | uint8_t ultralightStartPage = 8; 1536 | uint8_t ultralightACK[2] = {}; 1537 | MFRC522::MIFARE_Key ultralightKey; 1538 | for (uint8_t i = 0; i < 4; i++) ultralightKey.keyByte[i] = 0xFF; 1539 | 1540 | // check if we can authenticate with ultralightKey 1541 | piccStatus = (MFRC522::StatusCode)mfrc522.PCD_NTAG216_AUTH(ultralightKey.keyByte, ultralightACK); 1542 | if (piccStatus == MFRC522::STATUS_OK) { 1543 | // write 16 bytes to nfc tag (by default pages 8-11) 1544 | for (uint8_t ultralightPage = ultralightStartPage; ultralightPage < ultralightStartPage + 4; ultralightPage++) { 1545 | memcpy(piccWriteBuffer, nfcTagWriteBuffer + ((ultralightPage * 4) - (ultralightStartPage * 4)), 4); 1546 | piccStatus = (MFRC522::StatusCode)mfrc522.MIFARE_Write(ultralightPage, piccWriteBuffer, sizeof(piccWriteBuffer)); 1547 | if (piccStatus == MFRC522::STATUS_OK) nfcTagWriteSuccess = true; 1548 | else { 1549 | nfcTagWriteSuccess = false; 1550 | Serial.println(mfrc522.GetStatusCodeName(piccStatus)); 1551 | break; 1552 | } 1553 | } 1554 | } 1555 | else Serial.println(mfrc522.GetStatusCodeName(piccStatus)); 1556 | } 1557 | // picc type is not supported 1558 | else { 1559 | mfrc522.PICC_HaltA(); 1560 | mfrc522.PCD_StopCrypto1(); 1561 | return 0; 1562 | } 1563 | 1564 | Serial.print(nfcStatusMessage[2]); 1565 | Serial.print(nfcStatusMessage[0]); 1566 | printNfcTagType(piccType); 1567 | // write was successfull 1568 | if (nfcTagWriteSuccess) { 1569 | // log data to the console 1570 | Serial.print(nfcStatusMessage[3]); 1571 | printNfcTagData(nfcTagWriteBuffer, nfcTagWriteBufferSize, true); 1572 | mfrc522.PICC_HaltA(); 1573 | mfrc522.PCD_StopCrypto1(); 1574 | return 1; 1575 | } 1576 | // write was not successfull 1577 | else { 1578 | Serial.println(nfcStatusMessage[4]); 1579 | mfrc522.PICC_HaltA(); 1580 | mfrc522.PCD_StopCrypto1(); 1581 | return 0; 1582 | } 1583 | } 1584 | 1585 | // prints nfc tag data 1586 | void printNfcTagData(uint8_t dataBuffer[], uint8_t dataBufferSize, bool cr) { 1587 | for (uint8_t i = 0; i < dataBufferSize; i++) { 1588 | Serial.print(dataBuffer[i] < 0x10 ? " 0" : " "); 1589 | Serial.print(dataBuffer[i], HEX); 1590 | } 1591 | if (cr) Serial.println(); 1592 | } 1593 | 1594 | // prints nfc tag type 1595 | void printNfcTagType(MFRC522::PICC_Type nfcTagType) { 1596 | switch (nfcTagType) { 1597 | case MFRC522::PICC_TYPE_MIFARE_MINI: {} 1598 | case MFRC522::PICC_TYPE_MIFARE_1K: {} 1599 | case MFRC522::PICC_TYPE_MIFARE_4K: { 1600 | Serial.print(F("cl")); 1601 | break; 1602 | } 1603 | case MFRC522::PICC_TYPE_MIFARE_UL: { 1604 | Serial.print(F("ul|nt")); 1605 | break; 1606 | } 1607 | default: { 1608 | Serial.print(F("??")); 1609 | break; 1610 | } 1611 | } 1612 | Serial.print(nfcStatusMessage[0]); 1613 | } 1614 | 1615 | // starts, stops and checks the shutdown timer 1616 | void shutdownTimer(uint8_t timerAction) { 1617 | static uint64_t shutdownMillis = 0; 1618 | 1619 | switch (timerAction) { 1620 | case START: { 1621 | if (preference.shutdownMinutes != 0) shutdownMillis = millis() + (preference.shutdownMinutes * 60000); 1622 | else shutdownMillis = 0; 1623 | break; 1624 | } 1625 | case STOP: { 1626 | shutdownMillis = 0; 1627 | break; 1628 | } 1629 | case CHECK: { 1630 | if (shutdownMillis != 0 && millis() > shutdownMillis) { 1631 | shutdownTimer(SHUTDOWN); 1632 | } 1633 | break; 1634 | } 1635 | case SHUTDOWN: { 1636 | #if defined STATUSLED ^ defined STATUSLEDRGB 1637 | statusLedUpdate(OFF, 0, 0, 0, 0); 1638 | #endif 1639 | #if defined POLOLUSWITCH 1640 | digitalWrite(shutdownPin, HIGH); 1641 | #else 1642 | digitalWrite(shutdownPin, LOW); 1643 | #endif 1644 | mfrc522.PCD_AntennaOff(); 1645 | mfrc522.PCD_SoftPowerDown(); 1646 | mp3.sleep(); 1647 | set_sleep_mode(SLEEP_MODE_PWR_DOWN); 1648 | cli(); 1649 | sleep_mode(); 1650 | break; 1651 | } 1652 | default: { 1653 | break; 1654 | } 1655 | } 1656 | } 1657 | 1658 | // reads, writes, migrates and resets preferences in eeprom 1659 | void preferences(uint8_t preferenceAction) { 1660 | Serial.print(F("prefs ")); 1661 | switch (preferenceAction) { 1662 | case READ: { 1663 | Serial.println(F("read")); 1664 | EEPROM.get(100, preference); 1665 | if (preference.cookie != preferenceCookie) preferences(RESET); 1666 | else { 1667 | Serial.print(F(" v")); 1668 | Serial.println(preference.version); 1669 | preferences(MIGRATE); 1670 | } 1671 | break; 1672 | } 1673 | case WRITE: { 1674 | Serial.println(F("write")); 1675 | EEPROM.put(100, preference); 1676 | break; 1677 | } 1678 | case MIGRATE: { 1679 | Serial.println(F("migrate")); 1680 | // prepared for future preferences migration 1681 | switch (preference.version) { 1682 | //case 1: { 1683 | // Serial.println(F(" v1->v2")); 1684 | // preference.version = 2; 1685 | // } 1686 | //case 2: { 1687 | // Serial.println(F(" v2->v3")); 1688 | // preference.version = 3; 1689 | // preferences(WRITE); 1690 | // break; 1691 | // } 1692 | default: { 1693 | Serial.println(F(" -")); 1694 | break; 1695 | } 1696 | } 1697 | break; 1698 | } 1699 | case RESET: { 1700 | Serial.println(F("reset")); 1701 | preference.cookie = preferenceCookie; 1702 | preference.version = preferenceVersion; 1703 | preference.mp3StartVolume = mp3StartVolumeDefault; 1704 | preference.mp3MaxVolume = mp3MaxVolumeDefault; 1705 | preference.mp3MenuVolume = mp3MenuVolumeDefault; 1706 | preference.mp3Equalizer = mp3EqualizerDefault; 1707 | preference.shutdownMinutes = shutdownMinutesDefault; 1708 | memcpy(preference.irRemoteUserCodes, irRemoteUserCodesDefault, 14); 1709 | preferences(WRITE); 1710 | break; 1711 | } 1712 | case RESET_PROGRESS: { 1713 | Serial.println(F("reset progress")); 1714 | for (uint16_t i = 1; i < 100; i++) EEPROM.update(i, 0); 1715 | break; 1716 | } 1717 | default: { 1718 | break; 1719 | } 1720 | } 1721 | } 1722 | 1723 | // interactively prompts the user for options 1724 | uint8_t prompt(uint8_t promptOptions, uint16_t promptHeading, uint16_t promptOffset, uint8_t promptCurrent, uint8_t promptFolder, bool promptPreview, bool promptChangeVolume) { 1725 | uint8_t promptResult = promptCurrent; 1726 | 1727 | mp3.playMp3FolderTrack(promptHeading); 1728 | while (true) { 1729 | playback.isPlaying = !digitalRead(mp3BusyPin); 1730 | checkForInput(); 1731 | // serial console input 1732 | if (Serial.available() > 0) { 1733 | uint32_t promptResultSerial = Serial.parseInt(); 1734 | if (promptResultSerial != 0 && promptResultSerial <= promptOptions) { 1735 | Serial.println(promptResultSerial); 1736 | return (uint8_t)promptResultSerial; 1737 | } 1738 | } 1739 | // button 0 (middle) press or ir remote play/pause: confirm selection 1740 | if ((inputEvent == B0P || inputEvent == IRP) && promptResult != 0) { 1741 | if (promptPreview && !playback.isPlaying) { 1742 | if (promptFolder == 0) mp3.playFolderTrack(promptResult, 1); 1743 | else mp3.playFolderTrack(promptFolder, promptResult); 1744 | } 1745 | else return promptResult; 1746 | } 1747 | // button 0 (middle) double click or ir remote center: announce current folder, track number or option 1748 | else if ((inputEvent == B0D || inputEvent == IRC) && promptResult != 0) { 1749 | if (promptPreview && playback.isPlaying) mp3.playAdvertisement(promptResult); 1750 | else if (promptPreview && !playback.isPlaying) { 1751 | if (promptFolder == 0) mp3.playFolderTrack(promptResult, 1); 1752 | else mp3.playFolderTrack(promptFolder, promptResult); 1753 | } 1754 | else { 1755 | if (promptChangeVolume) mp3.setVolume(promptResult + promptOffset); 1756 | mp3.playMp3FolderTrack(promptResult + promptOffset); 1757 | } 1758 | } 1759 | // button 1 (right) press or ir remote up: next folder, track number or option 1760 | else if (inputEvent == B1P || inputEvent == IRU) { 1761 | promptResult = min(promptResult + 1, promptOptions); 1762 | Serial.println(promptResult); 1763 | if (promptPreview) { 1764 | if (promptFolder == 0) mp3.playFolderTrack(promptResult, 1); 1765 | else mp3.playFolderTrack(promptFolder, promptResult); 1766 | } 1767 | else { 1768 | if (promptChangeVolume) mp3.setVolume(promptResult + promptOffset); 1769 | mp3.playMp3FolderTrack(promptResult + promptOffset); 1770 | } 1771 | } 1772 | // button 2 (left) press or ir remote up: previous folder, track number or option 1773 | else if (inputEvent == B2P || inputEvent == IRD) { 1774 | promptResult = max(promptResult - 1, 1); 1775 | Serial.println(promptResult); 1776 | if (promptPreview) { 1777 | if (promptFolder == 0) mp3.playFolderTrack(promptResult, 1); 1778 | else mp3.playFolderTrack(promptFolder, promptResult); 1779 | } 1780 | else { 1781 | if (promptChangeVolume) mp3.setVolume(promptResult + promptOffset); 1782 | mp3.playMp3FolderTrack(promptResult + promptOffset); 1783 | } 1784 | } 1785 | // button 0 (middle) hold for 2 sec or ir remote menu: cancel 1786 | else if (inputEvent == B0H || inputEvent == IRM) { 1787 | Serial.println(F("cancel")); 1788 | return 0; 1789 | } 1790 | // button 1 (right) hold or ir remote right: jump 10 folders, tracks or options forward 1791 | else if (inputEvent == B1H || inputEvent == B4P || inputEvent == IRR) { 1792 | promptResult = min(promptResult + 10, promptOptions); 1793 | Serial.println(promptResult); 1794 | if (promptChangeVolume) mp3.setVolume(promptResult + promptOffset); 1795 | mp3.playMp3FolderTrack(promptResult + promptOffset); 1796 | } 1797 | // button 2 (left) hold or ir remote left: jump 10 folders, tracks or options backwards 1798 | else if (inputEvent == B2H || inputEvent == B3P || inputEvent == IRL) { 1799 | promptResult = max(promptResult - 10, 1); 1800 | Serial.println(promptResult); 1801 | if (promptChangeVolume) mp3.setVolume(promptResult + promptOffset); 1802 | mp3.playMp3FolderTrack(promptResult + promptOffset); 1803 | } 1804 | #if defined STATUSLED ^ defined STATUSLEDRGB 1805 | statusLedUpdate(BLINK, 255, 255, 0, 500); 1806 | #endif 1807 | mp3.loop(); 1808 | } 1809 | } 1810 | 1811 | // parents menu, offers various settings only parents do 1812 | void parentsMenu() { 1813 | #if defined PINCODE 1814 | if (!enterPinCode()) return; 1815 | #endif 1816 | 1817 | playback.playListMode = false; 1818 | 1819 | // set volume to menu volume 1820 | mp3.setVolume(preference.mp3MenuVolume); 1821 | 1822 | switchButtonConfiguration(CONFIG); 1823 | shutdownTimer(STOP); 1824 | 1825 | while (true) { 1826 | Serial.println(F("parents")); 1827 | uint8_t selectedOption = prompt(10, 900, 909, 0, 0, false, false); 1828 | // cancel 1829 | if (selectedOption == 0) { 1830 | mp3.playMp3FolderTrack(904); 1831 | waitPlaybackToFinish(255, 255, 0, 100); 1832 | break; 1833 | } 1834 | // erase tag 1835 | else if (selectedOption == 1) { 1836 | Serial.println(F("erase tag")); 1837 | mp3.playMp3FolderTrack(920); 1838 | // loop until tag is erased 1839 | uint8_t writeNfcTagStatus = 0; 1840 | while (!writeNfcTagStatus) { 1841 | checkForInput(); 1842 | // button 0 (middle) hold for 2 sec or ir remote menu: cancel erase nfc tag 1843 | if (inputEvent == B0H || inputEvent == IRM) { 1844 | Serial.println(F("cancel")); 1845 | mp3.playMp3FolderTrack(923); 1846 | waitPlaybackToFinish(255, 0, 0, 100); 1847 | break; 1848 | } 1849 | // wait for nfc tag, erase once detected 1850 | if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) { 1851 | uint8_t bytesToWrite[16] = {}; 1852 | writeNfcTagStatus = writeNfcTagData(bytesToWrite, sizeof(bytesToWrite)); 1853 | if (writeNfcTagStatus == 1) { 1854 | mp3.playMp3FolderTrack(921); 1855 | waitPlaybackToFinish(0, 255, 0, 100); 1856 | } 1857 | else mp3.playMp3FolderTrack(922); 1858 | } 1859 | #if defined STATUSLED ^ defined STATUSLEDRGB 1860 | statusLedUpdate(BLINK, 255, 0, 255, 500); 1861 | #endif 1862 | mp3.loop(); 1863 | } 1864 | } 1865 | // startup volume 1866 | else if (selectedOption == 2) { 1867 | Serial.println(F("start vol")); 1868 | uint8_t promptResult = prompt(preference.mp3MaxVolume, 930, 0, preference.mp3StartVolume, 0, false, true); 1869 | if (promptResult != 0) { 1870 | preference.mp3StartVolume = promptResult; 1871 | preferences(WRITE); 1872 | // set volume to menu volume 1873 | mp3.setVolume(preference.mp3MenuVolume); 1874 | mp3.playMp3FolderTrack(901); 1875 | waitPlaybackToFinish(0, 255, 0, 100); 1876 | } 1877 | } 1878 | // maximum volume 1879 | else if (selectedOption == 3) { 1880 | Serial.println(F("max vol")); 1881 | uint8_t promptResult = prompt(30, 931, 0, preference.mp3MaxVolume, 0, false, true); 1882 | if (promptResult != 0) { 1883 | preference.mp3MaxVolume = promptResult; 1884 | // startup volume can't be higher than maximum volume 1885 | preference.mp3StartVolume = min(preference.mp3StartVolume, preference.mp3MaxVolume); 1886 | preferences(WRITE); 1887 | // set volume to menu volume 1888 | mp3.setVolume(preference.mp3MenuVolume); 1889 | mp3.playMp3FolderTrack(901); 1890 | waitPlaybackToFinish(0, 255, 0, 100); 1891 | } 1892 | } 1893 | // parents volume 1894 | else if (selectedOption == 4) { 1895 | Serial.println(F("menu vol")); 1896 | uint8_t promptResult = prompt(30, 932, 0, preference.mp3MenuVolume, 0, false, true); 1897 | if (promptResult != 0) { 1898 | preference.mp3MenuVolume = promptResult; 1899 | preferences(WRITE); 1900 | // set volume to menu volume 1901 | mp3.setVolume(preference.mp3MenuVolume); 1902 | mp3.playMp3FolderTrack(901); 1903 | waitPlaybackToFinish(0, 255, 0, 100); 1904 | } 1905 | } 1906 | // equalizer 1907 | else if (selectedOption == 5) { 1908 | Serial.println(F("eq")); 1909 | uint8_t promptResult = prompt(6, 940, 940, preference.mp3Equalizer, 0, false, false); 1910 | if (promptResult != 0) { 1911 | preference.mp3Equalizer = promptResult; 1912 | mp3.setEq((DfMp3_Eq)(preference.mp3Equalizer - 1)); 1913 | preferences(WRITE); 1914 | mp3.playMp3FolderTrack(901); 1915 | waitPlaybackToFinish(0, 255, 0, 100); 1916 | } 1917 | } 1918 | // learn ir remote 1919 | else if (selectedOption == 6) { 1920 | #if defined IRREMOTE 1921 | Serial.println(F("learn remote")); 1922 | for (uint8_t i = 0; i < 7; i++) { 1923 | mp3.playMp3FolderTrack(951 + i); 1924 | waitPlaybackToFinish(0, 0, 255, 500); 1925 | // clear ir receive buffer 1926 | IrReceiver.resume(); 1927 | // wait for ir signal 1928 | while (!IrReceiver.decode()) { 1929 | #if defined STATUSLED ^ defined STATUSLEDRGB 1930 | statusLedUpdate(BLINK, 0, 0, 255, 300); 1931 | #endif 1932 | } 1933 | // process only codes which don't have the repeat flag set 1934 | if (!(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)) { 1935 | uint16_t irRemoteCode = IrReceiver.decodedIRData.command; 1936 | Serial.print(F("ir code: 0x")); 1937 | Serial.print(irRemoteCode <= 0x0010 ? "0" : ""); 1938 | Serial.print(irRemoteCode <= 0x0100 ? "0" : ""); 1939 | Serial.print(irRemoteCode <= 0x1000 ? "0" : ""); 1940 | Serial.println(irRemoteCode, HEX); 1941 | preference.irRemoteUserCodes[i] = irRemoteCode; 1942 | #if defined STATUSLED ^ defined STATUSLEDRGB 1943 | statusLedUpdate(BURST4, 0, 255, 0, 0); 1944 | #endif 1945 | } 1946 | // key was held down, repeat last question 1947 | else { 1948 | i--; 1949 | #if defined STATUSLED ^ defined STATUSLEDRGB 1950 | statusLedUpdate(BURST4, 255, 0, 0, 0); 1951 | #endif 1952 | } 1953 | mp3.loop(); 1954 | } 1955 | preferences(WRITE); 1956 | mp3.playMp3FolderTrack(901); 1957 | waitPlaybackToFinish(0, 255, 0, 100); 1958 | #else 1959 | mp3.playMp3FolderTrack(950); 1960 | waitPlaybackToFinish(255, 0, 0, 100); 1961 | #endif 1962 | } 1963 | // shutdown timer 1964 | else if (selectedOption == 7) { 1965 | Serial.println(F("timer")); 1966 | uint8_t promptResult = prompt(7, 960, 960, 0, 0, false, false); 1967 | if (promptResult != 0) { 1968 | switch (promptResult) { 1969 | case 1: { 1970 | preference.shutdownMinutes = 5; 1971 | break; 1972 | } 1973 | case 2: { 1974 | preference.shutdownMinutes = 10; 1975 | break; 1976 | } 1977 | case 3: { 1978 | preference.shutdownMinutes = 15; 1979 | break; 1980 | } 1981 | case 4: { 1982 | preference.shutdownMinutes = 20; 1983 | break; 1984 | } 1985 | case 5: { 1986 | preference.shutdownMinutes = 30; 1987 | break; 1988 | } 1989 | case 6: { 1990 | preference.shutdownMinutes = 60; 1991 | break; 1992 | } 1993 | case 7: { 1994 | preference.shutdownMinutes = 0; 1995 | break; 1996 | } 1997 | default: { 1998 | break; 1999 | } 2000 | } 2001 | preferences(WRITE); 2002 | mp3.playMp3FolderTrack(901); 2003 | waitPlaybackToFinish(0, 255, 0, 100); 2004 | } 2005 | } 2006 | // reset progress 2007 | else if (selectedOption == 8) { 2008 | preferences(RESET_PROGRESS); 2009 | mp3.playMp3FolderTrack(902); 2010 | waitPlaybackToFinish(0, 255, 0, 100); 2011 | } 2012 | // reset preferences 2013 | else if (selectedOption == 9) { 2014 | preferences(RESET); 2015 | mp3.setVolume(preference.mp3MenuVolume); 2016 | mp3.setEq((DfMp3_Eq)(preference.mp3Equalizer - 1)); 2017 | mp3.playMp3FolderTrack(903); 2018 | waitPlaybackToFinish(0, 255, 0, 100); 2019 | } 2020 | // manual box shutdown 2021 | else if (selectedOption == 10) { 2022 | Serial.println(F("manual shut")); 2023 | shutdownTimer(SHUTDOWN); 2024 | } 2025 | mp3.loop(); 2026 | } 2027 | 2028 | // restore playback volume, can't be higher than maximum volume 2029 | mp3.setVolume(playback.mp3CurrentVolume = min(playback.mp3CurrentVolume, preference.mp3MaxVolume)); 2030 | 2031 | switchButtonConfiguration(PAUSE); 2032 | shutdownTimer(START); 2033 | inputEvent = NOP; 2034 | } 2035 | 2036 | #if defined PINCODE 2037 | // requests pin code from user via buttons or ir remote 2038 | bool enterPinCode() { 2039 | uint8_t pinCodeEntered[pinCodeLength]; 2040 | uint8_t pinCodeSlot = 0; 2041 | uint64_t cancelEnterPinCodeMillis = millis() + enterPinCodeTimeout; 2042 | bool pinCodeMatch = true; 2043 | playback.playListMode = false; 2044 | 2045 | // set volume to menu volume 2046 | mp3.setVolume(preference.mp3MenuVolume); 2047 | 2048 | switchButtonConfiguration(PIN); 2049 | shutdownTimer(STOP); 2050 | 2051 | Serial.println(F("pin?")); 2052 | mp3.playMp3FolderTrack(810); 2053 | while (true) { 2054 | checkForInput(); 2055 | // map ir inputs to corresponding button inputs 2056 | if (inputEvent >= 16) inputEvent = pinCodeIrToButtonMapping[inputEvent - 16]; 2057 | // button 0 (middle) hold for 2 sec or ir remote menu: cancel 2058 | if (inputEvent == B0H || inputEvent == IRM || millis() > cancelEnterPinCodeMillis) { 2059 | Serial.println(F("cancel")); 2060 | mp3.playMp3FolderTrack(811); 2061 | waitPlaybackToFinish(255, 0, 0, 100); 2062 | 2063 | // restore playback volume, can't be higher than maximum volume 2064 | mp3.setVolume(playback.mp3CurrentVolume = min(playback.mp3CurrentVolume, preference.mp3MaxVolume)); 2065 | 2066 | switchButtonConfiguration(PAUSE); 2067 | shutdownTimer(START); 2068 | inputEvent = NOP; 2069 | 2070 | return false; 2071 | } 2072 | // record inputs 2073 | if (inputEvent != NOP) pinCodeEntered[pinCodeSlot++] = inputEvent; 2074 | // if the complete pin code has been recorded 2075 | if (pinCodeSlot == pinCodeLength) { 2076 | // compare entered with stored pin code 2077 | for (uint8_t i = 0; i < pinCodeLength; i++) if (pinCode[i] != pinCodeEntered[i]) pinCodeMatch = false; 2078 | // we have a match, exit 2079 | if (pinCodeMatch) { 2080 | // restore playback volume, can't be higher than maximum volume 2081 | mp3.setVolume(playback.mp3CurrentVolume = min(playback.mp3CurrentVolume, preference.mp3MaxVolume)); 2082 | 2083 | switchButtonConfiguration(PAUSE); 2084 | shutdownTimer(START); 2085 | inputEvent = NOP; 2086 | 2087 | return true; 2088 | } 2089 | // we don't have a match, repeat 2090 | else { 2091 | Serial.println(F("pin?")); 2092 | mp3.playMp3FolderTrack(810); 2093 | cancelEnterPinCodeMillis = millis() + enterPinCodeTimeout; 2094 | pinCodeSlot = 0; 2095 | pinCodeMatch = true; 2096 | #if defined STATUSLED ^ defined STATUSLEDRGB 2097 | statusLedUpdate(BURST4, 255, 0, 0, 0); 2098 | #endif 2099 | } 2100 | } 2101 | #if defined STATUSLED ^ defined STATUSLEDRGB 2102 | statusLedUpdate(BLINK, 255, 255, 0, 500); 2103 | #endif 2104 | mp3.loop(); 2105 | } 2106 | } 2107 | #endif 2108 | 2109 | #if defined STATUSLED ^ defined STATUSLEDRGB 2110 | // updates status led(s) with various pulse, blink or burst patterns 2111 | void statusLedUpdate(uint8_t statusLedAction, uint8_t red, uint8_t green, uint8_t blue, uint16_t statusLedUpdateInterval) { 2112 | static bool statusLedState = true; 2113 | static bool statusLedDirection = false; 2114 | static int16_t statusLedFade = 255; 2115 | static uint64_t statusLedOldMillis; 2116 | 2117 | if (millis() - statusLedOldMillis >= statusLedUpdateInterval) { 2118 | statusLedOldMillis = millis(); 2119 | switch (statusLedAction) { 2120 | case OFF: { 2121 | statusLedUpdateHal(red, green, blue, 0); 2122 | break; 2123 | } 2124 | case SOLID: { 2125 | statusLedFade = 255; 2126 | statusLedUpdateHal(red, green, blue, 255); 2127 | break; 2128 | } 2129 | case PULSE: { 2130 | if (statusLedDirection) { 2131 | statusLedFade += 10; 2132 | if (statusLedFade >= 255) { 2133 | statusLedFade = 255; 2134 | statusLedDirection = !statusLedDirection; 2135 | } 2136 | } 2137 | else { 2138 | statusLedFade -= 10; 2139 | if (statusLedFade <= 0) { 2140 | statusLedFade = 0; 2141 | statusLedDirection = !statusLedDirection; 2142 | } 2143 | } 2144 | statusLedUpdateHal(red, green, blue, statusLedFade); 2145 | break; 2146 | } 2147 | case BLINK: { 2148 | statusLedState = !statusLedState; 2149 | if (statusLedState) statusLedUpdateHal(red, green, blue, 255); 2150 | else statusLedUpdateHal(0, 0, 0, 0); 2151 | break; 2152 | } 2153 | case BURST2: { 2154 | for (uint8_t i = 0; i < 4; i++) { 2155 | statusLedState = !statusLedState; 2156 | if (statusLedState) statusLedUpdateHal(red, green, blue, 255); 2157 | else statusLedUpdateHal(0, 0, 0, 0); 2158 | delay(100); 2159 | } 2160 | break; 2161 | } 2162 | case BURST4: { 2163 | for (uint8_t i = 0; i < 8; i++) { 2164 | statusLedState = !statusLedState; 2165 | if (statusLedState) statusLedUpdateHal(red, green, blue, 255); 2166 | else statusLedUpdateHal(0, 0, 0, 0); 2167 | delay(100); 2168 | } 2169 | break; 2170 | } 2171 | case BURST8: { 2172 | for (uint8_t i = 0; i < 16; i++) { 2173 | statusLedState = !statusLedState; 2174 | if (statusLedState) statusLedUpdateHal(red, green, blue, 255); 2175 | else statusLedUpdateHal(0, 0, 0, 0); 2176 | delay(100); 2177 | } 2178 | break; 2179 | } 2180 | default: { 2181 | break; 2182 | } 2183 | } 2184 | } 2185 | } 2186 | 2187 | // abstracts status led(s) depending on what hardware is actually used (vanilla or ws281x led(s)) 2188 | void statusLedUpdateHal([[maybe_unused]] uint8_t red, [[maybe_unused]] uint8_t green, [[maybe_unused]] uint8_t blue, int16_t brightness) { 2189 | #if defined STATUSLEDRGB 2190 | cRGB rgbLedColor; 2191 | 2192 | // apply brightness and max brightness 2193 | rgbLedColor.r = (uint8_t)(((brightness / 255.0) * red) * (min(statusLedMaxBrightness, 100) / 100.0)); 2194 | rgbLedColor.g = (uint8_t)(((brightness / 255.0) * green) * (min(statusLedMaxBrightness, 100) / 100.0)); 2195 | rgbLedColor.b = (uint8_t)(((brightness / 255.0) * blue) * (min(statusLedMaxBrightness, 100) / 100.0)); 2196 | 2197 | // update led buffer 2198 | for (uint8_t i = 0; i < statusLedCount; i++) rgbLed.set_crgb_at(i, rgbLedColor); 2199 | 2200 | // send out the updated buffer 2201 | rgbLed.sync(); 2202 | #else 2203 | // update vanilla led 2204 | analogWrite(statusLedPin, (uint8_t)(brightness)); 2205 | #endif 2206 | } 2207 | #endif 2208 | -------------------------------------------------------------------------------- /tools/add_lead_in_messages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Adds a lead-in message to each mp3 file of a directory storing the result in another directory. 4 | # So - when played e.g. on a TonUINO - you first will hear the title of the track, then the track itself. 5 | 6 | 7 | import argparse, base64, json, os, re, subprocess, sys, text_to_speech 8 | 9 | 10 | argFormatter = lambda prog: argparse.RawDescriptionHelpFormatter(prog, max_help_position=27, width=100) 11 | argparser = text_to_speech.PatchedArgumentParser( 12 | description= 13 | 'Adds a lead-in message to each mp3 file of a directory storing the result in another directory.\n' + 14 | 'So - when played e.g. on a TonUINO - you first will hear the title of the track, then the track itself.\n\n' + 15 | text_to_speech.textToSpeechDescription, 16 | usage='%(prog)s -i my/source/dir -o my/output/dir [optional arguments...]', 17 | formatter_class=argFormatter) 18 | argparser.add_argument('-i', '--input', type=str, required=True, help='The input directory or mp3 file to process (input won\'t be changed)') 19 | argparser.add_argument('-o', '--output', type=str, required=True, help='The output directory where to write the mp3 files (will be created if not existing)') 20 | text_to_speech.addArgumentsToArgparser(argparser) 21 | argparser.add_argument('--file-regex', type=str, default=None, help="The regular expression to use for parsing the mp3 file name. If missing the whole file name except a leading number will be used as track title.") 22 | argparser.add_argument('--title-pattern', type=str, default=None, help="The pattern to use as track title. May contain groups of `--file-regex`, e.g. '\\1'") 23 | argparser.add_argument('--add-numbering', action='store_true', help='Whether to add a three-digit number to the mp3 files (suitable for DFPlayer Mini)') 24 | argparser.add_argument('--dry-run', action='store_true', help='Dry run: Only prints what the script would do, without actually creating files') 25 | args = argparser.parse_args() 26 | 27 | text_to_speech.checkArgs(argparser, args) 28 | 29 | fileRegex = re.compile(args.file_regex if args.file_regex is not None else '\\d*(.*)') 30 | titlePattern = args.title_pattern if args.title_pattern is not None else '\\1' 31 | 32 | mp3FileIndex = 0 33 | 34 | 35 | def fail(msg): 36 | print('ERROR: ' + msg) 37 | sys.exit(1) 38 | 39 | 40 | def addLeadInMessage(inputPath, outputPath): 41 | global mp3FileIndex 42 | 43 | if not os.path.exists(inputPath): 44 | fail('Input does not exist: ' + os.path.abspath(inputPath)) 45 | 46 | if os.path.isdir(inputPath): 47 | if os.path.exists(outputPath): 48 | if not os.path.isdir(outputPath): 49 | fail('Input is a directory, but output isn\'t: ' + os.path.abspath(outputPath)) 50 | elif not args.dry_run: 51 | os.mkdir(outputPath) 52 | 53 | mp3FileIndex = 0 54 | for child in sorted(os.listdir(inputPath)): 55 | addLeadInMessage(os.path.join(inputPath, child), os.path.join(outputPath, child)) 56 | 57 | return 58 | 59 | inputFileNameSplit = os.path.splitext(os.path.basename(inputPath)) 60 | inputFileName = inputFileNameSplit[0] 61 | inputFileExt = inputFileNameSplit[1].lower() 62 | 63 | if inputFileExt != '.mp3': 64 | print('Ignoring {} (no mp3 file)'.format(os.path.abspath(inputPath))) 65 | return 66 | 67 | if args.add_numbering: 68 | outputPathSplit = os.path.split(outputPath) 69 | outputPath = os.path.join(outputPathSplit[0], '{:0>3}_{}'.format(mp3FileIndex + 1, outputPathSplit[1])) 70 | mp3FileIndex += 1 71 | 72 | if os.path.isfile(outputPath): 73 | print('Skipping {} (file already exists)'.format(os.path.abspath(outputPath))) 74 | return 75 | 76 | text = re.sub(fileRegex, titlePattern, inputFileName).replace('_', ' ').strip() 77 | print('Adding lead-in "{}" to {}'.format(text, os.path.abspath(outputPath))) 78 | 79 | if not args.dry_run: 80 | tempLeadInFile = 'temp-lead-in.mp3' 81 | tempLeadInFileAdjusted = 'temp-lead-in_adjusted.mp3' 82 | tempListFile = 'temp-list.txt' 83 | tempTargetFile = 'temp-target.mp3' 84 | text_to_speech.textToSpeechUsingArgs(text=text, targetFile=tempLeadInFile, args=args) 85 | 86 | # Adjust sample rate and mono/stereo 87 | print('Detecting sample rate and channels') 88 | detectionInfo = detectAudioData(inputPath) 89 | if detectionInfo is None: 90 | # We can't adjust 91 | print('Detecting sample rate and channels failed -> Skipping adjustment') 92 | tempLeadInFileAdjusted = tempLeadInFile 93 | else: 94 | print('Adjust sample rate to {} and channels to {}'.format(detectionInfo['sampleRate'], detectionInfo['channels'])) 95 | subprocess.call([ 'ffmpeg', '-i', tempLeadInFile, '-vn', '-ar', detectionInfo['sampleRate'], '-ac', detectionInfo['channels'], tempLeadInFileAdjusted ]) 96 | 97 | print('Concat') 98 | # Use ffmpeg Concat demuxer 99 | with open(tempListFile, 'w') as f: 100 | f.write("file " + "'" + tempLeadInFileAdjusted + "'") 101 | f.write("\n") 102 | f.write("file " + "'" + inputPath + "'") 103 | subprocess.call([ 'ffmpeg', '-f', 'concat', '-safe', '0', '-i', tempListFile, '-c', 'copy', tempTargetFile ]) 104 | # Copy metadata from input file 105 | subprocess.call([ 'ffmpeg', '-i', inputPath, '-i', tempTargetFile, '-map', '1', '-c', 'copy', '-map_metadata', '0', outputPath ]) 106 | 107 | os.remove(tempLeadInFile) 108 | os.remove(tempLeadInFileAdjusted) 109 | os.remove(tempListFile) 110 | os.remove(tempTargetFile) 111 | print('\n') 112 | 113 | 114 | def detectAudioData(mp3File): 115 | try: 116 | output = subprocess.check_output([ 'ffmpeg', '-i', mp3File, '-hide_banner' ], stderr=subprocess.STDOUT) 117 | except Exception as e: 118 | output = str(e.output) 119 | 120 | match = re.match('.*Stream #\\d+:\\d+: Audio: mp3, (\\d+) Hz, (mono|stereo), .*', output, re.S) 121 | if match: 122 | return { 123 | 'sampleRate': match.group(1), 124 | 'channels': '2' if match.group(2) == 'stereo' else '1' 125 | } 126 | else: 127 | return None 128 | 129 | 130 | if not os.path.exists(args.output) and not args.dry_run: 131 | outputParent = os.path.dirname(os.path.abspath(args.output)) 132 | if not os.path.isdir(outputParent): 133 | fail('Parent of output is no directory: ' + os.path.abspath(outputParent)) 134 | 135 | addLeadInMessage(args.input, args.output) 136 | -------------------------------------------------------------------------------- /tools/create_audio_messages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Creates the audio messages needed by TonUINO. 4 | 5 | 6 | import argparse, os, re, shutil, sys, text_to_speech 7 | 8 | 9 | if __name__ == '__main__': 10 | argFormatter = lambda prog: argparse.RawDescriptionHelpFormatter(prog, max_help_position=30, width=100) 11 | argparser = text_to_speech.PatchedArgumentParser( 12 | description= 13 | 'Creates the audio messages needed by TonUINO.\n\n' + 14 | text_to_speech.textToSpeechDescription, 15 | usage='%(prog)s [optional arguments...]', 16 | formatter_class=argFormatter) 17 | argparser.add_argument('-i', '--input', type=str, default='.', help='The directory where `audio_messages_*.txt` files are located. (default: current directory)') 18 | argparser.add_argument('-o', '--output', type=str, default='sd-card', help='The directory where to create the audio messages. (default: `sd-card`)') 19 | text_to_speech.addArgumentsToArgparser(argparser) 20 | argparser.add_argument('--skip-numbers', action='store_true', help='If set, no number messages will be generated (`0001.mp3` - `0255.mp3`)') 21 | args = argparser.parse_args() 22 | 23 | 24 | text_to_speech.checkArgs(argparser, args) 25 | 26 | audioMessagesFile = '{}/audio_messages_{}.txt'.format(args.input, args.lang) 27 | if not os.path.isfile(audioMessagesFile): 28 | print('Input file does not exist: ' + os.path.abspath(audioMessagesFile)) 29 | exit(1) 30 | 31 | targetDir = args.output 32 | if os.path.isdir(targetDir): 33 | print("Directory `" + targetDir + "` already exists.") 34 | exit(1) 35 | else: 36 | os.mkdir(targetDir) 37 | os.mkdir(targetDir + '/advert') 38 | os.mkdir(targetDir + '/mp3') 39 | 40 | 41 | if not args.skip_numbers: 42 | for i in range(1,256): 43 | targetFile1 = '{}/mp3/{:0>4}.mp3'.format(targetDir, i) 44 | targetFile2 = '{}/advert/{:0>4}.mp3'.format(targetDir, i) 45 | text_to_speech.textToSpeechUsingArgs(text='{}'.format(i), targetFile=targetFile1, args=args) 46 | shutil.copy(targetFile1, targetFile2) 47 | 48 | with open(audioMessagesFile) as f: 49 | lineRe = re.compile('^([^|]+)\\|(.*)$') 50 | for line in f: 51 | match = lineRe.match(line.strip()) 52 | if match: 53 | fileName = match.group(1) 54 | text = match.group(2) 55 | text_to_speech.textToSpeechUsingArgs(text=text, targetFile=targetDir + '/mp3/' + fileName, args=args) 56 | -------------------------------------------------------------------------------- /tools/text_to_speech.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Converts text into spoken language saved to an mp3 file. 4 | 5 | 6 | import argparse, base64, json, os, subprocess, sys 7 | try: 8 | import urllib.request 9 | except ImportError: 10 | print("WARNING: It looks like you are using an old version of Python. Please use Python 3 if you intend to use Google Text to Speech.") 11 | 12 | class PatchedArgumentParser(argparse.ArgumentParser): 13 | def error(self, message): 14 | sys.stderr.write('ERROR: %s\n\n' % message) 15 | self.print_help() 16 | sys.exit(2) 17 | 18 | 19 | sayVoiceByLang = { 20 | 'de': 'Anna', 21 | 'en': 'Samantha', 22 | 'fr': 'Thomas', 23 | 'nl': 'Xander', 24 | 'es': 'Monica', 25 | 'cz': 'Zuzana', 26 | 'it': 'Alice' 27 | } 28 | googleVoiceByLang = { 29 | 'de': { 'languageCode': 'de-DE', 'name': 'de-DE-Wavenet-C' }, 30 | 'en': { 'languageCode': 'en-US', 'name': 'en-US-Wavenet-C' }, 31 | 'fr': { 'languageCode': 'fr-FR', 'name': 'fr-FR-Wavenet-C' }, 32 | 'nl': { 'languageCode': 'nl-NL', 'name': 'nl-NL-Wavenet-A' }, 33 | 'es': { 'languageCode': 'es-ES', 'name': '' }, 34 | 'cz': { 'languageCode': 'cs-CZ', 'name': 'cs-CZ-Wavenet-A' }, 35 | 'it': { 'languageCode': 'it-IT', 'name': 'it-IT-Standard-B' } 36 | } 37 | amazonVoiceByLang = { 38 | # See: https://docs.aws.amazon.com/de_de/polly/latest/dg/voicelist.html 39 | 'de': 'Vicki', 40 | 'en': 'Joanna', 41 | 'fr': 'Celine', 42 | 'nl': 'Lotte', 43 | 'es': 'Lucia', 44 | 'it': 'Carla' 45 | } 46 | 47 | 48 | textToSpeechDescription = """ 49 | The following text-to-speech engines are supported: 50 | - With `--use-say` the text-to-speech engine of MacOS is used (command `say`). 51 | - With `--use-amazon` Amazon Polly is used. Requires the AWS CLI to be installed and configured. See: https://aws.amazon.com/cli/ 52 | - With `--use-google-key=ABCD` Google text-to-speech is used. See: https://cloud.google.com/text-to-speech/ 53 | 54 | Amazon Polly sounds best, Google text-to-speech is second, MacOS `say` sounds worst.' 55 | """.strip() 56 | 57 | def addArgumentsToArgparser(argparser): 58 | argparser.add_argument('--lang', choices=['de', 'en', 'fr', 'nl', 'es', 'cz', 'it'], default='de', help='The language (default: de)') 59 | argparser.add_argument('--use-say', action='store_true', default=None, help="If set, the MacOS tool `say` will be used.") 60 | argparser.add_argument('--use-amazon', action='store_true', default=None, help="If set, Amazon Polly is used. If missing the MacOS tool `say` will be used.") 61 | argparser.add_argument('--use-google-key', type=str, default=None, help="The API key of the Google text-to-speech account to use.") 62 | 63 | 64 | def checkArgs(argparser, args): 65 | if not args.use_say and not args.use_amazon and args.use_google_key is None: 66 | print('ERROR: You have to provide one of the arguments `--use-say`, `--use-amazon` or `--use-google-key`\n') 67 | argparser.print_help() 68 | sys.exit(2) 69 | if args.use_say: 70 | checkLanguage(sayVoiceByLang, args.lang, argparser) 71 | if args.use_google_key: 72 | checkLanguage(googleVoiceByLang, args.lang, argparser) 73 | if args.use_amazon: 74 | checkLanguage(amazonVoiceByLang, args.lang, argparser) 75 | 76 | def checkLanguage(dictionary, lang, argparser): 77 | if lang not in dictionary: 78 | print('ERROR: Language is not supported by selected text-to-speech engine\n') 79 | argparser.print_help() 80 | sys.exit(2) 81 | 82 | 83 | def textToSpeechUsingArgs(text, targetFile, args): 84 | textToSpeech(text, targetFile, lang=args.lang, useAmazon=args.use_amazon, useGoogleKey=args.use_google_key) 85 | 86 | 87 | def textToSpeech(text, targetFile, lang='de', useAmazon=False, useGoogleKey=None): 88 | print('\nGenerating: ' + targetFile + ' - ' + text) 89 | if useAmazon: 90 | response = subprocess.check_output(['aws', 'polly', 'synthesize-speech', '--output-format', 'mp3', 91 | '--voice-id', amazonVoiceByLang[lang], '--text-type', 'ssml', 92 | '--text', '' + text + '', 93 | targetFile]) 94 | elif useGoogleKey: 95 | responseJson = postJson( 96 | 'https://texttospeech.googleapis.com/v1/text:synthesize?key=' + useGoogleKey, 97 | { 98 | 'audioConfig': { 99 | 'audioEncoding': 'MP3', 100 | 'speakingRate': 1.0, 101 | 'pitch': 2.0, # Default is 0.0 102 | 'sampleRateHertz': 44100, 103 | 'effectsProfileId': [ 'small-bluetooth-speaker-class-device' ] 104 | }, 105 | 'voice': googleVoiceByLang[lang], 106 | 'input': { 'text': text } 107 | } 108 | ) 109 | 110 | mp3Data = base64.b64decode(responseJson['audioContent']) 111 | 112 | with open(targetFile, 'wb') as f: 113 | f.write(mp3Data) 114 | else: 115 | subprocess.call([ 'say', '-v', sayVoiceByLang[lang], '-o', 'temp.aiff', text ]) 116 | subprocess.call([ 'ffmpeg', '-y', '-i', 'temp.aiff', '-acodec', 'libmp3lame', '-ab', '128k', '-ac', '1', targetFile ]) 117 | os.remove('temp.aiff') 118 | 119 | 120 | def postJson(postUrl, postBody, headers = {}): 121 | headers['Content-Type'] = 'application/json; charset=utf-8' 122 | postData = json.dumps(postBody).encode('utf-8') 123 | try: 124 | postRequest = urllib.request.Request(postUrl, postData, headers) 125 | with urllib.request.urlopen(postRequest) as req: 126 | postResponseData=req.read() 127 | return json.loads(postResponseData.decode()) 128 | except Exception as e: 129 | print(e) 130 | 131 | 132 | if __name__ == '__main__': 133 | argFormatter = lambda prog: argparse.RawDescriptionHelpFormatter(prog, max_help_position=30, width=100) 134 | argparser = PatchedArgumentParser( 135 | description= 136 | 'Converts text into spoken language saved to an mp3 file.\n\n' + 137 | textToSpeechDescription, 138 | usage='%(prog)s -t "This is my text" -o my-output.mp3 [optional arguments...]', 139 | formatter_class=argFormatter) 140 | argparser.add_argument('-t', '--text', type=str, required=True, help='The text to convert into spoken language.') 141 | argparser.add_argument('-o', '--output', type=str, required=True, help='The output mp3 file to create') 142 | addArgumentsToArgparser(argparser) 143 | args = argparser.parse_args() 144 | 145 | 146 | checkArgs(argparser, args) 147 | 148 | if os.path.exists(args.output): 149 | print('ERROR: Output file already exists: ' + os.path.abspath(args.output)) 150 | sys.exit(1) 151 | 152 | 153 | textToSpeechUsingArgs(text=args.text, targetFile=args.output, args=args) 154 | --------------------------------------------------------------------------------