pax_global_header00006660000000000000000000000064141144156400014512gustar00rootroot0000000000000052 comment=47c9fbaf55402bedb9f9f71269d4f0e7eb66c29e python-sense-emu-1.2/000077500000000000000000000000001411441564000145745ustar00rootroot00000000000000python-sense-emu-1.2/.gitignore000066400000000000000000000005411411441564000165640ustar00rootroot00000000000000*~ *.py[cdo] *.vim *.swp tags # Packages *.egg *.egg-info dist build eggs parts bin man var sdist develop-eggs .installed.cfg # Doc builds docs/_build/ # Installer logs pip-log.txt # Unit test / coverage reports coverage .coverage .tox .cache # Ignore virtualenvwrapper bits .venv # Translations *.mo # Generated images icons/*/ !icons/scalable/ python-sense-emu-1.2/LICENSE.txt000066400000000000000000001311201411441564000164150ustar00rootroot00000000000000Copyright (c) 2016 Raspberry Pi Foundation. The Sense HAT Emulator package is licensed under the terms of the Lesser GNU Public License version 2.1 or above, with the exception of the sense_emu_gui, sense_rec, sense_play, and sense_csv applications which are each licensed under the terms of the GNU General Public License version 2.0 or above. The full texts of these licenses can be found in the following sections. GNU Lesser General Public License v2.1 or later =============================================== GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble -------- The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION --------------------------------------------------------------- 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a. The modified work must itself be a software library. b. You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c. You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d. If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a. Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b. Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c. Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d. If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e. Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a. Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b. Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY ----------- 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS --------------------------- How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the library's name and an idea of what it does. Copyright (C) year name of author This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library 'Frob' (a library for tweaking knobs) written by James Random Hacker. signature of Ty Coon, 1 April 1990 Ty Coon, President of Vice That's all there is to it! GNU General Public License v2.0 or later ======================================== GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble -------- The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION --------------------------------------------------------------- 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a. You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b. You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c. If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a. Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b. Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c. Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY ----------- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS --------------------------- How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free software, and you are welcome to redistribute it under certain conditions; type 'show c' for details. The hypothetical commands 'show w' and 'show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than 'show w' and 'show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program 'Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice python-sense-emu-1.2/MANIFEST.in000066400000000000000000000000471411441564000163330ustar00rootroot00000000000000include README.rst include LICENSE.txt python-sense-emu-1.2/Makefile000066400000000000000000000125701411441564000162410ustar00rootroot00000000000000# vim: set noet sw=4 ts=4 fileencoding=utf-8: # External utilities PYTHON=python3 PIP=pip PYTEST=pytest TWINE=twine PYFLAGS= MSGINIT=msginit MSGMERGE=msgmerge MSGFMT=msgfmt XGETTEXT=xgettext GCS=glib-compile-schemas DEST_DIR=/ # Find the location of the GObject introspection libs and cairo (required for # the develop target) RTIMULIB:=$(wildcard /usr/lib/python3/dist-packages/RTIMU.*) CAIRO:=$(wildcard /usr/lib/python3/dist-packages/cairo) GI:=$(wildcard /usr/lib/python3/dist-packages/gi) GOBJECT:= GLIB:= # Calculate the base names of the distribution, the location of all source, # documentation, packaging, icon, and executable script files NAME:=$(shell $(PYTHON) $(PYFLAGS) setup.py --name) WHEEL_NAME:=$(subst -,_,$(NAME)) VER:=$(shell $(PYTHON) $(PYFLAGS) setup.py --version) PY_SOURCES:=$(shell \ $(PYTHON) $(PYFLAGS) setup.py egg_info >/dev/null 2>&1 && \ cat $(WHEEL_NAME).egg-info/SOURCES.txt | grep -v "\.egg-info" | grep -v "\.mo$$") DOC_SOURCES:=docs/conf.py \ $(wildcard docs/*.png) \ $(wildcard docs/*.svg) \ $(wildcard docs/*.dot) \ $(wildcard docs/*.mscgen) \ $(wildcard docs/*.gpi) \ $(wildcard docs/*.rst) \ $(wildcard docs/*.pdf) SUBDIRS:=icons # Calculate the name of all outputs DIST_WHEEL=dist/$(WHEEL_NAME)-$(VER)-py3-none-any.whl DIST_TAR=dist/$(NAME)-$(VER).tar.gz DIST_ZIP=dist/$(NAME)-$(VER).zip MAN_PAGES=man/sense_rec.1 man/sense_play.1 man/sense_csv.1 man/sense_emu_gui.1 POT_FILE=$(WHEEL_NAME)/locale/$(NAME).pot PO_FILES:=$(wildcard $(WHEEL_NAME)/locale/*.po) MO_FILES:=$(patsubst $(WHEEL_NAME)/locale/%.po,$(WHEEL_NAME)/locale/%/LC_MESSAGES/$(NAME).mo,$(PO_FILES)) GSCHEMA_FILES:=$(wildcard $(WHEEL_NAME)/*.gschema.xml) GSCHEMA_COMPILED=$(WHEEL_NAME)/gschemas.compiled # Default target all: @echo "make install - Install on local system" @echo "make develop - Install symlinks for development" @echo "make i18n - Update translation files" @echo "make gschema - Update gschema settings" @echo "make test - Run tests" @echo "make doc - Generate HTML and PDF documentation" @echo "make source - Create source package" @echo "make wheel - Generate a PyPI wheel package" @echo "make zip - Generate a source zip package" @echo "make tar - Generate a source tar package" @echo "make dist - Generate all packages" @echo "make clean - Get rid of all generated files" @echo "make release - Create and tag a new release" @echo "make upload - Upload the new release to repositories" install: $(SUBDIRS) $(MO_FILES) $(GSCHEMA_COMPILED) $(PYTHON) $(PYFLAGS) setup.py install --root $(DEST_DIR) doc: $(DOC_SOURCES) $(MAKE) -C docs clean $(MAKE) -C docs html $(MAKE) -C docs epub $(MAKE) -C docs latexpdf $(MAKE) $(MAN_PAGES) source: $(DIST_TAR) $(DIST_ZIP) wheel: $(DIST_WHEEL) zip: $(DIST_ZIP) tar: $(DIST_TAR) dist: $(DIST_WHEEL) $(DIST_TAR) $(DIST_ZIP) i18n: $(MO_FILES) $(PO_FILES) $(POT_FILE) gschema: $(GSCHEMA_COMPILED) develop: @# These have to be done separately to avoid a cockup... $(PIP) install -U setuptools $(PIP) install -U pip $(PIP) install -U twine $(PIP) install -U tox $(PIP) install -e .[doc,test] @# If we're in a venv, link the system's GObject Introspection (gi) into it ifeq ($(VIRTUAL_ENV),) @echo "Virtualenv not detected! You may need to link gi manually" else ifeq ($(RTIMULIB),) @echo "WARNING: RTIMULib not found. This is fine on non-Pi platforms" else ln -sf $(RTIMULIB) $(VIRTUAL_ENV)/lib/python*/site-packages/ endif ifeq ($(CAIRO),) @echo "ERROR: cairo not found. Install the python{,3}-cairo packages" else ln -sf $(CAIRO) $(VIRTUAL_ENV)/lib/python*/site-packages/ endif ifeq ($(GI),) @echo "ERROR: gi not found. Install the python{,3}-gi packages" else ln -sf $(GI) $(VIRTUAL_ENV)/lib/python*/site-packages/ endif ifneq ($(GLIB),) ln -sf $(GLIB) $(VIRTUAL_ENV)/lib/python*/site-packages/ endif ifneq ($(GOBJECT),) ln -sf $(GOBJECT) $(VIRTUAL_ENV)/lib/python*/site-packages/ endif endif test: $(PYTEST) clean: rm -fr dist/ build/ man/ .pytest_cache/ .mypy_cache/ $(WHEEL_NAME).egg-info/ tags .coverage for dir in $(SUBDIRS); do \ $(MAKE) -C $$dir clean; \ done find $(CURDIR) -name "*.pyc" -delete find $(CURDIR) -name "__pycache__" -delete tags: $(PY_SOURCES) ctags -R --exclude="build/*" --exclude="docs/*" --languages="Python" lint: $(PY_SOURCES) pylint $(WHEEL_NAME) $(SUBDIRS): $(MAKE) -C $@ $(MAN_PAGES): $(DOC_SOURCES) $(MAKE) -C docs man mkdir -p man/ cp build/man/*.[0-9] man/ $(POT_FILE): $(PY_SOURCES) $(XGETTEXT) -cI18N -o $@ $(filter %.py,$^) $(filter %.ui,$^) $(PO_FILES): $(POT_FILE) $(MSGMERGE) -U $@ $< $(MO_FILES): $(PO_FILES) mkdir -p $(dir $@) $(MSGFMT) $(patsubst $(WHEEL_NAME)/locale/%/LC_MESSAGES/$(NAME).mo,$(WHEEL_NAME)/locale/%.po,$@) -o $@ $(GSCHEMA_COMPILED): $(GSCHEMA_FILES) $(GCS) $(WHEEL_NAME) $(DIST_TAR): $(PY_SOURCES) $(MO_FILES) $(GSCHEMA_COMPILED) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats gztar $(DIST_ZIP): $(PY_SOURCES) $(MO_FILES) $(GSCHEMA_COMPILED) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats zip $(DIST_WHEEL): $(PY_SOURCES) $(MO_FILES) $(GSCHEMA_COMPILED) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py bdist_wheel release: $(MAKE) clean test -z "$(shell git status --porcelain)" git tag -s v$(VER) -m "Release v$(VER)" git push origin v$(VER) upload: $(DIST_TAR) $(DIST_WHEEL) $(TWINE) check $(DIST_TAR) $(DIST_WHEEL) $(TWINE) upload $(DIST_TAR) $(DIST_WHEEL) .PHONY: all install develop test doc source wheel zip tar dist clean tags release upload $(SUBDIRS) python-sense-emu-1.2/README.rst000066400000000000000000000024621411441564000162670ustar00rootroot00000000000000.. -*- rst -*- ================== Sense HAT Emulator ================== This package emulates the Raspberry Pi `Sense HAT`_. An interactive GTK application is provided to permit manipulation of the emulated sensors, along with command line utilities for recording and playing back sensor readings from an actual HAT. Links ===== * The library code is licensed under the `LGPL version 2.1`_ or above, while the applications ``sense_emu_gui``, ``sense_rec``, ``sense_play``, and ``sense_csv`` are licensed under the `GPL version 2.0`_ or above. * The `source code`_ can be obtained from GitHub, which also hosts the `bug tracker`_ * The `documentation`_ (which includes installation, API reference and example scripts) can be read on ReadTheDocs * Packages can be downloaded from `PyPI`_, but reading the installation instructions is likely to be more useful .. _Sense HAT: https://www.raspberrypi.org/products/sense-hat/ .. _source code: https://github.com/RPi-Distro/python-sense-emu .. _bug tracker: https://github.com/RPi-Distro/python-sense-emu/issues .. _documentation: https://sense-emu.readthedocs.io .. _PyPI: https://pypi.python.org/pypi/sense_emu/ .. _LGPL version 2.1: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html .. _GPL version 2.0: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html python-sense-emu-1.2/docs/000077500000000000000000000000001411441564000155245ustar00rootroot00000000000000python-sense-emu-1.2/docs/Makefile000066400000000000000000000173731411441564000171770ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../build DOT_DIAGRAMS := $(wildcard *.dot) MSC_DIAGRAMS := $(wildcard *.mscgen) GPI_DIAGRAMS := $(wildcard *.gpi) SVG_IMAGES := $(wildcard *.svg) $(DOT_DIAGRAMS:%.dot=%.svg) $(MSC_DIAGRAMS:%.mscgen=%.svg) PNG_IMAGES := $(wildcard *.png) $(GPI_DIAGRAMS:%.gpi=%.png) PDF_IMAGES := $(SVG_IMAGES:%.svg=%.pdf) $(GPI_DIAGRAMS:%.gpi=%.pdf) $(DOT_DIAGRAMS:%.dot=%.pdf) $(MSC_DIAGRAMS:%.mscgen=%.pdf) INKSCAPE_VER := $(shell inkscape --version | sed -ne '/^Inkscape/ s/^Inkscape \([0-9]\+\)\..*$$/\1/p') # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SVG_IMAGES) $(PNG_IMAGES) $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/picamera.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/picamera.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/picamera" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/picamera" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(PDF_IMAGES) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." %.svg: %.mscgen mscgen -T svg -o $@ $< %.svg: %.dot dot -T svg -o $@ $< %.png: %.gpi common.gp gnuplot -e "set term pngcairo size 640,480" $< > $@ ifeq ($(INKSCAPE_VER),0) %.png: %.svg inkscape --export-dpi 150 -e $@ $< %.pdf: %.svg inkscape -A $@ $< else %.png: %.svg inkscape --export-dpi 150 --export-type png -o $@ $< %.pdf: %.svg inkscape --export-type pdf -o $@ $< endif %.pdf: %.gpi common.gp gnuplot -e "set term pdfcairo size 10cm,7.5cm" $< > $@ %.pdf: %.mscgen mscgen -T eps -o - $< | ps2pdf -dEPSCrop - $@ .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext python-sense-emu-1.2/docs/_static/000077500000000000000000000000001411441564000171525ustar00rootroot00000000000000python-sense-emu-1.2/docs/_static/style_override.css000066400000000000000000000027721411441564000227330ustar00rootroot00000000000000/* override table width restrictions */ .wy-table-responsive table td, .wy-table-responsive table th { /* !important prevents the common CSS stylesheets from overriding this as on RTD they are loaded after this stylesheet */ white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } /* Sort out RTD's lacking code captions */ .rst-content div.code-block-caption { /* Copied from pre... */ font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace; font-size: 14px; font-weight: bold; background-color: #ddd; padding: 0.5em; } .rst-content div.highlight-console, .rst-content div.highlight-pycon, .rst-content div.highlight-python3 { border: 0 none; } .rst-content div.code-block-caption + div.highlight-python3 { margin-top: 0; } .rst-content div.code-block-caption a.headerlink { visibility: hidden; } .rst-content div.code-block-caption a.headerlink::after { font-family: FontAwesome; font-size: 12px; content: ""; visibility: hidden; } .rst-content div.code-block-caption:hover a.headerlink::after { visibility: visible; } /* Make highlighting color a little less ugly */ .rst-content div.highlight span.hll { background-color: #ddddff; } /* Equal spacing around content images */ .rst-content .document img { margin-bottom: 24px; } python-sense-emu-1.2/docs/api.rst000066400000000000000000000022251411441564000170300ustar00rootroot00000000000000.. _api: ============= Sense HAT API ============= .. module:: sense_emu The main class which is used to interact with the Sense HAT emulator is :class:`SenseHat`. This provides accesss to all sensors, the LED pixel display, and the joystick. It is recommended that you import the library using the following idiom:: from sense_emu import SenseHat This way, when you wish to deploy your code on an actual Sense HAT the only change you need to make is to this line, changing it to:: from sense_hat import SenseHat SenseHat ======== .. autoclass:: SenseHat :members: SenseStick ========== .. autoclass:: SenseStick :members: InputEvent ========== .. autoclass:: InputEvent :members: Constants ========= .. data:: DIRECTION_UP .. data:: DIRECTION_DOWN .. data:: DIRECTION_LEFT .. data:: DIRECTION_RIGHT .. data:: DIRECTION_MIDDLE Constants representating the direction in which the joystick has been pushed. :data:`DIRECTION_MIDDLE` refers to pressing the joystick as a button. .. data:: ACTION_PRESSED .. data:: ACTION_RELEASED .. data:: ACTION_HELD Constants representing the actions that can be applied to the joystick. python-sense-emu-1.2/docs/changelog.rst000066400000000000000000000024361411441564000202120ustar00rootroot00000000000000.. _changelog: ========== Change log ========== Release 1.2 (2021-09-03) ======================== * Updated code to work with later Gtk3 versions * Added configuration option for the editor launched for examples Release 1.1 (2018-07-07) ======================== * Enforce a minimum width of window to ensure orientation sliders are never excessively small (`#9`_) * Various documentation updates (`#12`_ etc.) * Resizing of the display for high-resolution displays (`#14`_) * Orientation sliders had no effect when world simulation was disabled (`#19`_) * When the emulator was spawned by instantiating ``SenseHat()`` in an interpreter, pressing Ctrl+C in the interpreter would affect the emulator (`#22`_) * Make :program:`sense_rec` interval configurable (`#24`_) Many thanks to everyone who reported bugs and provided patches! .. _#9: https://github.com/RPi-Distro/python-sense-emu/issues/9 .. _#12: https://github.com/RPi-Distro/python-sense-emu/issues/12 .. _#14: https://github.com/RPi-Distro/python-sense-emu/issues/14 .. _#19: https://github.com/RPi-Distro/python-sense-emu/issues/19 .. _#22: https://github.com/RPi-Distro/python-sense-emu/issues/22 .. _#24: https://github.com/RPi-Distro/python-sense-emu/issues/24 Release 1.0 (2016-08-31) ======================== * Initial release python-sense-emu-1.2/docs/conf.py000066400000000000000000000120671411441564000170310ustar00rootroot00000000000000#!/usr/bin/python3 # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import sys import os from pathlib import Path from datetime import datetime from setuptools.config import read_configuration on_rtd = os.environ.get('READTHEDOCS', None) == 'True' config = read_configuration(str(Path(__file__).parent / '..' / 'setup.cfg')) info = config['metadata'] # -- General configuration ------------------------------------------------ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] if on_rtd: needs_sphinx = '1.4.0' extensions.append('sphinx.ext.imgmath') imgmath_image_format = 'svg' tags.add('rtd') else: extensions.append('sphinx.ext.mathjax') mathjax_path = '/usr/share/javascript/mathjax/MathJax.js?config=TeX-AMS_HTML' templates_path = ['_templates'] source_suffix = '.rst' #source_encoding = 'utf-8-sig' master_doc = 'index' project = info['name'] copyright = '2016-{now:%Y} {info[author]}'.format(now=datetime.now(), info=info) version = info['version'] #release = None #language = None #today_fmt = '%B %d, %Y' exclude_patterns = ['_build'] highlight_language = 'python3' #default_role = None #add_function_parentheses = True #add_module_names = True #show_authors = False pygments_style = 'sphinx' #modindex_common_prefix = [] #keep_warnings = False # -- Autodoc configuration ------------------------------------------------ autodoc_member_order = 'groupwise' autodoc_mock_imports = [ 'numpy', 'RTIMU', 'PIL', ] # -- Intersphinx configuration -------------------------------------------- intersphinx_mapping = { 'python': ('http://docs.python.org/3.9', None), } # -- Options for HTML output ---------------------------------------------- html_theme = 'sphinx_rtd_theme' html_title = '{info[name]} {info[version]} Documentation'.format(info=info) #html_theme_path = [] #html_short_title = None #html_logo = None #html_favicon = None html_static_path = ['_static'] #html_extra_path = [] #html_last_updated_fmt = '%b %d, %Y' #html_use_smartypants = True #html_additional_pages = {} #html_domain_indices = True #html_use_index = True #html_split_index = False #html_show_sourcelink = True #html_show_sphinx = True #html_show_copyright = True #html_use_opensearch = '' #html_file_suffix = None htmlhelp_basename = '{info[name]}doc'.format(info=info) # Hack to make wide tables work properly in RTD # See https://github.com/snide/sphinx_rtd_theme/issues/117 for details def setup(app): app.add_css_file('style_override.css') # -- Options for LaTeX output --------------------------------------------- latex_engine = 'xelatex' latex_elements = { 'papersize': 'a4paper', 'pointsize': '10pt', 'preamble': r'\def\thempfootnote{\arabic{mpfootnote}}', # workaround sphinx issue #2530 } latex_documents = [ ( 'index', # source start file project + '.tex', # target filename html_title, # title info['author'], # author 'manual', # documentclass True, # documents ref'd from toctree only ), ] #latex_logo = None #latex_use_parts = False latex_show_pagerefs = True latex_show_urls = 'footnote' #latex_appendices = [] #latex_domain_indices = True # -- Options for epub output ---------------------------------------------- epub_basename = project #epub_theme = 'epub' #epub_title = html_title epub_author = info['author'] epub_identifier = 'https://{info[name]}.readthedocs.io/'.format(info=info) #epub_tocdepth = 3 epub_show_urls = 'no' #epub_use_index = True # -- Options for manual page output --------------------------------------- man_pages = [ ('sense_emu_gui', 'sense_emu_gui', 'Sense HAT emulator', [info['author']], 1), ('sense_rec', 'sense_rec', 'Sense HAT data recorder', [info['author']], 1), ('sense_play', 'sense_play', 'Sense HAT emulator playback', [info['author']], 1), ('sense_csv', 'sense_csv', 'Sense HAT CSV conversion tool', [info['author']], 1), ] #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- texinfo_documents = [] #texinfo_appendices = [] #texinfo_domain_indices = True #texinfo_show_urls = 'footnote' #texinfo_no_detailmenu = False # -- Options for linkcheck builder ---------------------------------------- linkcheck_retries = 3 linkcheck_workers = 20 linkcheck_anchors = True python-sense-emu-1.2/docs/examples.rst000066400000000000000000000037401411441564000201000ustar00rootroot00000000000000.. _examples: ======== Examples ======== Introduction ============ The Sense HAT emulator exactly mirrors the official Sense HAT API. The only difference (required because both the emulator and actual library can be installed simultaneously on a Pi) is the name: ``sense_emu`` instead of ``sense_hat``. It is recommended to import the library in the following manner at the top of your code:: from sense_emu import SenseHat Then, when you want to change your code to run on the actual HAT all you need do is change this line to:: from sense_hat import SenseHat To run your scripts under the emulator, first start the emulator application, then start your script. Several example scripts, with varying degrees of difficulty, are available from the :menuselection:`File --> Open example` menu within the emulator. Selecting an example from this menu will open it in Python's IDLE environment. .. note:: The example will be opened directly from the installation. To edit the example for your own purposes, use :menuselection:`File --> Save As` in IDLE to save the file under your home directory (e.g. :file:`/home/pi`). A selection of example scripts is given in the following sections. Temperature =========== Displays the current temperature reading on the Sense HAT's screen: .. literalinclude:: ../sense_emu/examples/basic/temperature.py Humidity ======== Displays the current humidity reading on the Sense HAT's screen: .. literalinclude:: ../sense_emu/examples/basic/humidity.py Joystick ======== Scrolls a blip around the Sense HAT's screen in response to joystick motions: .. literalinclude:: ../sense_emu/examples/intermediate/joystick_loop.py An alternative way to write this example using the joystick's event handler attributes is given below: .. literalinclude:: ../sense_emu/examples/advanced/joystick_events.py Rainbow ======= Scrolls a rainbow of colours across the Sense HAT's pixels: .. literalinclude:: ../sense_emu/examples/intermediate/rainbow.py python-sense-emu-1.2/docs/gui_overview.png000066400000000000000000003074351411441564000207600ustar00rootroot00000000000000PNG  IHDR bKGD pHYs+tIME敞iTXtCommentCreated with GIMPd.e IDATxwxT?gniRHQ.6P^P,{ޫ^׫PA]T@j衄M6vdʆd|<{ٙygyg}Τ E4zZhMCC;ĔKVU_F$}>k00gT%7|H7oVڴi#j6F ׼>DOM,*ObZ@ ?Zl#m#nX0=L}/b/Gm(+i̝H NYpHfe^ZΡ 聯SE۝b[V2>dG/Y؁Y[s$&^d`܋KŜ, d1'7dlؐZmhڴY :-o\\F1y57nHww=*bRJktH{n `Qlb.xٲ Jژb=,M \ܰollˣ骷M >{3^1RT.{FUz }5Jq Wn>S󖊇@2i-h\)H#蝧ɚgQL# #H ~CVIp } ł`06Ԛ>?7 "рh1S zB4F=>^M~౨irq~[^]:cl67F^@.]HLE2n܁p bVnƷ0 a< 6 %\q^F[ԘbLS"Q aRS 5/A$jO1HRH79i\B> JDސL&ErE ؐvA='#KاCQ]bq8h ;\8SE;q S4 %WZ1?G9Z#I TxN@>6u0\J$_<hn_^!B]0CWeE\Ȭ6w]PȲxM-Jgf&Y$*=[=Nաw|4aXqb$=Er6q! Nm\S'0_qpn-vb(>"uZYKrs4/;hhKHf!dY]",34MQl6l6{#;IVXl6D_| ,q=!0KH\Ҿ I+&a7#!c0gXAiݺꦡ;/䔈z>Q=\+(;ӎ<ߡP8_gsӒ5HhaC*))k_~9OD4ONjes)aʰul>iFN r[|-?V*Tz  S8Zܵ{:_6tTLiX(Y"hɚINTv2drK!vYq 4) cOGR+82sf5F.^m][7K²t:bKx}*7?/^wcPJVm(d=d^}i7]zhƐi{qm65&æ뾨pK~6*79|XǍts32"3)=k_JVA.3q[!J[ʠ&.)y,lx:Bp>~^ۥ||{X{}Bҿct:1Oŝ]jEWv?Vn3i߹?yh`Gn6FW;4j.s4/;Mm;^KoOeS1-ܟL(n/==RX շL˦Ma= X^z&i 7.;K@Na>^Ix[&7b=̽oK<%A!%Viى$ђ k0+i\ޒWY"HO4*qwgⳛ~#Vk\.Ҋ:]^yj%_q(J8o -Z|hsByCob!k|z q?4#9},A}^Ӽߟ+mAireͬ;fΈH{O8nw-[dΜPE+^؍_M{cϞ=^'Ҙ4zCDd)''LqѷM6HX# ?A3D^[JTFed5x J 999ϡS~4Nz9xxC;Cw?A1D .Z  y/JeW \Hnsq _s _33 gms44tXyTUvuSf5,(v;4?W5/fQRrw{<Ҍ3D^fc{ayl"}&$Mkͩ\֚l1TlFs$)Q/czWGҵwyä1D*'l\L@շQ> yہxyyy_>lg}Cn@Dje\ iUŃVYg}.: EM1}ENБ/bW+/ra}\~[*݃@hZP7dzZi UTm)'_Fm).g4m0^xV)UD$-GjdʍO>Q6h,_&s!-}AW.A k2~)h, ?1IVC̍0'.W pz@dw匒Q:` M?n-*nV*<++ь.s,p0"2`AT7AW!%b$"Tͽ0M<7AF>e ûˈezAeaXpX,+i!YCcN%JW)5,L6RL0񯴷jm+䵽cj\۩e(EbFOc#e:U}o`q8pp5z8CLby#K2 hZa5,í窯rٲ\~p\"c\ۅ:U^ 3Ռ &1%[UsKǁ|%ē@x=$ 8bi7ժ_v*6Η hr|B-ӳ!&ǝۃ/,cH.g5jܚyg5 ?_LJ|l")`á~?Ol<6q[hک]_PhԓI2mWwOY9 <=uŵ$V35-Bx--J.ݥM  Z?Pn&Ғ5Zץ`wbh3iq~ZZArOJ6Ďb{4a(RbZpVûoY]!i[Zs㵴'tyD誚J?q9ISFvl݊fߧOW'm6}:gXRhۈ}dJ5 ܩSr|rsCmWZ \j8}׫;a\c8ͱd9!}v"y`Ϧ/>j.a5da?vn=k%_ @AFw-7Wڸ <LJg-oyͼInw^j.i/|7;rxݤw0/R)sױ/#_mX r>Zl. a%CX ,P-Ѐ)8Z,Wd\"Aߪ;okWWm~f;q[wG$)''Gήq&uF)ɗ ڪ+t;|Vа'ql&?T8,!۶"Lg~H_xu̻u I|D{[]_75pyֵf3NgܺʵXޣG/1k͛7 &!70/+ dKick?ȫL%aÑB]3Nb80AQ$뫥EFqF_'HV*~ ّZ{Sv*YiŤns)Ft3Ҩ³f)6K)7L iMyfM6&5R0;}Q 95O`x}cFaQS roӇ[g3c| b&3@C 3 {} կ~gfn *}߮xdxmk5w?-s<|)2S)ɧo]1rc?ro嫥wS?e!w2eF_+)ygK_Ru։ϖ&6M[IebM,P9ffAE#WcM,X@ ʉXĚX.q?5uh8A tAO$a/Ѣ6wAIefV@C+k|6!#SvꨄR:FӃ qrru"->Da/ܠiħ~JBt۶*55BV8}4IIIlF ~kr?T$$$ "N<ނ{3QQòӗMFs>uHjj*ׯl6c6IMM%;;;m20͘L&RSSaN rV>zh[m6[{ӦM F ro۶ҼHeӦ7/ 63ŏ_pFK]0#9ll:Ys =ze/ :,^&J,u񚸘{tܢDRR=QpQ i4@Wkb3[87"5ZZJ$i{5W ( /Cd`ILPSS)pkq_/t1XՎߪ#C/M#kPZn |dq@$&|@q/.E(|+*Lu[1ᮻKD=.fݺT)͍1g)n:?*3D\$OQx6u:WzF騨1W4tWn-?~WL0#S{X],IvOG~Z[ƫBa~;c -h5Z{R ef*¨ېPDX}w\p{gNWNh@t; jSo?~[/BhЄ~gOֲaA&VY IDAT eӟ8W&Ga4q"~9K+v`U>%s?t+>S^=)wp<~1 9`ä E$Ghd |ӊI)B'Ins<yfG ա CiѶWomnV̳63$=O0N[j tYvR;iS^ o9#v:b & E vl" [Ěݝ>AQMx`ΦA1&~Vfl圠aC.IF#^OHW tWC|rh@c0Za"fYףINNAדYN]eo',#=Oz38|QnIJ\:a$qDrV >0Jd '8IdZހGRB}'VMz /}G{voSOT^)7C[wδ~g{,"dǽ3.H˕ di)6zOFehXB ʹ )߭js^"NNI)7[@Q}"RR: 558\H1F$^2Ptw[1 bh2[|,{~- 57R|eYS=Ge-O?$[.03C~-$IBQ*?ufb gZB5926T;óNNW IW+3oJH#AG]&g7B=.)Nga_"o#G$4g}ʭwLjp{y7;-FO4iӦ.]0~@ܕA멑P# *Yl:*EKp7 gQvM)gYi bwc0q2?DK,' ԁ/c JwI>Y+/]=M%K~*P1۴I$ѳgOOެi"9J_qE`}0g\oum;` J4ZgGwIL0O(DotC ;{UlI1.+GhA틌m^d{ؘq t=p]Ұ:& ~߫[ȿؗ#p'^55z=u W>#6 FgVm+9 e%q"t7c X~ܺSvZvdiNqal?~P$|fW| @$ʷ,T4Of[Qݭ$7EUع_nC䞢 Nat88(px]yf*n)ʠAb8кut=}bYIKdeV:U=6jRmڴR\z8-m=zVM6X;>,*Gvi()JoVޓ6lYg3oo;gemsWXUfE%6TN޿\ vJ!-ӯ/?`⹸ػv!O8ٶҲ۶Wrew۶y,a7DySWd2ia9o+w=3#GET3r ADąZ LTHXyO΁S6C'.;`eWXÏ>ƶk=[eɟػqj9~q|mՍ/_~̓۰p[Y9>︛>|hu|1(.4/bzePwۻXtdHǨO^׍+ǴRsyXp6'*~ :;- }kb)''3 2Qn.  D) `HTURWvr; b:B*e0}ߡfQsI'-8k=>LU\6m#P57ݼ7c:; ܔ #7qYJXD,.;Sl.4@;Tզ> v aടeg?W7^E tT,˃)|v¥ 9,Ή0!k`#f`1 !WKig;x)1 jc:r\yPFbظxSБvt^}J@$|*)o颅hh D-_ď (, @Ny^}h$`ZX , ` ؑza~?"{"t|/_ kcjwՉ^7;+BtfORՎ&uy#ws:c4Mb؎ֆluyiKuG6RqYi0"4D+75bM+X@@ `A +DcÆgholڴhχڵ }\oپEbwz &tt,zJGqğ6;{^DAn@jꚈݡCG4 7]&!#X~CRY״~W =z`(LI/b" fQwi?koc7cwdclUyы5֡jmQQk׮wᆲXïk`hB嗳%B~~]cE هslxsIw] pNAcfԦ_Sb/{Gj5k 6F%KPM6QQgB_HD3žh4ll6s wjjj'NM&S;777ȝViއ wYwaaa{ƍgϞ V (A:_1mb5'^Oy\:@ ) BuZQIWR˦M PZwБ=Z%Su+lPv(%bY5j+ R;eyaGNU,ˬX"^bEKرcA?ҼwUܫV4-[nwؼ B Iy>9]>p),^m[j@g ]bP ] =i+w<ճ+Xhj#$J$X)5JKb7~@XhYXHexVċ:C,M$KL,MS)uըNFxu,MdhIHOsl^̢B*S?g} MCo Բ屮NQf ~񪋃^ݢ+3h vg-!@10{6^\ʬ Ҕnτt%BHVd!e F+OwDԼ|(*y4?9_\NRo?Vb{Сݏôiэo=/;FW^9]'qmd3e҅S[F4flCVh&H z%txYbZCfsmAnVMoFK!1a+d eidS.겪c N9( 2>lFTntY*Aq8zǬY3ٺ넥a0D?>g!rL$E3𹂇GZC|<*q( Ŕ/ 4=T2(o= q>5I2}d\<44: Ic>7~uXuyA)Fc.שKrsrW*;㱰~0}(QL5~~[+uӽq[fZ͸.xij) ryjcT[`[bytyi4xپ`.^} 8gaIR! `eXb4+,@CBn.҇rsaɷ2+xۈm~y? ׿Sn܇Fw"ݲͳjU]'\Fd_L)CEO6x[oٽ숬\;pZ k[5NXOHNP>kwg~ɿ/Ch4rsmLZֈoâQZZvV@ sQ! ORRR5M%N~iiEM/ss69\@[Uw$A7ܸDiԵf4=4I&.!\Ed.KHf|uOz]rec3[<<.$+'홱/c{WƤyQ΀ALz._=ݚ=Z'&SOr*_؅/_('[~lQ7ˆSWbd-*Pݿ?L)|g][>'8.Z7-GP@puoߛ̥C1V#$1*Gj~H ^:%`5/ Y4vAgotNM@afKOj}aRIT ೅b}QOlWPRt9ަUN_R>!W繶)2ZI)SqM?l9EMPg)_ =0>yr&c4mwxMrPَt ԣj@. \479([r%R{mE[^Uc +_+&C+ٍ$O\Gyk@.ŇMh|t@y0[:b}6y%P=8a(bH^e-!~{ZRerBknJ-a˶ѢЫ |5EUe{DUXBWۏ! 8h$*B k)6{ d7DFdY&-m]lي6mEWG $2 T@ۊEo ;;I&MsXC [1c:7|{+_Z:y~InoImt1$0>//8@DTX-5f6>C o+@7v|lN;ODQlEv뗼6^ZXg3)MZ9 L-9]X}[8e?+ 2KX:L$ޯ~{sx =:n7wygϬD\]Wuiܼ04w7LaD#s0yK ߜg[[4wuk\{QŨ7^K&_~#W/u5I(yd?!YH=[%c6ٟ^AFNl5$&&^)틚9.=7s?S,0 Gxj;c>Zn]B[ьu圩wd?GF>gຄBeoޱ}94e )1^}L& h_EEx<^9aMx|>9)Zw"94' F|<^^mIرwfx3|J S#Oo»!2,s7FEYVZl̈́Э[z[NY?ZUy%Z_hX:`_v Q ػmLc-/xԩDS,:IKވYG'UmLTΠѲ͋6[^ytMHKS ˯Iy\/57"#  xUF0hbi⑀,wp} ^[@%lzGU;`Hg^)/״vgoۍok^P^XY'7\x=ԣoRMȔ҄'8J(nC +J ɫ+7BeʛL}siwaxX>Mg^/o!wİa#E/ĺ҆&s)h6A2859ET@tU"`F8RG ~6q\a v(EmN9d*[zdYfKbI9NR<9t%n)T͒?Us^`PBr_ AɦlVrz}f/<GQEebqDFUf%RbUb_GtgI[4C捍 ت3f6CaB;mG@=O)&gAb 5%.֪y~4V5o Dcb!\IDA)4L|S ^gt lڔFϞ_Ѡ(YOW:oh97]l$h IDAT;fܢi<3TSxo+2Z5_fՊ|M)j4$:~CBuOINW3E ]o&ҚΤUL$z%b}zahY8&( K)RĿw&( ǚ{tQ{3(<;N8'q4n($;N:=Kl?YSV8 ^VW?ժ-`/v"xɎӰ867(67('Sdm2l:y)OQ<ָCbr5VuJ?iz4~2;įS^'Ee&tZ銟>5_WŮd'`b-dh+(pl9?+u0w*WE#++aÆ!IK.wavPs2Öl&%!KBB7*Eس5S,9:`2N;bw]}s̖óo?`j J UgVJJ Zua+cpa&EKFFz8&fp# EvX0ݯ '^-Aa; >dVǚٸ$( 7,e::=I"hf N]Yg3@Dy$K>RrVp+-Z}vvڅFe/#,Mal 3#l8%%EAΓ\ӵ/9,޵mRx|ſq~ KMbytuu c.wVE;쮢*+z+sM7sNVX2d h(Jݝa/å|XEյжm:IY.u[KvGL-rxeuVg(a3yߧBm ?XG̜b@p*MF /@AANCԌ[d0Gd䏂*Dir(]RB:iHi;V0޿<_wŸ7ma7c+*$L]3+)c4֞$!>QU*oZw+g ;7hāb*U}ZjSՕ]o_g:o)o8N0,HyƿPW1s/N Xo+vb` }u7dg: )3[[:OgݮuhQx>} 2 ll6_XkݑR%07,\Y>Vq_9y F{\VEڹU[ FQFߋE;7d6VT;/Qd*{]ƸF> X4\-M 2 cڱdM-/XQAHE1,DF#*ckm:%I!X9#-ϸ;UiЄ`hH& q|4QY]4ďKTs*So~gpn{ä3**Ƕ(Pi{q>"A9hSoXo}MN_־|a=;<|̵|v?ėѱqK;ύ!Y3?KG_?;Oj4ؐy}thԜ}-0!@k!5A9n5=;;xn|@* ]طv@'nT4_^.OQџg48Īkhz5xfBzW* ~1PRPn[ TcyAXyrTA6oވ =.VG*괺 aUtg|zwU{ 5e%iMӏP$.1K{OM+Qs3׋ku7:uʫhf@KĊU \*Ҳ5y3>Y?||ϛ \+U|>0wXV js4>cbi?a1M)*#+9i|Z忸KtLgU#o]&/~zU#nS7~ 셞5{kӧr;qjT[:+Sؒsadu/9cy()))E^`AA`T1 P9k;A(1 O^c{-|_Ow;(ڌ٨u}f[V KG #d iFNGQJ3^z0[U&QAQ6KncmM~[|nz6{f>V$$r#b:7MʇP}j(Flӊ9ROKߒ  TAJBٲe'A4"-aAA,B6k߸-.rM9Ar0>\nÍL )}<ϻ߽ȰSe1h v%7ٯ|s7W2MA6-3uŵl=m2 %_;1=Ÿ}T65y ړHyR}y B4³ EӴ _2xVd7_=ǰod7*OLc1tHΛGO͟G PԨ?=>k^$O]A|*DIA*%AA$ soe7 G5c  AW>Tq(}|\:r o} YX"4鴟ԽV&ZnnFOxT_z+Uy ^+<1BYpps}-W aݑLov{ܖqәp;Zw<"Yn7oL Y\yl.Ypu3I'qr{y?pᦏ 6NnH `Y{JyZ&w;o~3._qy(̔.Mc_w|K%1᩷us4Î_^g|ro"e+Ye^1ɸY/_<-A}%L/U)Zy*ˁ_fԔgu\%=Kdv {?_Sa|KUoUeoe  ThAA$ aAA#, aA8wY~-ֿწ_Vt-bA;Νۈ&&&]ט3g(磣EP$&&Ittj22ʤI'o` ^|Zߊ ~aێl_X̱XKE B~Z6lDӦͫqK ǎW>5V??um(7~-8ڙaGݷxxPn^hG;>U)}|7RL޽$$re!)) /hwm% ~0L9}zcZ|,KPwɊ`P\;Clvnߝ <&I mŋ/&r譛Ryx<.T/kзo*#1 wF>و/ٽNv{i2ˏAZJLzz:vmWrgK/&J %,ASV-zGFF&bԪU[ p (qwܹy24hR򄇅GpEиNcQ %111!C.x=\e#`$4FX(C;*YlOH8ŒeS\.8*P%Չi8<I[5\j<(Df+jڲ/¤? uΐA3O~$+3]X[ ؞U RKq#3x#,#md]׉ #G8|?UV9alÆ y}y;vЬY3Zh򽺮y=))&Mкu<4oj|Qٽ{7 4]vyo޼9qqNOOgұc<7m(ݻw>#]Llܹs۶m >}(JngӦMDFFҭ[|;\/ h$hZf+t{/^[AA(_͎^{ Ki (`Xd񇳜qڵkƅr!11q(X쒰=,ǣk26\,+-TI\:ǿ|CpAؠxᏃi' ;2F Uqx:w<$%+F3i%g]G iM;C] ?oHҬ4 kܚ$5-ofv=ʛf.<|rѪyV滞{śquO+c8<Hz(xu`QD(R %>ŶT]g=X[' F61,o ʍB[ Ns;OVGᐙЂPb>DV)z0wlF-)EUpsjl0*.+Sf%T-EՅb ǣH?[?+՛_R]?=>a4iݺ ^ Án',˜1V-R">!Wafo`jq|l=mB{Hʹ%m9_?;(7o+<=9.N*:׽^?[pZ#FUy%%%ѽ{\;tNlei̕ 1,K4|)G(ֿ;l~L?ؖJvv6[14*z"3#lrU[=u.^]\(l<|%xi|R.MaFfQP&Durf+zKQhknQSd+òya񾣱F37Om J2;5p^-ƿ 7Ǟq̣/W<2է7ᓂӞ\m}ǎɭ  _q݀ǫms1Wn[&-C}"BǬe#$dN:GsĞyϒ1b5A1grE}Y7,ʪ!VlU{ݯ- aSлhTal6[qNLE ByaŷCAQ0_rJD(iz̚5coE BEa]!B U8[m &o,-ב"S38?/>kW5BzXMIxad{LڲM;У~Kn^8Ydpz7))m۶a2\yZn+XoUθmyi)).%*Ry)ܤ'~I]{0&naNð(+~9'`ʅ >D&/տAh╆X*{&Z.֎2Dmz$L䓝;߹q/Mgq{~g瓝{)9cħ9eorWOd;h5 IDATzJNhn\¥)w6[&[ªW>}1?ӦMk$ӎʛ]ԫW2G =c\ Jl ʰ:PSpoUjƄc{b0}MYcJrG,)\$#b`Dísò:cIъ79€AWhbWsGT0&ކh%3Y'(o &2„C.;,(c t]Z43Ђ5򙦹ˬ|}%Q>/O bAzh1њ&04QHM0zB]WbVԐd}@ ȕ[j>Q`8N/S/(P[z>پ\zȝws#NUgk3x}-=; R` i. K/[ވ^Czdn'GG_܃w\O/?Z@|<>? ? xO՛`Z2rzhЫWon*XEEU8:Yp#K:Fd ~= h6b`  [c /35dB0(TN^DrsO8r혒Z,uhaHt+cKfDV,=+¶e!Ms)1Zp[Lp_"  :YfMI>1^дvo - HMDDP߸1 {*LrVWӢE"oleX#u|fbk:n'}AG(&LXuw*ZdG`ۀhEU j9?Pm Pe/ھ$ =w'Y~Vŭ k8z4Vs2g,FWSawr;K,l/NEWnUO>LDD,Y.]wzd0@UI\[>Hib4Z0U#.^6[ׁ={b* ~r[]:}jcͯ ˿$'D1d jdD&K(a~(xaFbYTl>)daAx~9]8"K|%z- ; /TA' I iitVcޢDÒk=Fw󀮕l!1ksoĥciU+~f۱Xf8~c'**wx_~Aj 7`?jFvFBNS\!P-O Txh]# EI]NbZD%݊TA%DX(wyՈrϳMAp̉LVїSa )Q7`v39ds` f^k Lvf5t7.،8YbʗNF@v|{uc_2 mo;{*@)=z'7GFbċ5p>S/اkn<Wl<ɖIs>y'-훰ApޙEڸ(:RBv&q mLŷ  ]8񵜘Nig7G9a!ˮ l8[6]{ ם9Fy vTӡA=*g(?ڒN)o OX6Ԏ$!$bq"3]"sˬN$'0R?q8JQP;<j%3px\A#\"BvJhڽn2P7w*!lk1h.@?fPBڎiB«yPG6Юu_Se:߷dX mbRxA(!7o[hZ,PU3.9' Sr%gy'C(%5/OR[Sl^~,>-ӾASuHwvḱ<6x,/F:$O1i޼zO͛确-C'ܸ%% dqy~4!igվNȼ\P -kx]8%8(ӯt] f+!J ǃ-7a0ܝjLdVћ$+N,;7.I+ļ/լ#jb7='}A#1^q;O260竛%V]n>G~.xuۘ{_;y[||CF.;{3\%eP^42Ϲ'[ eٲ:r|>_ȑٳg-[ed(}*_P#yCX; τ Lv? -߼GL ;Xkcxgcb#i>.N~G"F%8:HNN*4"&*AkU#,pk:DB+x boٞ5:!vK]۷as<2b̘>kF.?q]9t nB ^&O~wSC a5RkPŠb>`yTqMӰ۳hݺ-[vӶm;ڶm(L2bq\ڵKWautԅf͚ʼ%dI<<h~`#|<8J1y-Ơ;G]s>rDqJ<|%,|Éfn!13&F;K`MQ }RO$rԍOT@Kٚ[v:~ג"TjkŏO'h:Xٙxd6nn^89xvrϲ/iUzj8~3HOn_?Wo~w㔧V*Cر^GktFWboڻw3Y}TK~n0t<̥.fCt"丙xt4 Ad/SBs0%}VF++w+ ˑŀg.fPe\.4MC?N Exq8x\ntˍ(I!E$<</?3|(*EQg2J_ i8i! OfG5;3O}j?%眳)=_N8Oo?Kqek۶L>8++pd'$۾߮O^2 Zdܸ0ez\"_1ЩӅ a(pH7f*>Ap3B A BQdeٱXgߑ# 4jTs3oO9Ļ0m ^h6wH5WM?Ahz3{6eED  tA8wH']N)^sOfvMfnDžTq~ ?T^[":a%:d~5Zx>[TcH;}؀4Lҁ#7ŦPOJOIR\ ޥr-aȞt+Ÿ|^ݽHF®ԣql/79}t+qR B%׬\7m\Z%zc gh&n H+o53ޫ+0q>اf$ۇ"x8e!ZRe#cRD& V+'ج,k> B 1Qu{].؃~cT2iR e[l[7=ӫ-AfGUuvtytG^\S'Oپ)ۗI+ظq=z\$Ԗ߱Zp`piڑTU7#ʶIzz z sulɎ%UlecJZaA8/P4P[5-jg%g͙ AɿUˆTS# ψ>= :EOiEC%]ngYO8/M%<РQ3=dz٩Y?+ PIOwW[*d.j+D֦C i\6Re';r݇/{EO_UV=Nvx+l/)ƀo9vv⭱aԴPjT%:t[yh;q}oD| #|<ћNh/AtJf!=xD@9A(:IE \Ѓ&dxq '{P{6p>”s7ʁ@܊/([WušEc~8(&JOֽM)T@($Srf8ȮlL  E]6r}#7 Ф6}k9*]FtSXt/sKZ^ȋ;2OOˈgԄ) #x>cKݭ\}|ըK-#\ˈ4YLJ|wY%rUEu?NW3n2Z8r]@&ouwlә.͑d4nV+hSnyd&ѻܲW.Ԥ3_&g61ywk=,22B[=!lװ٬<Ъә|pCq TeOaX{7:bZjg~{׍Ɍ["2)gVHzs0kdy]WvFr)jږ?BeԐ'CO_0,x=z {d9<<…EcV}mCٛ̉|ٶmiii(Bll,:tK.~}=ۉ=I|1Vg)Q>hz{f\2k[)4B#T7GVhz }97dQ=eM2vlʟ9UƐU:P }=ƋڰKq߅[޼ ڇI<3Œ&? Oy/raF3n}?{讂RDa݉3ܺ3eߟ79]ɓ,_˗ӹsg. Ce=aC5dUo֓?,-6Jz5D*}Jkh xQcR2KmxpSYo;m6ˎԋ;TEẮys&# pycɏtl؜>EW ^ײ/\~3Y0uwe 3[kw*̙3q >l )30:.(BIz5wok>g+"e*9*`Tr,[ L|+WϖP7?0G4F 3:;⑟??cuH<_Pky̏ڤ5 w9~.\q\N<.}{(h+&?;Xf 7ųNGQ1^\]#Z_[9f C%,-*gf(23̞I֜ѷMȺGi‹;~yN)wiZ3\#j #5;;GlD]Яe7h[MR Fn577i Sↇw LbUE!Wʄv̦{w%od[;v,ٳD&O\]ve̘1^09tz\treNl:YQKfM U^Hz5);.G||<93R\` O>νFr62],F3$3Y-ؓt;ܲ ?Us9Uzq a[s~M#O~t|.ڿ2†G&Լ}!?1o_Hu0 :P;0?LBBVb׮]Yu֥m۶ 6|-u0Xo >=1O+ u#ٻ3~(_{=A"FX  WUU8UN#{kvkr] s*6s{Xm8nL#Ev1kdV?nY`_?}omفz_v疞C|1張t&^5akyt|N4]Gle]eXE/ vAgt?.B['EUkG7/M/o#(U* JWhN;o--G&a2Y) 9+:/_*`O==Ϲ~x` Y:OZ̳~Gu"$m ;'C?%{UQݨ(0v}^U(~{`Y׼׋iwMٲea]kxt7;Isq( ڛ<[ÆU_r_JS%\|<~q(a*ad]_b'Tqt6ȗ֧[e/wslBsGvfHd5tC el|kC>^ǵ/;-l,;f<]y.躲X~-]v/4L&sԭ*ߗ~A1) m1tqdR/kkd k|Als/I>_ʰ1g#UKy [A`=s ٞyj; FTEQtmNdcw+ˈ&3^͇h*JpXk# n-xY{`'_[H5dͬػ^ݛpJ) K+ l\ٷT27G>CɵKQX^+8ؿ o! ѪA*T4Z7n}T8k(i ^|y\D 0*0M:lTjd0`PTk#{e;i:Zwu1Pw߾MF-d"C (޿77?F(64[\f&!)`] IDAT-)8 vqg/0|ǂciUqz4_d@k:9gԗΝJCQXt'Fi¬dLad񡨹ĹN05R3B.J[7#>;L^7>{)Z0F77|N}ikzXGHq_"su סuLz\rK-ѢkСS߹e5Dili >WW檴tVl_ز 5zf[Q|gq/*AJ([>=uR!B xkyuSbъ0 `hO S޹'MDueoA1‚aAB#, HKX PShԨ1YYv,iȑ5j"FXA$.Yoڴy֯*ELA aAA aAʖHUߐ|(ѵ b7pA o o޼EQu\E4s)C ['xphEa2H\1a[μ_s?]}z2aî's+r:o}L'.V3 o(=[-Q9Ap#y ̛Os0ˡ'ox(aJ kZk!qn77s'i6skSx7è'0Րvn󿧸om.0秙ʄ/y3ȸ{{3<9X?wyݹm2YE>f.x<]W8nWtoi|ì=FǦo? iӘ:`IJ/ǚuJ90ᣯg>z,#P~ ҄jiI2jMcV.L{\4a9e!(tƐ:0u63-קY7\;/ Nޣ9h_%--*D:@+ǰ"AcAɬM觶r\s"PXoۼi%Yz6 6xj]P[:D+wbE+`ĉ'Uw>s3s?I7o{y'22?oxwᬡj5\8֤?|t{ +t]5L[^ avߡZgkB)C|u'ջ'hܱ FU{jm(P'2C,;HAX|LZd[60rfqWu mU콴hpjD.[v=7EKߏ%ժ>M%ӿ1h9Zr\uq n_jyh)kPQO?BEEn>JVy^#;#> @ׂasLF-jq 5O(m޼zkbJJJѬYK0QN Br[<#`Uu‚ PiF8%%(0  '!!!hGbAATb|A l  ?Zt1‚ PIAJ(*ѬWH;e|w.-h¡MΠ̗1O~]].{0]y;ML?.eM6 IA.Sxy~6M _!^]I-"ˮ{qA*uZ{Qg$  t˟̠a3h ~O3rqg9\\:oU>W_DSA*RKb:oi. T:RA%, A#, bAA  bAA  b'Ner撀3VSLL}ˠas#S(KS?sZ軪2ç6|o5$VybwoaμyL}u"_\*:VYp~² ר^'So9<;߀ڭZV|=!yQ]}ծdɽȽ1M3I !BHtBB!4Sm06pl˒޶]V,dɖ>>;̝;w7ν~|:.?4Do+5ƞp=_Ϸ~*ɁbR?2Uk)?,= M^vo+W [cnov|̟V&7|i|}k\̿}U],k?ix-4-Hx;xwS>:.[zu73qEcwphX[NWRڎL}!O۳ Fs%ؘ7>l{qrʷ<+߲?,-+=>}fC)Syl]H=ooğ^|IwX/>ICߦZhq1+ՒZDOeT: jr4R^0_yG2+?Ozytot4_t{_ x㸴jNf}ENe<{8~Na?{s-B^}{f"w[x{=i~|c$o6˿r޸e|mۧ>вp_)Y{$#ApA8Q\IF x‚  4"‚  ", "‚     "‚  ",  =VߦӟSZDѱc'n7MGI^   g|.JYȖ-'#", $7ntQUUe :u$&Y xpnim5>0YaYҵKg ƾ\wճ'`'ӻ7UU>_cz@~SPPx!qaKjL9 7܌ix7nEEEGͨ#عsW鱣bӖxNO7u1Mӓ&N s*: `Xv $)1;g\{!?y1R`xB) srElgƅ#OPpСL0MժLtJϞ6_~n{)r ];O.C<'Ɠ?jqK6^wyۦibYvgqqfɒL<եcд㮀UTk*Y]b|ZP׼qW]3+R]̬IMNK^^x‚ M<3١'O_5>zsZ\Me_xc9 k֮cq\l> i=[y|M eջ́y洪t|>ի8~><.WH5'5^gx:4nfI BS;o0}vnjr<лt{P$|]T o*kU]y$$$Իa:tU0\Z Q)Gl<֬iA8 }7U#KˮzÌ瓅1Ҩ즧QRZʚ68sN4M!ZUR͠xȎʦWǁU] P(B|[Hy̺*YJf2褾MNJju @qqe,^ mu4'=/1_|;ɰîzNEs?euS7M0KN0K-x=F^5#99Y2AbWok׮f!"@DXAhӧd,***Xz $55U2FDXA8UBf* $:P!=2g{4*o65$Fe.c); $fμH2%ERpg8:Hz+y(*/|;1*}s 9T2G)eǟ?x^^bZ ^y1qt]oW>ݧ8lGAZo۴}Iķh#&LZUYkpy~onr/ytN,)v#ijV8Z&U4Dc=lU76Eee%*raFm/@ *.׆in `__k=^z=8Be\7ذW_)\|IOL' O^6908C)ί,՜@Ca#ux֭ رc~iӦzfeǨLHH`̘15o޼`z8g;N&N9ٶm(--ଳ")))j~^;w#G$ۚ1ujH.w&??CmTxfsϭ>''qaH.]NhtYUsssٿ?}GQ;*'[&Jܰ= {1k_#OAN (Y/>_N4\PO3Ԉ-s+K.wC<$({ g( yic8eE D3X6mY*()Vq'_u+i%4HV 岥<ބ@| h26f|NYbG:$v3vr&O$dt€#<)g\[o]֏u`37!)BSa[0 ]{ Vx/Қ}%p:HZ ?~&X^Q?{v|nVڂ-7 )©a.2;E 0wo5yZXވB+W wש_~>YYY϶fժU@-ׯ__ol{^޴iS zru޺ukmYK:zΝ=u5&ilٲ;;;.5LzZ7]Bny:]1fzA8%Z_{DUpnZfYg:wkR)-~.Oe{sZ*)V ބ=9Xg.q%ws-yV&4X*X *1XߥcO^Jˊx'0C]"p VcXעiѧi-F]i(jW)x5~@ xzj>S}؝ REi j-15Fk$-IN_X&…6;G ;LF&x}ТD/E\23*Knht8$A8De+lGUu X[7$gӆ4%cF96zZwQ"%%DlD^>ɘa)0l:vRThB8>xuΒؤzGƐ؝o`9ߍq t2zXκuYR K-sv6&Y| FǧᔎBf2Qh2_!*`҃/gϛ.ǟi(j<Mޖ;g>d͂9D*HV^)ޒ\n(UB/>"MI'w GȼV`0{% Y⿫Tn2~rN=4,ΟiXF# 8 q&Z~=II_϶q҇]$7~+.%>Qi#`ӿo`\Nx *޻-Ue+/SC[U >ӈ40kUdRݵNx%T`?/ -f鸳^&f%M\gxm4:tC kC/Tٱ~PU80*kA8pՎ.m"qΉah:j MǐB cz-;qt1f]v=sKΨm||Eu~X֮%w IDATf/xG<vQ@ϩױhLn\c*N'k-;:> jmz6|#FbO=zlji]7X2'>u$w52<)W"lP;ƯJk+[Z;yy쳏7nCg߾1 .k)/v2 kW3p%Ω,1_o`)NoOLx~ΪCS#K3Z9g[iZg91PwliZ9#mCi6v6f"vogyk|c1D%6netM# DM$+k;51` ʻ/LjGL>?ѣDzcv DjjdLs𿲗5:4_a3zzQ]猪oELۚ_N;Q,RM;=QaörTEgi+zTDe_]ȳyۧ6eq@͑쒯pƑSI}%4ռp` B{"66VG$%EXȑG-VZ2JĎ`w1'zC"sJ oW߀fU~aРQ ",DikyWpѨ3{Tx4fMWrL,x5 'n_AUƲ6GL'͕ȻV9b.2}w'O%--FEE99{q0ad VV. ,xqW`D>+|]k{NC5Zii-@E(qAlfj`(@ě(,oϙ |@ zU5(t)Mht W(i6;ӱ Sm I׀B6 E(t%a7z E#+ls7|e}܁ lftB}f{T6T~kV(Rz#~EL?10+w) &&aFp3td MŨy#b]ll, p]>|NP QZsnUN_-9j+[.;U)0`>,!.U>PP:,ziV&kX mnVȭnZ,~:z7VLԂ&+8p*[A%GV;xe_:UwX J3lQ0{ejhXۊۮBەS\Ǹn]X FY i3ht5}7mKƥ9.S!w g'||h՘FWZ^ :^U0ަJPf* +Te'e{*ei5\^DCs Mfwx n00~?oF o cc_AD.XFF߹ai+⢪P--ble\ۖ,9НOb [>)`C>ĕP&3fp ^[F <~2û9F_ӇVL0Ro+][^:rADx|ͧd&!`De;1Q`%մGe;b ]4mSQٶ|NhDqu54^q7to"C\I5z_eRkADXh(w.zߡ]MfɫۖcFﻧ4W-'Bƺl,HKɶƫᜮSoY4OattrFp10 rQA83=a_` eY8=gf=?Y$gې'|˧;IzyQvKffx\_. gL2;3zInQOS;j'ܡCnfy!p|&l* wG@rQA8sEXO-90UC`p,er5j %cڧK tc~@=҃v7,-Ysfͺ@ 8{i.h`0"§7}$`< =ؚk(t'#`Cl~k>tւ1gpRF].:hAAi2@ܣ 6V0dJ| :F1pl`u|ӬTZl6QR~8 /,`<罪ǖ%L|3n/&tP^X(t4< jO;dO855[?IPcϑdiiʂd>v'|)jֱ, Ef M(v  K,>,S.Ŗ}Fg֗+Ez3$6A PRCSf!57ʮP*<0B gx(]E*@l4\u6QQ+4(8P1&48BuM*"w\0J'|pŤ}Muf#|t~5$:o N7E{yDٵ `ASΎǀ;kտ/a^ _|lvN^mY%^Oa%7T0Tu O(eaN|U0 ƎpDҋq.iA^~]5Haԕlx).nOu?60 W+;zE ӥKVj_!q=\g][uznYTKE9S[ftƍҮkZԱuqięUh1,lƝeQzj<V)^AAb|rq[W2{o6+ : h]>LݦS.і"*"RtX#}Q"PzPDAک ۹g>d }0){]LRXUСL0mޑ[*^1kAطQХY[WJOhC&UNnʖYߋ_w܌ LĜ~;<|:5Ǻ% ݓN'cժL&L8 qeek׮bƌyv]wUv?cU̼9fRV@JAv@jj* eժ1 J֬Yf߂[Q`;6J[wHDJJ  %+ke̘qd;n( NTRCq7Y% ׃*UD^ӄb52أ*Lcl3OP|V`@V#`9^>I(:#8妓V;Xh %)=%a;*⬐yV.(8C˔?+;EY&MtӷFP P >8#V*е`7<( H˨n1-8(>yv/ D,ހa#`P@IσЃy0M<_/w KZ"9QUXAnWhi*,2;Ca- H}RHt Tً^VRU;/"UN_e'};lJ/v&ʨC9a\#f|٨A^ c=qhgcn]l1?y=|葜s+PJsᑋngJ:wwFuK^Q솁>DAh"*qHU˥vUQQlUN61V g!rUeu+I>#lg;niOď㶊5,9>Rq!q\wTVl~QwLkضsb*2>\/`o,| 1 aB!>B~tU n[0o?*Ҕ% !Tj&&4 rm']흸l9" leJ^=S:ɗb:'z_ulo=7 3kHzAگ'O4"ld4te9i*8Gdm\dǔYnoG{- ^9vg}ݛp`7w   ',M:)Xr\L $prNZ.2CU}aWӴI.AZ!MXA:j6d  kl7KR ?He۫L|D׍H5'S@2?jz+9|K6c5Dq0.Ah "QI/g2 [U)C$gt~Pz_vRpjKՋ(߳Ax&4nA<*s:7&J񐚘NBX2QK+#Y=VPv$pgjUAxf^-ەl0а\iQic9Sv٧K0:JrZYV ܲ,l#3n}02+?,nU8ZhoWPCx]|Rx&q"?fۯ,Zm1rVq8}޷JDD>Da"G6csX "׭eP5D`DEUQIHC `oǡ8q {V~G>6bZQ"ְcZk ~PrT{G1Qt\~f=viri? f`T(Hp8ËROn[ħL t {O*"fS!‚ " 0ˀ(bGk.2$)*= 2_p0Sf&GeTTh* ,5'jmb}iyjT׹LlS^ }(;Rgm{֕Xcܽ TMvF%Xr 3"‚ H?aMPu=9ǦC9!  ©">oVw5!  ©a`<|>;v ADXN p}>n7]2D3 [)ZOjZZT!ưI%It ",h72wCs)K'G{ cpZĶngԨ1z="E #tA4yd)E]-) (v)5hʊ*JЬ|p?a+O۲聆_z{miߪ jɫk%!q܍C;w 0 bfAh,={fн{:k׮fq"§]zqwR v4 rwb.v*$E;øQŎ.iBbGnbJCl?GzL9:UJu\:VwyfذtփO?]Ȑ!ѣdN٢Q#"L|lլbTm86*U|ִWUzm75fw*;)U)ۏOo:m7{Ͻg8;c0}ξG>~^| U*+Ջ֕kFMflu?~@+_?{G9!gW|ĬAc٭/}6s ϭX]_gMkR&W<_fý^B>4Mоvݘ#66.M+1fxɤ(PJI&)"l0EϨr3JB\bS0F5`'cUqYFT c]ICdl Ea\U+@Zk"nrv3syhK ҋ]Gza4ְK-RK"Ov°.D枭\6l"7']ͦ{IMΉrhߟ<0k]EaU93fD\?z>3-ٙ-1S~Uzt$(ӻT~3SEiB&A 8( W('{E O#4piZRA6RF:^AܱWԈHQ! qvlvly[k}X++ grO(v ~,dE|m9B{IcGvic _E+!iJo5wӝǺԇtڪ'|JG\l^.|Gn`08tޠCY. /; ~ IDATpP i|:j6Gexzv OGt@-7;*mE,6mz>e[#hx:ry-'8lTt:bs8q9T@|ڱ0=erǜG3)f(~' [g/-[6RRYi%umpۓRbG׶rw_ˈ=#|v37&^/n@?v XsI-b5ۮ>1XMF8:lЍXW8ܮ8NcVȅ4hۂI\!  XXa+d' #аOc &W4,,>0~S$KM/aK5z0l庄*&ZbSG#t E.pvj{**=>[mBCCyzA`MM3I:Y{hAd\IǝCҖ@ wbTa4n>clrJJx.(<će8b N"] Q.!vi1(Vn:hY!1(L`*z؂aUJW-x'(vT0Ζaud`pv[8J}8d;*αu [m4 <0`KC'$ #hχz!9r3dfAΕlk1v$tƇCU̘19c V;U`:X#6:zUXk9V2Y Ď^kާ+sXX7[흪:}Z}#7Ŷ gyy8x@ض5Ƙgڕ0\jp;CӘ\^ %0^>-gI%$҆mrYEZ]'W1\Pph42*l iyb FϞt޳Elڵ=z\9^a#]ooA#隆3J۶FV&1$5 Cڵ(Ǚ-7PhfV}CA.|\xYk\W׼ `fxJATjtMp 0WQ= VEnNN0s p{#G&D)JOsmS8|l^r HYr,8frw g*aAADXD4p{(AaaO&wg+ڮԅ_n[^0>+6Mg;r(AS=oSg6{86'J:[?Wڍ9LPhoT1#`˝1DE$ vSN_"XCa+c)V y\2Q \8KK-E\mMjpc/ HJL s.Uтq\qCv)~~ "W@RGtr;>gDJBZWޱ)N MΝ)#H4;I%,Ug9qcckS"'ŲH^|N+ph+.:D knJ P=G_ EVx<bS;S]/oQq>vjwG`>2 m]E:يiR3Ӆ˕ 4^PJiAnMNʺ҃wp Ww]7q8lTa L#ytRy B"_էjys>7bx(уjړ)"gn%g''w2QIPT-DG ]cK+xB|lĠH)'?4D)C cn$(`SصM*GUPJ_@Saۦfaި*8- UV3 xfUCש ,U@l^ń_H\KjC# ڮܕnDގ9wi:ʝ|w.^t8T>ےН$ڰ7f,f]؛x= }3;pi&p{Y^G૵Sbt`r>{7ݘUYǗh0Q5viw?t< in.jWqTbޘ"UuH2~^'vhVZa㯗Ȯ}.1ō_bol9rU!|tx%x>1A_37?ZBP' ?Pe1|9N'̚5FTō`Ah这6]  ;?~?>3W,_>Cލn wD\C({ ~\`&o_}U͛7uVJKKBkwӅkiLsFq}zދe`ukK#ўq=c\&yYc=krwusLŌџw3gݗ||ô,c\ =s׏]73[慩WE᣸El޼y/$>>K.$\40 NLЫy4y mڕ, ^_ˆMSս_@Nn]>ølDiy|kdx]:0koޏk_C;r ݡs"uyYw <1'KSCWZ;u f֭1cƐcǎXݻ1M#bpcBtڨZ^ɱRdI 3DswT˂3^Uz ʛ+jqѼq9SұO/LoHLAeo] N=ۣ?GKX?OG[V7+mYYnz%CʚbK?rr.n ӼM6Ǘǔ'J揞h׭[n!0SRR8p`׭[~;wmQ`4Jsލ2݅ڶzO-*Jp+4luƣ`KkڡC6/ߎqT<,زN8CƑ_^ycy. 7:BPbŻ.n WlXuי;k_~/>۾_yp`7ߣsǯxhK)/n,>&xp_-H r1.S־{ ٨]GZSJ^pdso~k@_%_nߝBzh;yP'(s)*(?dRɴۑU:UYmڴ!66]>|I#[˖-Wo.8.⏀PAK≍mP#ĞS;+m`:+/ML]4O^EM8I;z7nC |Nj214]o.̟3ZFDz%N\\ և/aQX&l%mφ-9y7/cp,ܺk+kϏ6mڔV7n 4o޼\ғԑө*^{-#F]vc4 K.\y\yםDcR n⡍G:(P+V~G:Rf&q7⋣ܲ- Pڑ `-*2 r*gԇ0'؟ʘϦiVP(l>6lB^^DGG{GW]~>zmAQ0**c.K{c[4~W4 qshX` *DY$''ȺM&:u/^*^zC+ dVݽV]GUj]Ŵ0'3-8lXLflNf@N<{&HV/)ge{3kOD^Rr Q\qT`2]}Gο j4mZ_B9|8ۣQS#¸Ќ_7`zTXRմQ;v+?ڬB+.q5nUUYv]t?gӷKD^Aa+;Wǝr[24{Lw[H¼Kddn pAר5M"twߤm\s 9MnNĆFYsϫu+ܧlJ_p9*m} ug6E!:*OsZl,L8GڒR{LKMK!8Fs8s:}=+2o t~rral d9y-u g/ac/"3p)Y+\bCDPఓ^[:smH\u ^v/[;^`EwsQ́<3rfv|eŽ㋲֧;Fv1u Nt]֧<۳ .ӫw.2&u,oT\ʹz,r\{.8DLfE/{<ya٧\Y߭j]nMv3a<4*_1%H`D^9?{\Q 4}]|O^9|P_;^;2yқ \^QBH.v)3b&{W+-~LA2~;c-wұpDx0OKxvPpW1dD1 u^~nEv~ӱaG)}(Ou%LQvْ\@p#eU6*I=p+_n0B6vi+$cIUA?QZ J0{oōtn?v]Kx̥#2K.ϒ<}4aTEl01GkYC@8Y(J`+#T:d.WL9,KfRxon5Ƕ^e .L4䕋n{_tBp #y+QdaZJIĐ2BZid&ۢ̃ƒe,- qnzz+P. p~NV!1WlTk+?Yϫgg]輝ER;Y]RZپ?hڇ'{(2[hQs/ )HD}tB"S_ O5o釵̒T(*+*yh:? p3 P?0^'Ǐs8?/sInzKg=I5,F.͍XM~ (vy"؋̥/t ۅQ5*J1eu|o 6~I`}f%71׸P_i}: Huy^"pߏ{}V5o %ѸR2!)sʠx;qXBɫYf+(> 7 F3 0rwg}BT;ˉd}F11BoҖ<[@|7̍=.$V5/ ϟQ1*f?]s/v]IH8lYd}lNQ:raNŁC$dP6Pk /B)j%UJ#' SCm߼0)(S KǬS 7+G5FC@׉q9r`uT&?r > jȫ{Ԣ!TbFՀĠݸ-beNG $^DfaEįᷠiyH/8 د!ok f ,7c>^Y ]>?SeLUsv) &?eSt`9<B5=nՎꡯh`OÕwF!:k+j >v=xD0a<8JܺƎ2Ϣ ui |n6b`h|t5.b֕\<n"&$`aCR#:1"c^RJ?_ԗ`?bB"K_߾upYE~f5fK XuleC_% .3+S+i|WBQTɑ`/w߬wԍPn>nm)\ܹ)3%<_no`)g\y IDATey_,sz锟c˻ֲ8lMLDU{-^A+'9CETqGVG"]Os`Oڈ{^WB=Uͧg*ܸ + !Y tDtmۙMIݙKxme.* ޠ׭%>nCplwm| (=~ϗJߪgs]ä>]|VgHMu%ǹkUiֿ x]Lߺçe}݆kf@5z=qmvYy!#Br9_|3Ŝe/Tt8̍4;<;v4ɏ_+MzE8nV/ 8w4-ݍ[szW8NpL5 i,wW}8Mh?endMbߐyZ;}ܴ-NJ,:$ Jl}zc.$!Of$)l*IhƮ Oj$v=6M(NGΪD1G9f9q}h+=r ㊜]rٿ5jbh4@K?LG _: @aYѸ_/H ~Q}cPtj>mt!PU}ա G@f[FϞ͟;39y >C"\/<dz?b+pfէ? y=̞=xs 'ijQwEQAK^èO84zK2xuvoOWn֣7^Uw>G\d9]Vn_=chi6f~zk$z gGc-Bq˲^V/;p9}C=8i3`G-O[Cr/"F9O!\U VkImX((îrN'{o= `^2 ;iXRƷ^W%_McH|=uI\;f,dqAqرWY`ʢMÎ*;t+d?hдፏ0~lfx܌mbќA7~ tb>ؖe%'ny| 茟aŗTwI ޹ 0eerۣ/q['2,<%M y<{׍ O>~OyHi7рty\:2 0j7mCuc2x|GgDpגw%H񼌦B ҈0 %_uX<\Ьm_}6B'M-^.3G){-Wwh9U~A$><2KPϗFu,_ oubCq\l:z;/Yc[ Aܽ\rFN6΅COXD+y(eՏxTɕ߷Laea4;o{h]@vl[@ёJҙ*m!䚣l^zU7m>@~Xɵ7n~3\Uf]G z]ԙs˵ ׹5 K>eG:\X:<125bH#31+']@ϟgrÔlZW;(F1i)!6@oO9 #?- ʇ! \~UFE,ݑF1rOʶ#go|%.h $Fٕn-p9-!0ԁ :IA8:I7^4?32pu痩7vo?9S4j 狯| ~gWc>@p{x[Ν~\HË1qF7&pϞĔSx~hs.=ƎߏPsYېqkSO% Q9=)K>/iQ^ǎdM5A՟/M66Dv*bZ\d}$1Nf$,BS SBDMOTB!N (JFFƑXI !(o"B!BZkU6J̒ X!B!Dmw$U, X!B!9CFB!B!B!@Vn Ba!g1mx `0)4kb,XF]-NVff[cCeݬLXs{&\e"g~ytVYx*3;Q:.c~&ƥh.8mZj/Jm+\<0KR# 󭓝!Ee`W+ܹ^L+:K~)zfwxWvl;)+W=%]KV!8TV$ϺP,BSDgO(3ɠp^? 7N׍*!Ws1KT1@s|@h4_;mhF:Svr'ؠmlw+)/dG+b5Cm5N]2mj{㊤w]WEP냦s0܄t4$jeDAHº[FXVR +il1!ӝ]ZG0'{2G|]4BQ{y T+@X!DQ۫v>oxz{ Џ D:fs?#C,;S!n͒sE!NGY։v]e'c;5Z!ęk幘Bfoeu?:nvԷE7h  (~t&'e[6%njz*47W8 ķQ̴fÞ܇<ݏT&-pVcuv;GK(ffF?uT|C#%L'ӶZ+!/KcZ'3tƒTt4f~{%C\Ff^`yBQ;ykYdE!I'#=_*gH$!B! y}B!B B!B B!BZ@ٰa0]GB!BXUHO;B!B!- !B!@X!B!a§p{kt_x 꾍dFvY 8X6m"KCLT Sh^ty1G`F#sOسH1S/:$]=p/X?_=oI<,B!D \w}5p+լ3'UUR ?wvAJB!BI`ٴqY|7I?>εwN}98Y&{ejjEs]3'!#2S8MKcC3`{:6̝?ܸxai2>B!BS)t&!͡4t/b'9a7&/.90uBO3 U+p9#o96V8$ !B!]S1]P#:&6'}NN8f Un>F3NB!SzGfj;w>+t;M̢o}vJA la꣟:#sy*3=kN3=ݴ&?nK${ߟv[d]?͒B!.eÆ z\8tM54=,9`lB!B B!Bqn1JBj4^~[A!B!LB!B!$B!B!$B!B!$B!B!$B!B!$B!B!$B!'egryk_Z8MOH ,DHq z6't*`׿ػyMHSEogNw33u1g/p{ytAvjxr,7B'ektjz?u32g>xΛ?y=K3n~}Yo5Wz.W3'nFIQ{>z'bK"hF^~+'Q7%@6|7Y?doFp\.q<7ƨ17k}._Wշ?d#"[;nchˑZ{eho2֥%xhLW咔8wZZ'|G5Ofpc\p3$g+ٟG^Ws=:@)>8n(ifцds͝;E99式M9ϭ$oғQƕCQ緧F; /z#Py5I0ѓ۹qc'`n9sfB3z'3:&Zeoba~2Ĵ1$<Ӳ2>j8X6m"Kwe>A4=ۻn.c_dicuqeXz/,gej'BȔx=lևͨ%sǛ&>swGvB ׉m1\؜UǞz>;}Ļ-e{zpmڿfBE M7ivLY^Sc8~һYCzw_&|Oi0#NOsѼ{S(>!gzEv>*oιHS-#;{yeB%/{YO~+JT&N\^jUup[yzcKo9Č铙9=2&}Ȅwo ؓۑɭ%k.\:=^:v}y%M4s&m^*xz2 <5ݩ, n|kxg93,hNr6nޭ^}Ƭ_S7H9lOD\L;p0hzcsu8/i4WqNzGJ̟N_EQ |M ׼;9y Zx6hoGP!Uk"֕9,;vȯW\.tV1m4}%޸v]5xg,-iI ,D En?:q|Y̺3]kןjB>dTf\DI҆s2ILO|o +Yi?~X. RVִ4OyyxG`ggn7m}\zEwg1s7 ^%-LLyYc3vV2BZ+ջҒYz b*YEP.j<1& Yd~!tO%vǿ\xIӉVUu\__cMO:=R{3Ѷc" '$ֹקgLy9[3)ğ&p L{ `΃6e7Әp5sD4ʥcnfhTo'hΟoc[?jc/nFPUQyt"V0g;~[R z{Bm[m|5O]!]Ŷt7AqqkEFj:Qc۫S4|:w^AG_ hz(n% '>YxLsQǿ;cS"9.+u[[_a|+# c-OKK~_Z|m,d*7Tgq].!P+[3W? /³ڝ|6  M(g; ?I:M(/=_u=oN/|Ixm5G8k)6lơkBƼG%gg5K_ʋ9_CqBB|B!r]_ }:I,8[b#,B!K(B!B!) !B!@X!B!@X!B!@X!B!@X!B!@X!B!8% Bq*@\\c%QBH ,BԔ5kV@vv6ݺD9Gz1%H ,Z!$B!jl6|vIB!B!B,MHH8ddJddq5DUeH#!B!,:6%''aFбcgڶmihhth|BBܹ+H" B!MDu̼+vle3˅v{|HPrw:t8F y4h'Y9MrE` mwt_2:,sZk=?NY#B!8E 4z l})i ym<<&#c v7,k0V2tpvq/v6l8V-G46l$ZMpl޲HcK ٱ4P whsE?!i=r CI%9\ѧT@X!BB*C7"=q!}*0xFY_\0 IDAT S3i>V@K'_/G7x&Z-䗸$)W.&?ny(̓Z.ѣq:'صnݖ~dI* ӥs'rض}mZ!ӈOEorwkf\ρ4j,JvGz> !BSL!$ =>&Lx:[]}7M_q1'⹋e6[nCu fgӖ-8Ng>lΆM}UK!<<@t]??HMM# (,V ko ''?z:222  >a\DFWs(VZMrJ*9ϱcXb%hsvލA5[?EuDh@4 @E =\7)>.sP 6q 5B~+W.+VLϞ%QH Ql6W@UUZnKhhtA%++۷iݻbHb U-B!Y}keքzF`0î];ɦkמK"Ja!B!)ΠACbݺӪUBBBe׮ҵk B!B!,,4 ^f%!!n $*I B!8]Ӽyk\K,N#M{LE`D1F?P 8oSBBt7hntY,ӿ]@(B!ΌIJB/NL_㗵RMCc A1C^nBqziN`ǖuL_D ;^C~D}I$!BH \,}??6.A  S^^x^|/CUU!8+麎({rE11a )wү<~B!e%'r߇?#5<-=̺pPkN+BؽoD1GXUT*L봂:XW\J(qQqr`Bqn.9{rwڤi `M#HЀSdMXl.|OQڵ+.~=7i҄Jg%#ԩSwu]g{֭_HLd6lHÆ +]5kxj{Uu6mDNNGzwؑJOIIaׯOӦM+]nfwY,tXy6ߺu+۷oOXXw:Ν;=׭[-ZT`ڵ8N4۷oy|ǎyLoݺ5QQQ.֭[=GFFҦM*ϭ+WzdK.U={HNNޢE ֭[lڴczXXڵrݬ_Byzl6W}qj yyylذkйsjFc(3elj !#@V694{]Cf]x71%B!8iqGcm*hp]tC* Mn଺t]GӴjؠHkw}o_mM;(/iv2φm_|rw}'dNtOL(ZuBO tϟFr~A5괽ҟO&}BMB!DCh;vrݫm9׀ԪWCjE\XMCi)ʥ_anضBd봗v2Ȧqk׳%a!B U+9{E5$^@ZK`]Em۶^X,UaCddgcnPܕK.`鷶j~:u1nGmUշA{ZhWUݹKdSN^/i޴iS}pif2U0hCиqc]rVkˆx諸I׎;z췢(> xyXsKUUڴiw]߿疮S堭FX$ uYe *2( Ɛ8E3LY@5PjO!B@??pA.m6К:l4sN*@E|X+E9A,O sAP3wU/W0(P~T{YS[K[STm[Vފ.VGO~vn2A׉siU]$*e5\k oD΁CY2.KjO!B@uaz~5q~~Vw ڝů!vC>'\sic*GPQkqF>=/[X,%IL˯pFO=4V"[^$RT#h~[B!prV23tXݧEsv &Z4@ؾkO N;M NJJQ֭[ݰlrss=Vygp1l6SN*GNMM^zUwnn.Ӄv.tqm$&&eu]'-- 1N:Uޥ#++˳AT.|c4[nO>LQQ*瓙1= JuݤxW+kӽY]B=[*Gu$VUuV833Uޱl^GʶX,DEEUzniFjjGţWս:++^&T6Rv u\9vF Z28/{ U-8j'%mԜB![sV qSߕU۪yg4`Ҟw\ױEzBB6F*L<7mԧF}uuԩ%%%y "##} maÆU.@ 99IP)((ȧ@~׭[ק@8>>| ҼVէ@~GFF8p#4DEEU{}}d)aaa>^<[npFF'R& ˄@2\*(MeמU~׬5|i>u !B.,5/:V rHKOm(EBc!Ikt9HL]l$B!^RQH5p|躧ݏڱ^p&S4Mcg8N6a4b O#L&2{tu+???/lFHxo_ض(>j#Sfe e}@KQ,GBE'P|Io_-oznY,^b4LωOͰ GW~[s~#O6I ,BZJٰa0]\n` Sho-݋\t(,^&Pmؾ:hBq$%%{^,7G1tP ',P-Qŕ#u+^_wVʕ˨Wqdz-OB+pwXg GQ17T..7z7U!tw w n`5#B!M'DsN[2K5死وG3vb{Y!'LM);B(ae O8W5KQX!B*) 3[hg/yRkK <:6#*M~r2VlzK y$BSB4p`"),,~j%2284f/.V6ZA sh("DsWV7Y8h2W`!8 6Kt57-E4`gPkMX!lذl Aǎi۶=z*b4q:,Z40:w*o)gn \JQ2Xi4Xu``ͅaBG\(*lHJ+U#kAevݘtU`S2eA_!ifg0}䢠LG7L;uL{6ّf%wY$嘩{?4=,9`lLϞ=>|$.q}GN1tpF# |O-i N/YPVn#1ĵN1+]FǑn Yctlp \ĦW{(R ,yK!Nr`lWT3iһYq2OyyxdV[ʋ=GpaJxΔ}k\9ND$Y2tpvq/v6l8V-G46l$Zc\$[ gȎ^_C+Teޝq^Z}ߝx8Jv2NCf2p!} \L;n!Iı}Ķ꽻G+mre|:U]]OSb;~/#Se3һd3<֫K:^B!d\'&OU?K^ӿ +NyO.^qǒq[^)]Y-OgL"x_i,La֬KxѿpD0M'QUUu e|Oo*+vR0aS‚.N *V]?>]'\:2ņB!.n;N/2\VNf55F Ѡ}|U|;ks=>~/n=O59>>׭D/ _珼̔wPX1Av~3QL-H$=D!STJi :v3.Sd |sBh,.mti!w+3ؓ߳4x]:2jJuc:K;-B Z/NPl0Yا5< [ a)H!y4{L]wy mǯ>WLv_?9]im=?~ɣ%PxZ6cc^ saJ{̜=;H$BG{BNOM a$Ip==jԅZg=.) eɶ\V0&Q{_ m5/?C駿ǧ.2j-[sϭc9r$Z볌8yM^o/\9֌'"js!ĞW_nԵZaD}#|$B!c2#?޷]/_̥}so Uqw~w'CAA7^nѣo2k%8Ӡ3@M>ᦛn%//O ts&cMd%(MYg&a]›oθLU J!HٶM]]-hhz_B"0a$Əip7B Gyyy^}ܹ nj())3itw8t @-L Q DB6?>cP<1UQ,_;ZB!Z}nd\z8a`.:{)&N̢EK3~CYYW_:;;ٱc+>3gS\z'СYh$b&.!q>'|9T S)ϲ ]ӌy tx˶/93T3s[+x$D1(bN 7Gh#2 Xڕ'Gir”(kr{?B#H9 SoFuigYw~{I Mwul]\vb};z+),*>p ˖bܸRB Cx۶-a6PX`qN 'cd"qWso#׵͚t';uJpR=1TʹzmaSLJ)Oi ڎ;Bu>cDWk=oyǨ˱cG@ ^ϴix}zI91Mam=i{aN̘nN]UnBos0J e !Cضm[喵XcX_ɓO>ƤISP/y)1<aò+3BJlٴ)FgB T$0w0DJ&4Dd l!gH!"ƍρϪIt&55?~BIB1Xٿ/D"@%qX2)P!9"Bήm%nJz<tut`ܴe\8\`bqoP'儒qB#}ŴFxLŞ|BT.j/. &cD$U~ZtĤ kv֭{9s2jhw!&h۷#Fr뭷z@BH",-S|sZ&+G9 von`F^jUס~.ݭn㣳W7R_13;}S2Bڿ//-8hͬ%6^/˖$ #GQXXANh&0b=JB!.D8ƛ8͝g U CV"zb=C I=+܎"CipvJo |9?6_үM)8_/lC;=&Oò-N6_꒜8 ood^\M+E|y"n[4qBtCyO'SB Taa!ӧ`ڴ$ b?z/oͿs._!#Vl:Q%3+vdyG:j5"cfngȘI8=5~Xkujŵ>eEDyԩ#iCvC~X*V1}bo֭,;NQ;[!!2Ҍet $5 (v'N'c83U1K{_H'yivog-o6FnKƽmLQ~Vxn6sBcz`*X wRNl`QDCs3%on<gHKɯvRx^^/'oQwS\\,$b$®#qMy^7A_wQHf AP'IpI:HJWwr`3m6'KNM231k;ud>3am ĉnd<4ɘ:7v6:kMQM[d-m{ցv$>Cz후vRerF'gYK;İnqbdN= no'ha1K'NHI: a4'|9T SL43esRK Y fS̴fiKXaf%R8JfQ^0YՕۧbw7xޙἘ51RuyWa:&;Zs$В)- =y0(N舅J!BO BfBS=U35(Ţsn8хg16uӖRTZSR;4N-kNDg|hƊGl>!'F!BI9M(pZ!4x{jP3`3KBzran-B!.DXk}<q]lX %{L'|piX*}Etms *'PX16n[ vVFJ$30;[_ B!Pa4$_;5&sȜ#G8Wg<^ϵdqi9Ep2~N?!AI=ro{v,<#ӆfB!R"4MܽQ!%/1\ga un[^xO:'GerNĒ$ԯsY;eqT!BJCI63< z>s(0btAgs/ү/B!Ba!D5N%it}J :  1O1 .f⥸n6n|vXpDx j!7PI^Ƽ}YdgL$ǝUqVb<4_0Zbe,{Hh' U1=Cb*˘ #9F`*X:j5"cfngȘI$VX6պk}rneYbSGN0 0UEs1Oͺbqxܩ#j;QGʘJ3 6蓼7Ij [YګB:ɳN}DX*2 Ѭ97+heJUekX}-e|f޵t4Q-ݔY:b2 7LǨi>=j8§\͞z2i| Id[~Ü_}/0=&;-gYݷO黟䵫~—8<#}4>oGmc6K,yanemX UPin# 匿d Ƨ{4.J]qFy9c z"DgGfp h7 6=c^^~1Gw(+$5st!fjj66閬td~P|n}hwvw;˷p [c|oGt#:uzɻN^ ߽vÍ$tǴ唔nnOc}?R^Su~^vݱ]c! h ./^GΗ_08Gp{./|]K/>&- d#_~e);UZZJcc=uuRo#HKN Ǜ*җqJ2b'-=%,rDbkmiR'Mݼi|'97ITa̤iŞ\sO>Ϙ|sJ[&y`6ydƷy߈xە?GZhMcCw' c&MRo4]L0]}Ji{LOC6kg?8+> ÝW2Y!'0c)!({h[,\[HnaP*g}]#eeʷoŲe1wrжmv%eKPM(Ӿx:F8 ?W2}k˶;+ԶO㮜tyUB!w$cKvw|c=?7 ϣ͗&vǽ|ct4]r?ҥ+(--p3i07^g̚u`2Yf,>U;&u p I{.DL.3 z^LÜ s?{+B!İb2N y߽n.ԔY7՟p2V~8R̝;'m 1c%%ضOl &|jJJJ0I.<>4Ԅq,郜ʠb4ga Gvt.B!kNv؊cƌ= qũitw8t @ERVV&'.DH`+!2 a`vv[4()B!~i q3gݒ7B4iwsw+:˄wpU}EkBq!$lݺf޿XH",ǹ+Pa8ZCIïzID Xgǣ|Z\,B1ԕ !$ΕWhwaS]kTf |ҝHqp3. -fn~ֻeLA cNj1Ym:%#4=%xs;d8"gĎ(V +F,*.,|EQ'Ŏmq}&=rFTg;\6f"lGÍj~^$ݤ!B,n Jc^1T1o"m*Ia*ΘJNGu3QȋrXN2czj|#O_ɼ֨LPCN^Y2Gwl,Q7tduJUT9yP\kS[t+zmOW:['xbU:hvneyq6:u$YO{N2O1Of&:};vQLg-Wt&n7'ncFn R]LqY25,͜gxm9i$I <ʭ0";U^A#Ok?yxhKܹj~YZCxa++i[_3oP|m|?>IW4(~1|qOnflq%]Oh+5yNB!"h~wNߦEoaݝu9دpZ{c:uz (&&ݒuރΑ=ʮ/ޭ;ح;},tO{;+|Q/%'~ſ\slcWc|IefLB!C"\m]R :aĹTy$C:'y[bo¼wx{_łMڈ]d1dnbG{bk.XlQ[݅Ԋ%v)ؖ[  uXnbkvo7QǺNT#/FMB5fR̺Gq^9:Lqp&C~K|鲬1 Dow==B!XኘAir3qaH򨸋򄙳u- k֬˧YUh  nKcP_Z@K2dXbB~Frۆnu)s3" / fV 6v2wY ؐ? SAk\8n0$1qPCnh2^k/^`'<+.5s+W[)3tn EG[ {,?'GLN5ij-]'B!a!M-L,!v8p>H[s=M2=ڞ =֞;`K@jN֥>ޑ(֚ur!BH",Cvβ92\B!$B!tG J {&Qx2$!B!DXqAZ]1Gw֚_nxVrt< +CAǤwX"4sj041zXnF &e(W= qB~ ( \B!Śftpk dvsLw6D%iq"V#Ĩs"ftrʣTg,۠t`23M|WYNP{ZώHxV1et7q8#d&lṶCP93czg2_JN|ajZ.Zwsktܧ[Yk{q #G 2m%n'1}bխ,;d4U2* v67Iحh(QA2XfSGޭKd@'/\y;׌rk.#mͼztЎhS L!ܨ?N4K璗wA{,c߾=7nLI(-TP Im3(ɘ⤎2IevjөN_pG8N2T~Ʋun`Q1=1d*ΘI^Q馧5F8 )cT:IeQ1As 0heP4Kkd,ۢKtN.=1mj+LEsK)S~]:Ad1b@fq6;1B73)b*X6r:Ym,˴p(V Lх\8\ibQqڍ ;fhLr|5zN+k̑1Re`<1~fڨ'.g!,ufO1ן6]~ er;yaʪ*1n8͘'7Qv۶ uTWK]ܹ\D&L1MS K F/_E<'Of̘ͯI",(M :$ N$CJJm:(5S%jole&'v@%ĶC|H% SM&Nt$yvQV؎tde>mQENeQg-u4Ǝ)鱁JAOud/vBuo,TFr趓*Z6KLҥ,^$ȲQeN.#3 2bw: 9k qno]Ɇ#{yͽe3I#yD8Cv3}BKJqh= 1liپ jkqu72B.t.8a`.:{)&N̢EKՈ2@Iiw:Zw (Vz2a{nw^d]Kh@wnݑu:ݐuq]C؇uwSgm?dաwص:4 DhБr>W%ketq9OWo+ͧ-.vAƕV= ˏ3ke0?\n IDATƿ=;n5l:VMC׬KKx~X0X?ڈ߸#|?*OUЛsC<]ه>3Q+ yM&Î 8GEX3u.ͻ>K&i芆Bw-[^#.[֚X,urz|l-)Rn.uG*}$a!_lxs/Lfsm ]0},+[_ນY3e.x7ɗpd=ݱ*p+r%ikњ0߸#+߇R* _L,~w\>el;ᅃ;ĪӮ|| vXMSRe#TG]nW[Ǚ7f2cTQElxwV/=s&b|i[`dQ)Jh w:J)_P2?Kp!.ǎСyǰxOv g Ƕ1˸q9p`?/z^i@$Ba)!ȝP<=O<;@>kc z^8 9 ϊٲafąnŊ<#Ouvp 9; Ibw{-_^o|W".{w%@2gx= 4]z_03bX[kv֭{9s2jhTe9@4bĈzx^)PsmE8Y4jv?|oSDX!Bk/>~OɺA_hvVg5Zzrߛ ̈lJBMM<_ 9r\n U 9qh4PXX((v""F`,#NgR#-$Bq2(HӞ&Jtm+#Nv=`Cj'S%=n'd$ y*$dLwJ }/̈᮰g0mtX7 ]O_sHaAb=e=b7D4Ny!yDBsIpaK.i䮫" 8o63j9!IOXyl(#zc eۧkEFyJr܉0[e"JS WQQe$X*3'p]We::]~}na9e-!ujDtvYAi]Nty+US?cFZwqFʲ^QdO' ǝ AiFcL؛u+{ŎSGN%­NQ2Ҍet $5 (v'= pN򘿍/<4q9O7tKTEF%f@Ny5^ecf'BcS~QB!$v^1n^t1z][#N6tgD3$Όmqp->&'YY>1;ۺom}a'{㽶GLmnZ~3ĮRVםԁ^Ie6K%oVhԑ%Qwxx2B!B D|35T>oڜ(DDiags[D ccWr[kM%;k4]vtfWax)49e:-v(G:]Vvr{Ϡ( 0zW\^o+aɣ̖; [rYIxo]0VvvB \!DMM,_x)d!o\wGoo$ƒa׶;[QU,{A $B zѲ}}bL}WQ}-PB/tqm(Ŋu׺ʪ.v].6PQQiJ' @ :3@ @~?|ɽ9gwΙ38鳝-BY  EafFyu89'!-;eO'N iX,-[[!Axu|l*(J5ӱZmy +蘚8eK-QtlyD8輭PuPd:IтZk(3]-~h>UѠ\>>k oۧ)Z 0׮au2oCS*|a2!'1uxalwfnfyEncՅv$r@}I^ϒ]VB! }{Rdȿzy2l?@Uզa4]birm&cK-{R΀ZQ¯wJܦ5ulJpKp1۪C9u7vYE9NS/ 2J*M>2=5s]h:hJ`iwRKYH]dY %5yދw.)Ѥ(AMe֓iVrz`.7K_y|oNYf f/(cz@h7o7?9 acT%T%ɴ".P[4o/spct]l Ѱj U4pJ֦&o.EŦM*)u#'߬o2Ld m28-&A>I0,tAh2jk(} l[M"IkZ_z@C={CQ,Jd !@ٻ{HI&4cYGW-I]1ћdڵf91Հ"[gQ(klQdD69DfxvPɬ"X˲7hbt#}ok1Ћjn2dfzv6W5hkLg!XoIkO6;5 Cf&|3\ WY9Yw~!q,)fvVW䱼" EUqTU] ?e`8:bY,`Y}>&~!\JUtܵrvBъAʠdgq'FkekQ:Kio 3x}⦠yRaS]LE!*?O+V%~gsfQ6wu}ͻt1 'Rгr}u}u>ݳۣ9}-O޼oltE^B/niEf -F[w} @ű=حyDD{'<2ºj.E=x#Jxp^64*wWR9~7,?90ܕEQxc^ͷ)BWBb#0UsiB" 9BTW2F3yeO;.#bj\z Šauj=(&^}bGzC u强7i!~G %:v?YC~U)9*R")u;ɭ(n{-EEU<ΌL2 2TpZxc)mҽ(UGgC~ܓ2rO=rVQq>n^Don@߳]8 b5J~XBіF|A7g >IF SIROU:IE y-zgߙއǐ|a!ı1c/:z~ذOW-;-V#ԕIqM;ćDwSQfҝ>l B̅2(+# yWST.I[4/JuEG~4m%7!AfZ(8}[Y($E>r)bGy!; ej̛aq{OS^_8/UQK\H89TSQBPzu8Wnf]#yl-]yuJJ*SX3MIVV999x0 0PUMӰ턄бcGZN7N7^G%p_e; 4 إsX0) ,lj^H'N lAnQo!ʜ-,۹~4ɭ(!P6"mcfJjH "M͏[Faڲ*+ q!~s1nZ: aspmb? )BG[(F%=|>V"++z_RRR$55Ν;0V !Y,dq}1gP7i6u}a}}(lL!$*h>ޅwdyѶ/[c:W+£_?u%zql:MGL2&%U,ݹsnAi^#=\;Ot v6~W\[C߿O͏w s&p4Hf&N !8바OA8W9JF倌-Z3LªiT2y,6RqظkspB"ymx q\2)~|t}\ѿ: ͏7ϟ˗f1C=b8K_nUbyᢛyOs?dhv}/=~ )٪}^.?7£,iKn%.$?|mmΩwwl_4uzX><;sL_!qtxlvAzz!8::^zf;o),,dٲe\bЮ];{#gx@ fTo9&N|* !8Ha)8I(0ҽD^u~wVBQY(լԨZLķ: _F6^6lc{ń5;NLlHɺi%}! uX< UUy}mL+{wkV6GۅYUVn_GJT\-,ؚp4\6aqzxu > IÔu4t}|ܵtNǍiT~ H#/ :({atY6Y+OynV֐#>77 Z@Uf[KJJzѽ{K-~wj HK>}7OQSvc-VU SBqt2?9TӤI׍syh#RхV(Ѭl;p*C)E_!Ylvۀ:a$ya9Ӊ !?bfy ;p\2I1aړLxIb0 Jr yitF"C @wpzu콲aAĶ|%k?zej_'qf+ܦ-6?>zu ˩ΩZsTwc <~ G:0{oY뢽ǍN3M즏pGw1LOc☑R>q}>hFu/^|F?Vk`:}؟"bN&]ʘy^ضړxe( e5;qYQtJ`u6ZSe:rPVWOQMjvњBiauE GGSUn|iUr$ktBwiYP?|pQT^r{O5:';K? f0}}6Yqqo2qZi|G8=z,ّɻKg)2 e8NН5yyי\9tԙy0mIoX4W~ǺҗYÿ~NY}nnry\뀪ޤDq~!LEi98YW`c´'1LϛVo"CU5:N:oun'ne;7rS/]^ܭ'w:Udpf`{z#"HJJ n+W6y]RII '00Ϧix?EPPݻwgРAu׺5GZM>:b5W-===t8IsRY # 5?demtuw8)R|'apy웈옌f_%lS?FLjXϋkOV2QmgL2_v'!~mϺh>KNpF`2 Ċ0h BCCq8lڴ&eTQtBrr2G&22eZ.ֺjC,+&9kX8B!M4@~|R>q2a49lډŢY((-$ URJZ^-)?C>YEP>JâVs MhJv<7st's痯3 ZP j~.0qcE5\5p,}š >Y5Lvl*==x;XF}x w<{&rAë{:{" dLgqߟ.4nyoqD?jwKN@{AqΝԩ֭cӦM}f[|JJJ 0=l4BC4sNLflduU~ h tQVh6"cXmTTPWWݢ-ٴ8U MS !B{-~brѸItܿ Ȳ">r*Ue t8Y8%9 |RM|ܦex8q3㭆-6_Ks33g4y>EP{K=M`fA>x_˩q;9G01<|'7?Q DUusՇ备ׁކg7pzT*Lfa&O($v̴T'mX|}d+xeb3 օiء!'';wRUUFuLl)nAǎm3BiRۚaҵ;qi|/.λ٫vy8X弳ock*䋩]]N;,2B!$wGd WSk.&"7_(o(55gc7 ܪ 22 2~]=;ʋp(\vѭttw<Ѣ0dNEBfft:ի~~m+8$&&Ɣ |d6 ) QjҢ `mi +ѦkqKߑ.щ`} dCa6wwWԛ>y9sᮯ8ݒ3sՍsӘeMwKvl$.*g?nL#-{3]Wl{5fmX~@>K_:{"}|ghC âB%4aEQ  (ȟz\nϱ[b Bn̍HfXx vs6U䲼"*Xf!$84B#Z a!Cvq[Wvβe%KnVu3M/}܅rg.¢[Q/VǸLtnB׎@b㋀~'K76 -g >z1i<>cTE nbe 3'ڸrw zW8m.Utk@0c;%:(?6—7>߾|G;sya;aݢWuo8`1:Dy[l¢ |WVnm|ԢټbBIIȀ`KW;Qmݱ]9횬5(VzZ}7UUE!8؟z**d mw yf+εLLHo0*=A;_o'qDpbSm wZ*祯8Ofe_@civ.їsywL˟f%OlHdH q/_BtVNcȐa1O*dE1+m;Fv:U{ð d[l9Z4x;"; 3O Daj 6o&,?a6G@?S/XIs1YñnW7|h4MoNBv{B49G6m3Lzy>ZH+sBIuI WTV9?.i$ׯ't 7!1:[,d-zZBOɝA qXt? EKvUe`buƚQVxLxŵUη1sݒߵj#bx䜫ic ;JW>]竵 !3Kh9MUyc,n_O0w-6;Q^h]ٛc06C^e)W5 y*8)ܶǜm aovh]O VVRMD.@KP3B!AX.tOg?0l( kAI'^&PYȰ𚤖p6 ?T/Seݏ.k;>GV@h`ĉQqq/5jFWyL7^蹆 ʊJ <(|t}ڎExx E5[/J_Gr3(*g5ćFoyf$>[Β<;s޻^v19Yll.5ɿ癗YW.5yHߕEF:}ׁOW'-T ݵX,PEGUKWh&xgn/]!aqaY&O­E5{V?iulP98 ^U)ȳ2ϣXx5diW@w}8L/ՊѤٓ(҂UJ'(0M6|&$/߶3`U6nr6Zjc[i>W֒PPl(j? qZ<~5ԸOm$v!=o;›gގz0 3w3t68kHx}W~/K=u"|8 N;kwe_H]Y޸鿑ņœqגW=uTe'l# EbzXSgW: 43B"a łitaG> =v.JP;P4HêwISθ\˾sp|6UkX0"ymw r[=FevYFוm u*DX| z963IK1NNIgȨѧZnчƔk0+_g9 KhXMj[YX 9ӏٲY:2}q}}|5E*sFnD!GvN9V͵fEXv%!")WOaS-eN_8n(6Te[҄,wQb0^86:c9$G%=B!14^~$!o`H}++-& -/12t7e!B!$& 林P9ny/z Sx䥡8:,N ,B!8Fkmன1vJD OU)UNT ,B!'(Y B!B!}a!B!ĩ !AX!Bq8V] ,B!8!Ih!AX!BqaiB!-$ !B!NV׋imru]jʆ ,B!-ڟtt:Ժ;tBjjِB!e4M_7@*C9TB!B B!B!a!B!BB!B!AX!B! ,B!BHB!B!$ !B!B!B B!B!H!8yp׵e IDATJ6:BSn&%'aԆB!B7 995Z!B!ĩ]ssq:??6U88yR#11IZ!ڨx*!!BA8?C rr\tKNrmr<B!N l 8!h8$BFr#qB9 !B ~y>ު@Aގ y\<{>Rq?iɾR ^{切 =]ڇp~Lyv:z."'Ax_4n>;CExB!'h&||˜XgQiWS#=wC[̏`xsS|LI&I}~g[GI=1OJu̝NE꘱ a.acZvS{p1W M]gW~).bdzo|Zrҗ)z[p4R2K+x-i2(-!GonW'./aNӒ1ڶ˶Q'1u4g96}k۷qL9']+Ue#6:`y|Z#SI m(c惏R:%n,- u^1[jJ")כ6Ry~4w_xٰEH800V0hrĒAZF ԛSme`zu'Nn͜(LTb Z*jDŽ=HS:s. ş_IC$06~YD?ɥ2f\:$0Kεt'EWb>r~s{o6=7zLEVņ5tΥEQn ѪAU|_֛njLj}ovwDXǧ߁eG^~k c-$ &gecL۾-[wz$pu@TcTcQa{.z$*&~bQΙM&sfBND-ĉL:cg:<<R5pu)1d0nIl]>9u& 5hz8&䛙Pߑ1gWgA&Y̎sx)tW\){.JaAUK "BUZ5[|V-e39׭S: ;BR8범n(켯JbϡO@s?+H o;?.kYlJG{5~NlôFk738ϺDF_vĖ#51W-ix+u|K|K[ogGeYy9uȰelZ4؆噵lm>!WtoHS2|Te3y-/]YlTb cQTctoìOs}:3㨦9QC{3"f13>Cϟԑ)IìƼ.QDTrf>}sLj<ޙ55Q>▩|y"c<^/Oˏ`{Vyλ<;#{޼ޜ[X<-oype*"GfěF[qn^g<4DzIK[S' ,oD^̵^9tH>`v+Qw,!:qͽ&ҵx5~R{ɮՎCq!zOq;%WavgG=iq8/!iܓ*W6a0'E14m:$GFEŪxb5EV:|=w1kTʌn\1"nwwhzNM (tu:}wwEv?ed'fhᝆ0 2utڳ_0t}ikWoݑqtŮ \cu9/BwWĚ ۃ?Țγ+³{=]Ɣw'3M{ɞ03S ^v| 4g]%umqN˝T~${?ĵϼϵR5H-%Nulg6b܅;yشJ "R@v'N;ENfPFAAr!ɠx"ۍo􉜔GyO u"Z sfL(a(1j-[SUԦ=E1>hh&ytDXYKj]|8´MM;+ q.!Ս7Z:d>'m)3cNlfR/&ĩ(ƺ$6eh9UQP=OO~w6 yb"/c?zOzs5~1-DP@d8J6rkLC-XqmT!8tj9Yc/f/(Mgr):PzJ q鯂 Ç'>jX30WJ0}Aڬh7aID:ܕlL[\O VUlL2b@ 1 Z -GQ M~>h`Vg69h]CL\|;uytϝ15M>O(<Ar=xרՅ?/*ڞ? 'xVdDN0A'wy}tc6a=$rd>^ɲo/y~ˮjfRrAe]| Imyy9'E9 &{mr<B͊wyhdT8~XU9|p\\<99;lMrSZZDBB2`$BU'T!8J`GyqoJT >\B!BqR-2B!BSdHM!B!8d琘(eeeIIR;B!B!N ?Û՚IENDB`python-sense-emu-1.2/docs/gui_prefs.png000066400000000000000000000637411411441564000202300ustar00rootroot00000000000000PNG  IHDRm_bKGD pHYs+tIME+ 6iTXtCommentCreated with GIMPd.e IDATxwx׀nzt "1t [hAED @5i"; g33M64! }y;wΜ9s{hZhR<6/6@]mҍM\l|91J=h}YcӦuvJ k*~<)i#ie}'B`ʾI]9 wvv&22fzbLWWr'~c>`5tɺ1cnۡ.4.I0#w؆@[ Dxý7KNc7BAeYfɒ(@!4htCQ(1 lA #%}.=Ĕ9y2agg9a!pɒ4o*nD6LSfLzt藣wl3˖-׷ShתS8$]ndslܳvzkM%^DWcYbs5-?[… ?F?9GvoFå$4vNcwL 5>GU`>M!33䙹JU3\̢r`-ɗ\FO1+*FL\\nK=:^t٬9Yy1Z &OvQê%I &o Çy!!!y/_ףmqOo]78f'(>7oޠUѸ:`i_ա<:~a֮]:~ dBw 擳mL 2|-B4U%5j|@~O`ַmNKkqvvԫQ0n*?& ee~PҮDKWU޽CbjusMzSeމwTExz5+ G Gςát3RE$)I"zqE!1;7׏%8SΘ%KyNJ[bГ;M!u%~!IH6u@>\JdW`2%e]o\8>66}d7"zXLA30)a$n~קCC)UdSvHIaa(Vxc&...q@ MNg]ZLS>We bǟ%ઌ_]U\`_H=4<1`nBsWK&/+*jxK.ter_huyJ s8EJ *д 6q҂qVPC G0G( E/H4(]3:"nhGn\rB ϫC>".v>qss7࣏͜j۶=Fc+acɢԌ#iV^sٓI&E¤t5q)GTdcc m9z0X]đ~9UgUZ%NKzmJ!ODՃSL^_z*7-'' m^~ӥȦdAMVЭh%{}xΟ?Gj5?|G RCmY%+S٧DmY˞'[c?6k MXI]YN}0@,y;Ah:]CA[ .~1sluGGOe#K,`#1xP@oq^= /P\ďH+klxß;[\GS34)n%]M?%-1ekgMe#Ҹ KmZX}D[DF =+aCLx ~̭[~6ZhgMe%qNA+o36[jyl6d/֫םD22w6bOpxƆeb4q\e3kNNz;b7I{I=+tO=gslW?eƈk&>\ն,wnYφT:s2eh4˗BO_5brvϧWL"lV\+^+5{C/7Z]2K/--o]1w'GG3#^˙'+OzɩoXM%\6=ql|"M9d&i̻4r2^Ö2Aq3쎥bۡAd$ON-v,xxx a?M$)Ρ'uI$.cvC흖d7[.Ń2d~rϜ9Mn~$Y2 %RN0x!$v]Js$S\"Ih"V,#y#rA<<<)Q ZCޗ kџ]s:L6'i6'l ҥLwa[.3ɧgY,>sT6_x)J _x Mn;?zX?IC?Nl]L9)粜(0 e5tqOR,Ȳl9jme)1lnZvJ#xi,|^?jzuP PN%z 56ex[}.ޏg’Ll1&)XFsh<3FtwF2IFh[ лTJrtxÕquOZU0ٷo/{3pKDdҰ1kGھ6@Ph=1PqM,e>JɻDbyI&yvPa6o^' Xɇ~o[[^싋pF`ёPYhh G`p;Jn!1W$aWod̨^,(Fl c@בcݘ^kݨQTݸ%Z{%i}},_T+Ok˰ ! G7M痱*m(L49Ȅ{),Ԫ+hʗmSVOˤh9n,)a:uٌl&0pwo{/~nvfwPG>z9i2Gʏ?h4m<ܓ]h(Zḧ+g O[_b}\Yҏ>Gs[#~Ck}q{F `oJm kNռp7 4b_`ㄘ=0ًgZ8"/p f\ !I$>ęypCUnl_@kso6j~Cۻux +/٭/(uFWzwa|y} KJNq2b>CQMϥH"tPJ} +踱wvF>ȥKpuu׷MI–%ߍz*i./|YA^ιRA&(Dlm%Jduz@'.[Zq;I|OhKb? f ,ui!ɼdZ z%h$S25ķ-]g>/y[r9( |EHL,){Y]lvU2/j;}M@R?' dF Xo]7-ںgԚެ&h7i5)h Xޟ`N1tEmp {%8u'SHeשIt֧j4#fZ03:Obv:wS wt:[M>Cfo߾sgەp\0\lMp5~R!z)ɒojp'Z%l݁YV19;}h9$| n+إKΗE׉< }j0իȳB<{ %J pggFLA??BeAc&ms+X=>Mo17*_6sQ-pbDMfi75ʒ1^rH4vCF4y7͛9bSzײTru6#TjgF7mory$ ^u5V}ڟ$s^ts ًh?Ii1|}|ڭv3xq&]⌸;GGܶ%-65W2mhvmfմXFia+W>D2EȑZ{D aȑT~יhυS VD q$mfHdYf}p}zv-Zl+lƧj/`8ٙ{e 2l}g5Ř^"~M 6c4.РIx#O*lD'A{ިTHvzL&)gA ";|QLk׮<9[['y^)_ͅ_߾q.]qʝWh[1 MҬ-m-Cc+(f L"*fK:e4~i挻vE{ۅ5.Suiʋx3uРxZN";P%UeMq=JHfA+c+#J}race@ ɸʙ PbbqX] bT ҷ$WqI|Gz [  !;+ȗ-n@J!(0R@A!a6+qvv?KX⸸᠄LCl).(Έmb$Z X7f,S+.rvOC1Ohߑo.]H6K|OxZ{љW} )AV_.PS'u+_ IJ 0p+m3UW~Vvoglɠm>VKqr]BHHP'BC)r+s|{'\NLnd. =QO[ :5θTr? M:%U[k~*WGY3%Mo4gڦϤMї=F8Oc0dH< vWM< 8Okw+_XĬ_lpeoY{W;~5JUw&K)Odp|u~JwOԧARR^f%lFQkԩ{(w˹(AϤ0 6 oӥ4 AA{Zog&' LhΝ;Wqss筷gjBBBr rhB!,s><<0%Ed%v2fI(Μ90+$%PX4˩-ht^^}wj$ XOɜfmDS˺֋(ⵇbإHqԮ4p}:\KK;oDđ-E$sP7 r`axMKb1u1>ߵP߿+`yDA_"K7 H!{iG|1Z#*:^BvӾj;p5:=}9iϬ؇5dJâ+jD߱hy+3Qa֝Ov$5',nmݴlz~] _.K97V6m_{%JXڷEʅmZt:)F "{aa6C%pKҢFjByGmq6 .{Xؽt۶mke[}r -i(B32\ڷygӌ+?iL!j$U:$Eiw-=Y(q3lWn-:ѹhN7]@:&cnSZSNL=`t̻l{F߷$ IDAT]LMF}MQdut#.GȥD^J.< ry:R~YF0~:j1q~BΫC[^{[~ﻈY?W$qP9mXML{q%*noGXPt3X|`:[jDSe&oTnƾS}w7Dм @#L'k޹mKcק7XtÃTks gP&a,^ ӓ+JgX Y/&BnȈ?;Àt(|`iq4 EpdB+3IE Z c8xߒqv?062,?qwѿ,W([2n$3 '?Wj<Yqg|zWMf¡8;dʱ(`dA:t_~%Ke< Gq-R-JBOwP/ىGa E(VЍGxy_ʕTaD1YA؝xvG߽n~Um~mڣkPnAT>ׯ&>>>׶11݆PnAtͳ׶2Kv;ʔ)є/_*ϊ$} _"[ [ -b?udYfL\tG 0kM[25Tu3aJ}Ԡ̽СtO$IJw[A*?&M1aEFqҬwO-ؕ{9{؊~CRM-wLloOY.PITˋаnZǷM<~h+U3p]V{n(h4li.~Y~%uiR:{] wo)# ܼ^k{FT罂g"4{ΗqKbU;A 3bq7}Ϋ(Xp8뻙?Ʈzq.,ka#k5G3ag0pP1d<1Ĕgɜ0C8x+GVS:ioAn?W.ن\xRzzˁs&ڏL"\9t+(ۤNPxfyصGbx΍g''-;K]`I-T(+1wiиW$^qam֐ߩT̍>ɁS=X؛)j? K_,baɇ?fiOʾך:??g`(fR:'nnnv:rd[*ɲ {yhG9\q1Fk/=6*.T@5?ﻙfM9/^Ij)plI1x?.$ɨI9Q#*fѾߑriܟ*`=jÇCѧX_ƇHԊ"#-R(R0r85p}:|Te(j= -mR!'7f(d6bM?l6dLwf *;8t+Ӭ+^׎cLܑJPfY{oD6ngh YAoph 63t8UJ+0&Kz/g|Fcfs_|i u/{(%E#镜 eCcv}x4zMۂIN`%TrFtf =ΦW=39APipuN:N\\,WR*b_A[hPl@(@Yi RmD")eWjKMRt=Qߏ7~;#0H% ~uc{;4>!F:[ldjo?ˠ1E-=,Ep:#e2r(P$OJrOJci4~%i0c* ?#)Po< L`ZTmJ$?%;ɓR\'tzm[ٽٟQZ/Ғvv#ս̞m /{ hѩfy܊EmS~Jd);ɓR&΢Q%URJGDI`񑠯Hp7T;.MH{h,2*AuD}:t|mO,+R Oٍޗ{r̛o37]Bv)ɋ 䏱 Xw&f@_uT/6]qtZ>?%4xRe }B[b b BPʉG.\8ղw.! y3gNj[~=odyEiC]ÿ]!8BcJ/1P爔Y;1{'pF|)qR0E^bP#Fa՚(€ :w[KS ]5dl%L"Υ|8wӼ)܌X]ZbDR&̰5l|@s3m&Agb(@P?֜CQRn+;ɌٿҶS?A_׹SR&ԖlԥW#libL%i¼X3G>rXZbDR&̰ A{?{#_\Ĭ_lpeƃVUo+;1V*CCKZ~nsK2%)7~xh.;a$9%sB&kAI3:\X1}0yv5wb[عWWW8y"?GQE~zku[!YI|&2guH lBѡ{3d2CqMsg(vF||įV5j !rݧ3z`ïXJ A`}#B!x*Pd̅_ʧ1 V=1K+@>v:Nu9IzOLz|cIvHR;O #dbo5O_ֳn4&oMQ"SO~ }j^W #db= Z7Z aZNNzktoA5w111B(dNeP)f&6y<]_>T{S~/m=fNChH#Kq2ׯq\-yR.CԪ]TD(v ֮]A]>e$)KO=SQk|k >J\2T+Ic?vʡ,ieL@fDO%e~ޑg3y{X cճ#Z; X ɲq}hס FgmS7ڦ%mMq%MWۢ9S_)x7oXwy/x7mW-sӦXu:R<mZ6pKT!Y%Sh/U~5cҸzw8Q2ֽW1lǚ}E.14ލ1?9Ph4lߴ&,}{$1×K"`#D5~2V&o |vcVj~S,ebu%F:xb]ss>-aP?y/rf^_#(m@~6əhneY~6S:kw-}Vt-ʝΙasxCWJGY1n#ǚ)egʳcZsUzgJAbjSh4eW-A=J E̚f? H+b%Ηz-IHpxeKP岔FX&9w^/lcZ;wByWpqqx QX.t ^>+.N7 ~T~'B|w b?cNW+n>Ql=9! ݬ_e_eHj { 4rI3ܼ,;pFUJZsJ$};~04xAjLoG㕭8!Z6oč,+iI)ṉݔmY>i , '>,W cXޟ!'#ǘ400 ϫC6kt Rw }*.Z٣Wi7f~Y̮Q&i3L66ldp-S`ထrs6'A˘Mk(ucw7n)5S, sP̈́ӓYS@ěM|WsbiS.S>(n+fׂ`IqsٲtoLWql?`ߌ>T4Se$wfcl:{y<뺭U~̭MPe['>YXuIqbϰ)W7x: xqά ]OeyߒAtŽE,Nbܶ%[7iuim_k? NM2<-W}ھynP^7ݴ} jSbGscb-y ^Mt_TLjҘm[qke5u[NS,\u T,PDǵі]=țe$w8Y-տ yUCޝ|i?x CM8ŘT\C>Z6mgjRV }J/VPD1ǶxUa٘4h,˳_"FAc?w3T]6g,ƙ#zӮ?nF)a,^ 9WС4<#up gf:u3ï8){bGʖ-/8rP'f[b DQ|r۹%KLrdWhLk\6Uv\WD.f9׷QQt:pEYSPlAPU%϶]DEZ,rezunK>(3O\OVG-&WDK5~33G*Em[b_t &d( ꏇ;Ǭ2xׂ; *N#J-[e;[ +eRG"E1ph)+GjPo5yS֜Ҿb )ŹIMsQUQ@DK e[|Ofu;G2JR/^U~b[( MdYx:07$We2>z! qLv*y-n w'U6.Yڷ#ēGAѰzu<!k@mAB#5ۍ:SPfm pЌs5=-J֍F=rE==ߗY·P]~yO %İU&.*iA$Q3}@rZ ( Ι$(IFCt}SGy7~U7U,sDqCMj)UsOfBerrfXWD/b52Ԉ 1엉hhԅLlNczBY(7>pMDw^w'hX`"XD $z 2@םE3_1;ʉ+T_A/}\е;nvhx劙 BrN0Iki ӎY=5WTn'Eu硥@k7:"yYN2,sA~e'RkpԢ:|$c_>b2 e*UKhpY~Hi{- G1ߕ 6JeFh2S/?9W؉&RFO_/߇EgsfaOcgYxOX"ɎlN2h ř5 T20mkbeT_f qJZʹ2A&fUQ }vBZ*d6L?LBfjDL[ br@ -a9Gs lw{A.6Z 7o@ %a@ @ -.4,sQ_J*oK/SprN>E/QjuZh AA$>k$Ie$Y2%^>| IDATyxxR;VU [nՕjjdzgOp ‹f`uF{b'aj:^B 'W3 Ufn- kT.0qJNɯj0:UDgF>>h `ӓުh\3&c'}ڐn]:h~O9yGδ rպ\CJ?s,+0S/Eט53 j8h 777BComiڸQ4nd۷qssƖ1Ng ( :'0K֗(ȊAWEU0'{礓GyK;Ree۬wǗK@Gy5>d߾=ԪJFҹC{zuE%nUU};Ͷvȗ}P-NhDaVzT?NckhQ<#x@d>O̢b1k?lܬ|~|UrSGwc ," <<<(],'OڵkȲ(d"!H||ܾ}cǂx>|R"E=NcwNlذFQnf3&f2;RkHFX~54]]ɀ#Yt$hI"1`5L=֍dUPd..0b FjǤ)nߏsfۙԫ#<r2r>t1F^~L4Ix{0lAnD; y'{2?#RfBے)mJ{ʇaP9zŸtki>G^k\'VC('t .0r^Qc/܋"MUtzT{6F)dN hQnjM)mzvETW2n׋N?okX+gEkѮ@z/bhiigfk͛H,Q>@p6 [GiT*u^'4UQã:%TS[ɇh Fkaw2r>UzxVeZ`#v+yP" I>٘ ag 7 H;D$:ӹtFJ$ڵ %DXMY-.D:ԕtySt?|0cK7 K>.k38fϬK|s? eeDmZNIU+5@˾&Ww|G:O(4~q3%퇽63Wb~ 5]{5c%lF˥pDkgT])7;+Xr0~z ٺmL3%$$ R>0z'HY 60%l#6æN iiˆ/}̂b,^lCffO< 1<~;%Ce1 UKQl0uOƋf:7]J%*"DJ^ No^Bs%3k|rmk'r%T1Ơ2 dh,s܏IB#cJ#EGs]>eDQY/-'nƥ!I2L:I}Oilɶ6+gɌ=1il>;v2qt 𥆬Ix 0x\ȐSV6X~;-j0btmօPm;ViT6?rYVIGiKa%tz@Rď ]n??8q53է?Nv{67DžFm;1ģSƧ45T ȝR wٍ,ߜՎ>mmڵrY9[5J.\2ً%IÇz VT 5)rÓ|:*azQ9:Su#ӤXum'FM{ƠepINNxT^uQtOhcB>l egD<JT<9|X%vt0&@]Ƭw~>CD %*Ԧ}Pk_lm@MLL[>111]8:ں0ff/.#ZqDFFXZRhQO9GSwbH9bV(om?c<#܉bQʙz1t8lM;m;xmƂ܉(T*My屒ﱌwn"?@U&PZQH*2Z$T*U)XZZbb" 9."5 3-P @@ h m@ AN#fFq9n߾/L%x ᄄT5 @ -x'=Zuv7 9[ ^kbff_Ғڵex<\ŋ] KaQ?eDh,B3 ERّxzWgPܥzr.=gm*Řqr/,^mظ1:xHTdžc~ #{dOi' gM&l*) mk-0BOSpaWRBQ*4oޒKs [fV Mqe{6][lI~*D05Ee*N/UEIxeǏwit3M;7һiβ`AOdUW˯ߏkwMfݮ,5N2:v>ٶ 7pq1N7o^ 't,_!apv*XQڥ9fȘ2E(5K{) U-~ A%cNml 'e˶^Сɓ(hZ Je"O<&,'4*-[.{#ӊg QDatUJ˒rP8ktMĦW:ǰ}$*ﱺ\ƭ#`$ѩxzKIθ2ڎM!shqHvUN_WE6?AO%y DCC((];2/^B$wd4{DnYh A>m|m@ -!@ D[ r :K.NRRQXZZ\w(b @ D[رWt醧gÔ~dT*oΝzG5Wˉf&}6 "#$%%ajjfPj5&MƳI3>jڜ#\GfW4R ϱ `kCVk!*ҜcY)YY97|,| gimw,d~iͱX3]¶/o\K\Se[;Gkq ˄h ߧ7^{1jx], X>սjB /^͡[סd1: r%Tamoi` *TnK ?OkYa"m!vѨm'x$45Ɂz,#6x*KƄh Ƒ#)W|!>&..W/s'*Uf)<`1ߦ+UX7FsY_;VfK[g&7Bw3>VaeWfݼY ^naYq+r/Fn?h s/Mvaѩ|Mʊã:wZbAjzUF6lm\6Y)]o^OBBJNg>  *L&FX'gmJޮ)|ώ>ij6[gv*wU ^NÍzh#HYd2OxD0$LKP̯ۘSw,ݩP=~fj~VaM̨HlZ#WJTPB}>E̦; Am3^ ۿѓnZ:ШY3<+ãAfm6Be.Sk&|ST|OJ20mq_ /YS j!ǖgb kTl)wyGl,sυs?& G)<78m= J<&&KK B]'1)Vƍ[Swn܋ٳ\]ʣDG?rŊtfX/QyAdhDUH.ߩwi6f&~ >@f|QAjpkxY ^+7fP@v$yƑa8-C )ڽMfSF]8~04eMCD=?zpJq|XCZm0`bvUNvYVN]B#y& Sҳ!>5{b*H^/!/YRƆfR6V  l_ %0͡B  Ed&tiQu+SR 9Y?&ʵ$jVw;m~yL~ڝDxD#ܹJDt,72gtN9>CLBt)Z۷_ŤJ_`Ȕ _<Xw|yɋHFѪ5]WSvU̮Xs. ɱrMFMro_f̽x5:L\@!䶴hS VO,\wnǬ;G'Hýׄ;..k׮r,P*U=MP (;O KG B@ O"F|ݗ:tUώw[69sOd-_;ӛeTsr'j9`a^2?ei۬އwi2,GMD7-ڴEv[:k0}jڷ>D"<|&c2ÿ_%YJLnVi71>*y8g̔NBcq ^ {>&7L>b@C{,yV8Ҹ dX,Z)/[Kįc>n ZK_o,!IDAT(i77rot(5*^L>>|\{&UZ0mu/ vfC RMvgHJMpV8ͯ1eeJZQȮ:;}'m\ȡ5 S8}1D_=ΩS9r4--AwkNyO%8;'ob-ξ|2h&¥ Wau;!eZ_j$[Lu0٬3Czhs)M!FV2ei2K,fvчY|qĤ|{\^ [h`f#׫ї>:Fa2(Vػ5[C:}L}pK?_e5cWz:r|23мmh5h0l͜sC湔mBZ+ >7z5:<"iWHHX^I?=UM@Q+s1~tsԚDf ѓ|}v.^ʮHTkuE× @V^fڄ-JD#6f BrE(#s6+7nCs+:P?^wckf0!]C^l<EVͳmk/KW!؂\V*rSMAϒWa@PP mڴ"ވ|޽_3OiH*(Ji>/ VO[("`T&anwsO[ի֭D̆wY_@ Ȧh̲t+2ERP6LC)inC:$ .z?Jү^so<.sFD3' zԥS(ZBAܒ/"Mu4׸ֶߕTtɞ$ɀG2e nDuE!,[RD;^b |X?W\Y!۟Op+[}ޟ C.Y&((坅!rp˛\nLx oG"D[ d Nm@/(S,)Y salT~-rr88*UߋE !D[ hw ),!y;888dUf vCu 5 tr<IENDB`python-sense-emu-1.2/docs/gui_replay.png000066400000000000000000003157441411441564000204100ustar00rootroot00000000000000PNG  IHDR bKGD pHYs+tIME#qiTXtCommentCreated with GIMPd.e IDATxwxTU?wZL 5DHbGU+XUw]]m(EERTDBI(PB @z>LI2pss=s}y{B~A>-P,`]n]vp3&NYx]3&M;|uPUJnRSSd%kkzӪ\2E9Y:\hZ:\7zeyX鐅 no,:~wvX-z6/{Y`WPTFz ;׿eRx`ut_m#msnU+2@j!Hkj_:'s uNnؽ{GH-M C\R:ϒ&GӡYsE"WS~t9LZ*fh4u3vBf":"AӢ|RvS&>pNc3>Vgg<#tʲ36VeFlz^c)fgWQn0trd0oS&u'{SsyYegWS[džG_:l*P7b(ÍtzT"قU}7XZ Vֵqo_Q7C83O-m_eo"Iy$$$' nyqT$̓3]/eQ5ƒ~2)ܘwjΜ*5Wm\mޑ#кu6mZ_w 6}Ew;̀D?5I_;,Nq_~8&k$%8mwfX-g\/n l8{N>>{Qn.\ȓ'vn?h X:o.ޟոK 23P,B[N[Ve!Ê|)S]l\J6>egVsX"~*<`˥gd"̟?>y6i]F+? ؞ۑZ+l".*V#!+FF#%3 ' _]zq3gN,d-kt ޚ`SBjjt$ϭ]ƕe>1'w!TFe-̀6nόRrskZ,ewF]o  #\Z Qߦn`R1p fu([j!8 ^;X>R=/KJ[?\>' j ֮_?uvZCvƷl٪N<|k'*j E#BRRx_jtYY'ݻ;3x LBqT8-PdɧOΦeB(%tܹ f`q7wӑ;ᆛ$U~/׶>na8~r}4u:pmb`z"_e:bMGpF _*,rү)tI=]7w?tYNs,zKV. ļ:+JOgѾ%!2 >ըNV<;Y]B5au{a//qB&2i}4/o*8tOeL݉[Gs/?vxiq'`@{UNDDh9q$M EaP릌܊Hh@M߲UkunbFr'w]P6E$&b+L#$P; ,fݢdѦҏf}3{']hun<r3HG?Ue@NM4^{-ߧO.T[PU\ 3%hlvCuꮢA$퇥5k஝ˈv5S_>"T|b3Vj-n֬\" Svnne蕐U$Hxmqש nz%p":C /Bܜq`z3G5'乻\9ջUh-6VOޗq;Me{-ײm6+ `|$U]u`$35I^ efPK+ T\|S喫WrMӅLڴl9#'>f?Ӡ.e0 MN:-DQvL ֹ91`GIϒdh"H8eSe0A^,D~ dz;ri|k,Gs/O/[:ۙq)T|#!+A$=Wͤ!JA[>_E7TܙŐ"lV{\az֙JcKb4I$)ġx,In"OY(BJ$_1S:k@L @$cKd̹>Wʇkt-Yt ZxY꫃/Ο; IvD8>^q_\Kpk<:F[eIcx]}38t7V;.h> ]q$yg眚Ijv_rXRpy4Sªq%GtMp3YPpnM8\ [4 k~4YAy[Nˤ)=pAiwГ-,tMa+֫N*`h;7質)5]vt]&Mz(+_Z? |\hm`CGtکiֺ. [ 1e@.IGtt`./_1%*Ʌ)F$GLNZ"]FmܗEb/++;D ON, 켵|=x *C^ ^ 9(;=}g*ΧcgmZ M[LW:Ɛܼj+׽{wf͚gg }37uHAG9^xj.z399zaZnQ &u=ߤVTֺU<_pbB,~r#\}:˧k>ۂykQ޼ت`,^.XG~7]r!M?w[d}P]BE|^^_M_FÆ(:r[йSLv5?H!XF2VN廴GuJnqy]^F~Zށ\XR]#fT=_a/Bvpau} W9D۸O+|~SRSSU&9sѪ{Y*6Fcrh̐`9*8hr|0Q]D7$G{T"n"ZZF`p ij[?wl>ԭœV+(#4\W "Hhkr LϤTvZ&NMMՖP)Fex-'^קi~ck-[>*?`ty<j/P*T3 *0T"VB%o(.,`?!cL7YMe9bl-o7+_I-/-8" wvW('Ȼ>Y)_|HVۚCZ|2ٕ/* |iEÿzR~ԧ^`|~ɰ]vʲd-^m/ Oζ%g_*)S]EhLԩrꚸy0&VB%bM kr9b)CӡDi> @$i[[tRԝ7`6_n*+{6VScלpsťZ$ =rPYF;qF8]-/_%"tĎ(3Eڊ麭d8zRQBTT IVQYt:q:Jb%Uu:Ni].C [V$IlVKDDDv!hԸK`-%>rڤ,|:ts:.Q~}m8| zAEls}BD&Y=XKQOmhѢ:EٜwiWgFU"V+k'<C/<5K6 ^׮]ˆ |YYY>5$I]VyvIڵk gdd?,999>M6U[}|NPo^m;w~GZKڵܓQtm]*UN3?*3F>#F\SMv:.hs8l}3u&VL'X!c /Yp}_ %SW}*,;fQ|f%,,gafZWݤ6Hx!&Q\PTV5  !NGZڮǥ?.jnrD/ўІ J|@v +j)mwE RL>EKwG0/5j! KsH`}#s&0CH%iACút[!uh+N74">g/aV\ 3|(R:kF B"'(JIvF$@H N߽3B=RHe W=䍛LO~ʺ\8*I IpWLHHtIOwA|uO/^nQ%I.u&k7Pɚnmxaͼ'3lIɵIe7EEDGTf~~Zf1J܅B .Q; JĺƐR> !FVNphl Vcw&h%ҾyъKY]/22F ϛu7cΎmw>U ?˥HJn%bGkh4觎C7#[hUCML]x(d[jB*[' AӫhYMb@DܱsgI41jKUw.I.U\Eؠ4pzܥ˭$nNbK6n(k>AwXe Cz "62QQ~tСtܵM\}p3`h[3":sk G}z%w >oNMRQ@bd0성e ^ۍV[)K..W^y/֗ =mj 鉓ycR^ڴ&C$MSl?y{LgKs+<{dm1S&ƈ" IDATڠCi$"N܉i6ms:͏O#0t8Pd`vCM[z|7-{!.$n,'TDf̘rP Le 3}t6!:,5՟_lqg׮~ចU j|alE5o".>=K"pP~2'db9eV"AyJt@ S6@RB_lDF 8ʬ$QtFϾzI|*U8ẗ6HĎcQy5s )5躵S M87`VQg}D\.'μnMU$F1t>4񲭥UܦсKYO[AfsI@%&&SѓIˠN2V#o|')IܡSlw.z ~N1A:HHB*՟+fٙ\mĽRN ekwW;S:tU< Z78÷Gu f}$'^{e^?l9M#W/Q-畴;*'^av.Oy.] eڟwEXƼ|͙duKjX$J9{Ln K!v6'w!='X9k'&V|;w'A6Ru}.!Ѱ3gJ8Vyyn|e4":f!y}']y^#Ly2qg>^{523} U=kFvBw"zQt3R-I+ (&i}+Ǐɓ?ު=ΝKq3thцUoΒ=6D/o2?֪ޙ$%%q7cX?>~-'OVff[XVȫqTMArM"_ *-`E4n$f<9K~t[}yR^w1 Vy\sG\G+sʼ;\ ۮ#S=;o<Umo禐/v̙C.];w.?3B-̴nUAQehZKU~U4@Pfbc^!WR fVrbXNqnD/ܒovo{ؾ O|> b|{LQX2c)stteEE/Ae~ceaʔ)LR1`E8^M6|sk__!ݪҒj^5"ȃ`䬿 h *:ӛl7;!m8ڄ^r.8U%G h5,jŭGqϐ 1";Ix)}'FFSd+ ?#I\噍}5:Xh5ڐ? ?q$&S94/8\ƼƵ'hC6 4Q_DއNy!OhP" :C34PQS&U_< i:Vk}TUhL֦@=g-pzD Lmf fo6mڤVQf\ Z]~Ϫny}C g*orra3ٻt~ж | " ͪJ|gH,Xzggx$>rs?Dy^ VNW[H|0w|#Vȏi?h/V 5x}}}p~Ϫd2] 7XWnGg?0&m40) %]-;on 6뫭W.,^S&fȍxt}I7GY kҮH!@n &I}h7Z>[2zkcI#ߑH CB4Z-"$r0q{Wpb,Zh;sNwpC'~*Th:OB @|P'{FT&?::ayt⢆jn\nƱS~*_1"m,:=i^ ~|mZ~ϫt3EJ&mׯʽkiTPv~>s^zcO3kve]kb*gd0.D׶m&Vжm oa6 M &e*PDLPƝmT?eH @eIUml( G{*7N\laJ=Ffz^Ûe:0;7Qz\?eqkgb `J|L WUIt#бɎ'r aoM1`(~uK'UkHK ]$-k )Uh0"NҽN*RB]P! 3  B$8w T\T4U <~lt2Mx-yG/(O {? mVZG4קE՘HmՏ9KLsnQЪ-BE=AXy߽*T4EVBE*TP X *P%jxv{9Xxm}@)ٛkYm9!C%|[ӕG^"w3O>1vϯS<'z&B '7V$a;)pңw֊/gOb$Zy9O8iE]+~+;* lv#ݜhemP Ç#ݘVlW-`n, jJ5/ yr!O=ri{܋$ &~[g=sA[qvSvzh @jN(#,'w/f+:^|Y"Ƿ)ݺEQ~wUO4+Wn)9>ʈ6]vZkA1BGُa+l<Ьa;`߈ \nT5` |Ѣ_/.||+g#L='Bmj?7_ Țf:T܉.yT@GiJV~IIƀt1}&$RC[~^=ipJH.7)Cbs.MDϬZmf ֺ:N݀ Y[Vp-?-䚾qu:]FS\m 0lp;0_!=HVD)FGe>aﲽΝ رڲ333]9\XX޹sge:t't:]VVK c)FJ * N3l3Q1L,-x)gTJ`k' cmDIEGXEf`mdF O_R[K%SspEm u#`3ǵTb.e}vZ/ZOxڵՆ;w>W[kTvnnOxƍՖv8.)){<կNEУf?#μ=/)е`)ᛯ vMgSB-+G^۳,[vQ[.QHDY,c?Gǫ~x)^V_e?2phBD1į7f*T8H"ՆPQoS y eT22pKH_ :RX7Գ*㻾V7F,/,kыEO$vל.+4 ^Ehs﹦r=ab;g:"ʤhuSTTXo,G޹s{I{MllO\.^Sߏ jQsiF=`&E.\f3Wٳ7zi: G%IF#yEt@H} b\"_&*麔1 eԑWZHb<õ%<ѷlseJh+iA5 t6a-)/J5D"\(apb4)],mT؛JZT8b<8$*:&~őJn蕲+-llݴ)0@ByL*so!+WwU+ܘ-K(G%xnyڴЩSZ[k'>C+M ގYi7,Jؒ, _ܬqϹ0jC3fX`Eo+4[HcV- +V+/**AmV?={l \1fBO|'>0ѻ+e!v*.]~2g(9Ccccm://ȊΔt*!Fд5p֠0.B*e] xϝZDҩŅ{wBB"ӳg/LWMN4jv5dС(܅RFO+eNB.tڍU?tAZKsb4V!%H0GD)#]&%Rh;{c D>{L*%Ғ\v).L98*uL"ib5^nX]p+e#Kb5tYʸTe,]<9t$s8I2krxw0{ ?`?1}T ipŕn&6І :HmlІ k ~/O=ߓ,[5&@*!}RWJٵ ctޟjgC~ 7kx{&J*!,ň.nc}cItW6eꗦpsF޳TpqKYgp6 /k"glXYheg`U4J@6lM |6t1pbWK$[ٱhQ x&R\DَEra q凈njG;:s1.IB!~ (r;XgvR,9d/~eɦg\D UU;Sfc>r:,?ߝ]}-WQ<$/Hv_Uج%\/^f Xx 5ۮ2E,RWaW3z_Es^q%jRuq&"lN+a@h3ƷR b۶HTKh`9|$K0STXw6'zn~#he vCJ{!]HͭנsJ2WheUn,喏,V6aV)-O|~EރW"JyHJ1z%`f7DVFx)!iQ4khic܅. ?,Sꈍ2[ZHHD تle>Zi}['SNQiD%&I%FBBǗpEA`)tCB41\ ƚoq\,ض<,^uc:2x"r8'Z:չBe>gΕNNVϊcdTHR־;wr){ѳgo .U;𱹱GHo9Xc_2SG]%a婏(r͂LkZ.l6Ӆ@ aĦM6lJ>7A/M殦,{,2E]4-μ"C_ 'Hu8ݠR| IDATA3c=WƀC2utWq`a|է5NߧO?t:E*\j}Ԧ?_P e4dew5Ncvuq󅍀ލbA tvI96E A{e8,[ׂNI֟(#N3PWhܹO$K vZ{&ʳ_F3dj,teTM!\}̧4t0h+ vZI5AU8rPzj:cNklkZ-qt6IIkK /ll E 66oMV?>;G5:? 9Je읚uF| W]uG;~C ;v,wy'erU)tH$1+3-JiaBm^|abAZ G}\mmܿ}3<]ӟ%\.󅅅̜9$IbڵL<X+gF lDʢhx^p$_ޖ" C k51J$ GΟj4L4_ɲwh_a갻BWB<#hZ,X@ZZ;/0Fh*iꝀN[&V_^bRXaU{nv"Fyj\%nʠXK} OĮS|Vv)_ /{]s!Z=FU_*ꉅ4IJ /{yR^w=xWosuA|;y Z:k3eaº?Qc η~;gf֬Y1py:~ nAP745*~]e6q Jڠ͡mL $I";cɞ hz=7Eo4\DI⡯&y|\5kFYho"6}R֭Cˣop-W e*4"_NA몫V}'zߐ׍62 Q-&VPk߽V07gNэ;̔똻yv~azgO畇^}`A8{d^|vVVA:=1tOFASia|I`ss2'w!r]ʞ/-Lq>q-A\K[M4~ZQoٓ}vjM+BsV#Is RК**G&ls&P'xQ41L, ~u)^5c'שCVۻ"􍲳L6.g@,eePc#^6lDׄNh o۶};q;<1gfM&@D9Z>~)ގ9aj} OiWG݊)" ND2r4ti8..'FCBk &wi:֦8uݽ[T.b4sui2.juX37S_#g#kP|DJoni$ -t@:XPDW]mmVײ+6iRDPCP~|M I{<;s{Μ93)yOH1c($|owk}k@0wHtr ӷ]'8/ JLHLs>EN#UwI՜cY+!, 1E!,Gs4&4l) ]Guָ2M׸fo#;fU&YYJaK54Э[ʼnhM2ViN.߾f4M+k$.(*趞k{}j[VًjFyy|1k_}ϥl ~ᙻ13yl"mXBa W \|!~aY{6WsNGW'%DVQ_\ʯ+V(k +܊@18\5/c7^}!h9کyTLNzpƻKؔGungZ+ʎ%߂^@Q^nm+މcN_u%v8CdHrA0gx"._`o3+VQqcƍt 9o?%Ϛ5[\䰀VHyFA{Q_x߶|i w>U>:ܙ,_ۭG<!uX-v?n7Rhd&ԇOT=E;ȦO9zk'&&YOjZͭ|{C^͟ ¹0%BC&8SeLUfo~*K/ WFO!S[nׄpt^<8]*K8§S9 c7_׼No1)R[&0K9f;똢Ppd%!=!K\N/ yQCHjMK5?ыw` iE#wgo铻|}`soΞ` iǴ.7BmozZuh\3'kjsC)J0|vMI# u7j:iߘRCj*syGIB-JZC"hlxIbZ2sx sm2|[ddT`/ u aQve+eZKQ2] cy(YY4*/  e Aa$A1 _e#!>lovzd*&9\ܴLev޵7Mclpjp !_V\v! n: KBhJo%Lث2BnBhKtV1޽S-XL o\g (ݿO5s b&_)/*2xs"\/`3K7xj{ (\^"og!cmtQ¸ Ϲ0ʘH6&W@QxR v(LSjO q~=YSecTVd$d} |}D'?M|W#M* 1̽;quif6SP_eh:֪85w80U"W Nv2:8h?O#"qbMtj JLӫ9R.PƖA  4JRR'%! gS BC^>q֓lQ  ԃVy􇌪c)\Ͷn+^]_D\^aCXZ{4oZ&??~<}Ai*9?ͤP"ˇͅ'r'Q*R?WfwOHSkeWpLtLiy BҬg?vWAWE( BC)aY$  (aAAh d+ó^?).~Bʤ (!5NtvWэvzK@ϿDl|$B)h,[=[: ID F'ZNER8co,w#DdPbnŕ'#ժz@􄅺O17X4+>8ȆhYty 3;npo-yy=t##b"uD[0wjsd n͏3dc jI֧^̅q* o/.!3y-)صh6; u@UK?kGdl5qS.o~jH֛nWMbӼ2wj\RIU;kYrxh 6N~p|[L"Z5{]#3f5snP_r;]j,Ju4ynŊysЁ]h>dvQg2G9 na%ϋB)}/K8~ݾF B,$RgOvr S.h)#2 E A\I۔;yTXz36ZuFB`-FC^Woٞ'^A~j˘~0F_^N!lbPmmaeꂊҫYP .h[''IsB+d`I-UCFc1~~{|TZU].ed43fXv뀙X;q9-hAqB6gRڼM*c;X(WEl2bh( 4AQ‚  JXAQ‚  JX:&1q &SӶ4L$&]ٿw6lXGR&233D 44_z꧚ΦLU,lG^ 6DGd2u)uw1֯䷔SkϏl^}o{3KQCC>^v'MwL\C%i Բ3uX;z) $ӉA[{ˆuLz,oflGb.H%a ecnn{.4hkp~zM VLvO/N_Ą'uc:WlHoma{]%/^ջJ+])Pdb"5Ah蚊3}/àȠP;\Б)0& DM :^x)G U;ql[gRnW[n]t;o3`I=8AvC(o0\zP l$ u VVμJYLxlcP; J$R{z!--tF1Ndpߜ<'&aĈ֛ &իW6z%q.޽ݻyMHN>@ǎ=T1+غ]ز6=EtpUu]VQdVʕ+=+ cƌ ̑#G˽c6 t]gŊc49Brr2:tSN>=nuSSSK.>W\yF7RU'N`Ϟ=iӆ=z\_z5.¸`ǎDFFҧOV~ j XvNNIIIDDDЯ_?ׯÇcXϦM eРAf{7: s6Y)4`ث񗄱L;MrPCg-{rطo/zA4En=6d+:9ػ]=(*J {RיPPMebA6n`iHƐH۵iyڧ01Bn=nz4{8}8a+{Gu >+yd眬PZ{(yFzA vѲ8cǎ2*ATۭAmӁG+. ql+y資 G RW4vZ{& m7ptue8ᇣWRXasNT^ZS)61/ NX/ y\zMB#)]q|7_ Α:+B >=z@~m(8Ezl8Z1_]eL3Mec52}hQ*@>an2sQa O-VBljꏮsIHJ s;JQ|pť+N+)!y#m! @hxvقE0ӿ֪˧-6m CЂ 4V%gnc}=u$-v&BUJ]O;ν(toЮZV)Ҷʹ1$dOv{,ig݊E 46RRӪU$aa~7m@߾bsƎ"w 6΀XzV1CjA5W]ɫ.eN"~Wr}ARHpo׊}5X-8'~Ϟ]t燎 l&++enl{Q{^} |F6lHd7^w!% 6:Z SstHe}PA/g0]jeB2}dWůĹmt7W P`~HF7`Q Ͷ~^h70U#ru2u,udeeҲefUmοUx͚v`dBƒVP:A 1`@B[Z^eF$ 0#vY aï&ܠpjgpAC=nj{Za xsl""ZbXѣCq JP622stU#hy$Zvg˼slPqwnv~N<Gs]Y5( ł B"88&AٺuKsڵ8p`ׅpfmrسg,BӴsF-;>]m2y,:-'i3.Nߺ=RgS!?B.Ⴘ^ M?yn2LHHXu LTTܤ"''yutF.ݤ0ΆVn\N_0͓x?D/w [\tU00.Yr 6yjfۡɃ9 |ÊF[7ioX IDATct8q7PzL@{oF2!fxtJ RUmebN0tMUr:de1-M|m 2YbwwbP~{k7xWY2خ)ot=ٚ۷ryѣ)wm x3 g2,ThzJzsϵn;dgf{%N+'JSHU>5,րL}WE l6"T)/0Xv@hn4:hXG/r ӟ<Ȱ1ci{ooEzg^_ ECBB KV[Ĕ%XGS_&M6MQ‘Fs zl'o@||W:Hll֡۷ogRP¹%%rI6!fCflL;`D'zvs~(5G)mMXc7dPFhZGx2Y-)B ™vLc|h ")x@E( j*R |<ٿ[ho!Bp@#^ ^@{ųb) t\HchZג|wu'm* Sϡ@Ogcرx !K4ښ˯Dhe~/tf𸮴ۃnJj3M-zwZxB>[ 6vogcz3r\$%%qscς{Op禲BMDRex =ߩsra^LF(ڨGPԞ-Vq4T411qMmf>-g+ahQzkTA ?8A`:N 4K̚?ڞ)=@w,ZfcwӫyUرS{u 9t;]M~ _2z2!ԙ3Qeԩߦ<& L0x{`s/gscMnz{ }qڵ$ďR< ],uXG u_t2gy +m| $]vԽ{OM.֭NcU‹r3ѭڹ7A.K!D+f-Vƻ\z)-JM`ah60v|-ul q`"`MKc=Iˢct6K=ϡY\k@ZlYf jI-Roxsڢe1T-t)4Je&ǢpT=嵈I=%Xk9g,Վ0([ 6ϵ3:@)aUBQW0]Nt]Ǡ(YlhFˉ( Թ2.LWOy6JdJ[KS=e _#X8[pH=4~ &7jl,qw&UCٻ\vNR3R=q ogO|^h1eXl\{(JZ~Q߱'-B iAth~Bg {GM_f3=%f/BSi=Gh]XG=۲ﶬ8xJPVZqJN^evDzA@u?lD/x GCt8/XQ9+8XXÿGs@ӎpa JM s.>[dtAFf0=> g$%^0Z$Ϻ$= c_wD{FRHVu c~ٮbNrK̉Iv̯XutXXK){Rlx%[Rs#~ t!R^aF(_߫jXoԳH(-c~F%opኊ֢c_ hږ5n[XX}E мp̏߇) h d%S?r*cESѱL|BEݧVRjmT|dd*F~4K}\RFB]L!p@Iʰ-. _c\c0A YBUͫ`le{NYG2>DM1n:"2ؿ 9K|iytٕG[jܶ)k*@_GlFW<ǂ^<^ZbcǙI 駇{eSlzô'8\:!胯,JmzeGk=>+׌Y2p*nh2VT|8HƳdVFnϻ70COڅl!-#㩋o E$wx̳Ȃ:l"Z@XoXʳ7y׮3sl0Ѿevc9LZI,ȭ@ ؘ;wUdd6c@}~娺|`j%*wAMRlS /фB6ڠ)!t,vB#Ĺm)^vba&;&<bMv`aw(t3$;6NRBB%fvYy`;h0𸩼lf L`S~4Bٴ{F]<+lf \0ח?ypkxvWGET-CiCp*7O-<3 &ų{UNQE5+͚׮9;`I 8#F{^B\2m/_%X&GQ̔,RtO GY/ԭ{úC{w3o?x㔸'lr fm]ҙQsyd΄{F]khNJI%H޿ fDQ u|$** ,;vlG^RY& \ŘQ‚ٟq<]C^2*D|2LϞu]ШPNj gI#^xw,G@kgڒLbU|>ѝxiҊrهDiDΦ4hyyUf;@*dpѓR` _* #M:e8476X۝wFl&H{(F9b$J3jiŤꬎ6dSQ8ɉ9FFF:G_FR]HJJ'v-=%{$xqq$TLQXIN3P0jeYV3U_10zN_-\~deh>\f#g"-ڻͯo9ů_(\a2,nA~>c`hr^ wn*^i$&aĈѸMg2Xz%Co<ʺ!..CѮ]LTſ'|oޭf{NUdy5{8V7xlR﮴t‹u+0١&+n[@- dG(,@6m"Cq$ֺ4tBߵtNIy<Ϊo6kO&C"~J:צdF?}{sTs&E ͝ E T7"! 4r%;\h?|;v+%8[(JJuQl({^whLf3NM??4k حS0&4;tutkkmK7 V"خgs^rוN Sb/ Surc mبgPa?ٙVRpz"?kGd-I jB{,:: Z9ma%^j ú$=e> Ļ=O|g YZ착,9/v>ӝ[5ca|}{[󨪪O5)%|LΏ PQV@(tB/Ta`w=hd=+Ɏ]cXCmrOJwt&[cP ")+.g}:VL2ҢlCOrya$Z7mf'G+o+cǯTɼ'08gLkOÉ<;> eX +۲z~v8rutvB3~RH, fSXLDZ"袄pD/}j=Ov"K]F7K/A%`\D)X/^]vqL-@ &6k 4I@ZnD:ZuRJMֆе6h 6xZ~:*R۴cMlTtݳ[יҶJNnOx"8?ncqVy፤9 +YプnoWP0R7s8xp7A*KfM%g_azq^ȑ{S*Nv:%iEu׋.i8&u6^BP[d%d#u7rQP'D=W"7)q!{AZ8XT3339qC6(,rPX (JQcl\=N:&xɊYkZsU*{ª\p ~ 6*s}V3tʀXySi_νYĿ.=|6&ϻ7XeўMuvO^C#`n_#}&&&`+n3ydz@Q `dPw:J ft e{-DcϞ)նRa&ݒ^sQ‡*-X/O{}.Ơ(ąGX7ܒBLO/$Uvɧ>i#0 Bv6 G*8_Ǜ+2gdy Y#ш$;l~8+|~{5Z9)Fͦe<8=-6~%ڵhgpׯ[9rJQ̜9_~Ө3;vw^UU/_΁x׹?~7n0̜s++Zu:oĪ2 MB$!a(sΤ^ӗf^WoHX,[q>+p![l!dMb~kzN@)uԡ{==+2}7D5FǟǬչ!*4Q1=vm`LLw<p..n`@XMf4BڸM+rӐ<+6^oܸnz4V wvYRc Yެ{R؟m&=*5w1so #`Ty$뗰ٹw(B۶m+n||suLDm>a;+~g v7|„j*f]%ł֯ z[T٬vK|h'ݝilƝDf{9-\qf>~1]ީڴ߿|\]}%,+i/d"T RvXFv=!9E-V&3#^0[xrTp9jڬ.Nx #BS[V#kxX4]ﻗTjm(v7:."ӆ uS_/r FO[n;ޏWiX_ʚOVLqŴ2~1$K < OShttlKux/XYo ʛ+WR7ܼrо%dRJ0KfxouFKw㼶.̹-î0F8-ZC(11j6d%P<)3ݭ*٪F>yNߵvPRKLk5vni::ϽՂu|7_=, JRRk=GywwUiUcz7c`ԭHXiE[]p-qSa.QQp~'L`5dR^EA+82K_"фS6wpؕXQ(fV00F'!2e(O_twbFtDL&V^Сu>υnȲӧo_<}5J#88y*`RpW֐l]^k.Tqs( +ZTiuwܘ>9=7+K:1 {Ǩ3Uwޚ'\ޡ-a|[W% ?.#]ȏ;nFn-+9λGDPzЃ-ɮGxᲛy>\7_nMF3K_2-uCӥ/+oCB IDATWEyl*>QVZ}qH:CFrʳb#rjZ-邛'pky<'׳>,af)j]zNy4i2{>ngC"t9WWƳ {t,;Zj|iݣbեn^ybunU2q˃0Å $ܶOӍ]%w>FO г~Pd] _/x#{CAL ?K?_|q*2)dj㹽 bu7 s|R ׼uT].WpȨhZa 8>DEEdϟ (^Y4̼qN?ׄgX hњh"CMVkFK=yŧE8p.2Ѣ0P2btGo1^Ve|,һ5i҅TCEy早D^UPS8xwv}~[=3TK':@g_6/b;o"?oɤhqLQiw'd5ԃJ86T`㛷?nƿ䕍.8^vѻŨiO,Jا΀28|04q:5J#d*ɜ㔥q/jpKg/'%|zCٻ+.*Vos)ti$-]G]0ãl`lmc{CZ#|6pVl<37 ʬ_XۑPQ<emy8qJ@wgcj!%9?zf 畳R2wGs[$fuqob7x/F}=^Y6sͫ<c! r1Cph;ch W);@YƟ,oRJaJJlzI#ƶ4 %o9٨Rj4g&zO#QlPd  4 AfAJaRS5X(aAYЮ],6<Aa軆BG)3//f?vşϥU4Y X pQdYT72n7w2`'?5uTaOx[Bht7迀roJ{>Ghsͫlyvk_ُUzW?5h}Cξ!ԙ* 8`㿎ӹsZ*3P|hO=x{yx/|" (lg<eCJxyHzrqDI4b}Mϱq|[p;G 2uFh4Rr7zdAsjUzk^|h*w1g’()SCWlz97C\}]?u _=~/Q: ~ghe> Y#-D r!-ݦ\P ̈́ÇFMTM}XG ͂:5<6oެȣAˑGP={4êK B@BQ   gfXuQ‚ @AB( B ̿Ƙ>ԥQEA6o*|*3O0Ξ PEϹ3%aZ<KkuĈDD B`00$ {;Meٙ. /׹"@(}m_US;^U[wR(@h|3w5j×)Ii\ڄŤ77* VCwv&U"\?]DqWE B-mVF^Nf0Fl'NΣu*3P+2uwrKBn. \Ѧuti:G]3uOK 2YWqu>=0F'U?\IcqM S%,@0ii*u1Ӯ.J,a*F@+8޽9[gsmUM+ӚF 1V?_"eԲ_~8Z zۗ 5BL NjW&1`ˆo6V #B \Quh3}P=,uv?+v S`[1+HjaƸS> t/_ib^ce~ iTk3h/,Luc  4AQ‚  JXAL $%%Ez‚ PAQ‚  JUrKMvAzP|4M0A2*QMSiLҀϿ1Йo g?2dT>L*cμ%N֍h:gfnUreƽ0 -u$$ٔMv$n 6e$lB `L3Ƹ-[Bn*tn9?Օ%KWdKy=;s=gΜ99gy aFfr hVoz꬞gmYOٖE4Zߓ1_]p*]ϖVX$@At"=i_ʤ/4e7 @4);;_Qu7i<ln4]DZz?^^Neˮx&G{ (xn[S`š혦W^WBд(ݲ 'wp7߈n1睝qxᓂ^pe gBοU#=YP.^nzu<&ӫZڡ2 [/|XzJxFՇŖH] z=eMTl fsmzi*dIo8770M6-Ej 6?Ag?3/3_%nu3>7<}oD\8+8ދc#"3w ;VfJQs8m4|!&c &c؍UnYam[Yzىvϧ1muJO'z?^V1 "Qk$wi USz|~S\;{YWX&[+vsSui{ʴ쮶yɗt{]֔ޗrfWrD/D:A}kq+F%l?ߝ˚h<}OY~ kяf 7k.zּ{2)AL&34N1#l8eLHicO~B畞^TgլJ,BӴο$ 2{[p;X[EycONyٖh 4o_+/VfXV_%\򯻲7|VHe>0+V*1)U,]2BdAsVvIFAi o     ", "‚     "‚0ػwgHEZDa`b=٭+/W^ja5/N fǓ&^|< @_fAxqS w/ rY2P[[üy -j6gHOyL-ɿ'ox곖򅛦v:w+a}(\5wG0r Bq2ZXvqkMոfߌz/u2QR'[ܱj־̛W޽ C'}Ns̏;k>~͕LWo=ȍsGGOwS'+Tʸ;ȡq7ߖ3l=ZR*A&2{ln_p;˧-G4p\ɛkd-7;a2q$XjaD Ϝ^đ?u |8N.]iccGk:MAk\հJk4#u ^hp$h:.ſXm1%@b R&!+'P+o6vdg椷02R\"DǑtey1{x'$$ $#\]]MffVydI6E8˻Mc;a-Gg8<6o+Eŭ/ՏI)̓RrEc\v|bg"ѳs?)x !(Mf2incf_P54c^g孷HFy455r0%]po +! cL@Ww-cڍ̻Ԝ&1ks(L$פ9]-˛~la3~|.GϟS=ZSԵt)/l~1+Z:M,6P}}6vٞ={0|bر#}5txw@rr2͋9~!Xpaʹ;~?zrxG3gq.**Yf'ۚqW?vUUUL6m+m6W]uYo'9}49NeY]RRBYY'N$'''twd`9ziBolu'd@pOfvlr0ș4i2Gϟ9rCne,~/0+3]d!o!:Lӥ͕&|߮yG$aHq>G YR5~I{Xm۠\"҅lo3ʊ;369Q2Q'OaҤ!;__3}5- 8вJ22ĺn |ٰi#V`S%Po';N]:iY% FJKK;+-|.NHHb)|k.8 "z"gtiú~2"bbRxv㳒 ;AM;Isķi!Aevg0657߿fp^#')…a)2١`'u`=_iY; ߶m[tNW#7l>ѻvvttw^/۷ownGGwgݝ@ ֭a].((vttw-bѮFGu;::4M6mtθ]cw~1&4FS]S.{^Apvjrf|A$%vyxaPG7:,`(vE [VwyWNii߉ p4:zѣĞK4M6v>IӸ0~~)LxikUFG M18>rX4>ivqG3Xk;ylπYFG_8dt0m%@aM5Otk\ $:>SU//ݞ{ ¾ `9*PagI0Be6‚ 6٩4 7qݔS?{0t\^b}b+[ht%A5e;bG]u x/!nH  iOٰӂ %1vs|dF1l>t9ew<atBAfZ5]IN&ˈhA R 9 \Ljlv'IF04E3nƙWI @  tEmm ee%381o.)8hE8OŮ&cK2mcskhw@mtCʠ!e]e!AAR1wxfI%n3o"6_cxEA)_I*Mxʽ "B[:/19dp^_+@7ȝu5 `zQUžz7s3`1oނNSSSILLrpN<#bYN*.o)aYIϼ[CO㒥;Z5 CQ1Z74|p/&fWjKg`mm IDATM3CZ5 YY_s!p|=&+8O=* QjAc"^&gٝkln ^w^ˉ6pŪٽe~O\E 4kzLtWL8۷[p83gye|0grf8.bX&JQ  CSOf0a\;J aƕ14Λ/\˦dQAGLSII<e>`saN'K.two=!v-][!wU=ɖX}tegw#[::A$EZshCVnWN`744;HkbV(#m(Ym{pDob=Z{]͠s= 5\Rq{}B2ne!ѡS#BU踂^m=!!qPTO7suu5Y%${ZhB9u;nI#+W"?ś-Fzvl(J>{\vrt AQR})M0|#Mge+rq> ˓j[Ry /jL&L.Nd|t|wos^חGSS#)7KKVrkuY>-;rfY -ihaiiiaƍ %Lǩ)Æ olPWW˘1c/|ȘuK}m߃LwL-hnF[9tnNeeI]x>|ٳ^w`ŸUG Zv%^yyhN0ш ĮdGKsMMIII%!!z%.qƏϥ(Sb&O*ԛޞ`w8p8"p9QWW;g$.&MLa,+<ȑL8I#+lk2nJB3mfmQ͌O}*@~ |?[TJq5|; Bw.@OM'z4::WgѺ8oӥ%}XZ"l=;g&$$0yʔwE 2H̐ۧ꘭&&jaQo~}"Ed--#"*a1N {ڤ*YT5,Ylr#l\2Z\{1 qHkrnGMUX wns=lAH: bKp6Oq=<9maa$=Fኣ b^H 4,kmWm/Q_Sʂѓ{]&F_ i63zdF`˨6Rk'{3&z**ΐ;EMMeee{N0* anoQU?mnoS5~_U>+#ӭ|R5sTy><mK{n]> .׷Yp|n_uc y9u9WAENZZ:۷oedffbĈ݆ʫ"}{V ̊ÿsbZhzB 57[z hFoV;L%IsUsJa}a/3]%z)=WojXml{KB# P|n*tl Zb?ј>2QgNbOMQ, χ\Bhh<,5Oxf0j:#68䧏$%!EaݻSoka趄Om˲p/ pff&~y!B&l* o(@HrSAtEX@CŻ0C^[TŻ(*A,`&.e2 Mes`ӥH -"'n tsح_B)fi "܉b\#-9i53^f=ZqKhJߚ+Zgr{F/!,8j43&S Bav3y\5q;eVOuvs*XCh:fvnsHjk*D9M%@*hv57|B51mVt1qG0Ph(zM-nx_-ጌ ]>|f#}eksAZ81IUU%̚5W2g0p߮pae"݇J12pheIxR 7#bJ 5!#FfO8k"4/ʮGҺ)]c髰̈N=@݅m^B:Ʉze6_e@JmPʜȈ/l!5CYדNnG)x>yRRrc3lvHFɁI-Ƹl?=އ.ut/#VO罪筓]=ު)ʿsG=OձOuynj:/F>lK{nQ}OKG6:?o- ℧ CAN6RkXhyW6g,9]+29(r5-3ggAVo/gʻY4.9T c _g^1tWs:wʱbyEJdc8rpGS|ӞV'0$7" 6!~yTNzztW9Eൂ\הu֒z;98훰\5X;e0?|Csocy{#  z8B02sR\!Sv?dԨ1C9*G05~{m0aHz.VEn' }J;V3bHqJ^c<U3Zt [ YIwaհPo1KT<u|cT^k|GgUp>Vs"_J@u|,bQ;b(1k)h֚elYzxV9(x=O:TkfE)P.5}]ow B8"{ vF D݁(dB6;PP~Ag!xrT8p ^HTTd"n-+EluHݙ%\k/znePĩ+y!@5zOs7jI_VLхSF vH"+bA*z<=E;.ZW=#3GmL\X.mllf,J)yG/%{7lz=҆j솁DA E8"Jg?;>EsUŴqUvOiV)yٮGelGӭ੤<мO&Ehv9ߙK+ιm'(ߋ ;"%vzmK#o~&z kkjr-]r#_ö|,㶽),egjC| ͯN//-Ze~LwQ VW.GBfRw\SX?]U^>cӇeJ)8[SWb?㩓^˷%O4"d4tk`9i󭪸#D~~/՞Jן:g?ZcW~ < d  HKXY^Gpj桹Q  .N݆h~O;˲kRMی ",\,妑}xuLu 0o‚  -aR`v7rSvر%;.WQb GFJV4}krnGMUX /^Wd' *@y Yphnc;TMtfi'-Hl0аo^i1g#onz=O_78JrV2oreYf][6YUQVkY5v U=~4r\AfޢBoxN:oTvs˱cCV}4}M*=UuvDatD8LDAJ(zW;:n;nbt̗UF ԑ֞/W;8< \"h:Da[ Q"cNc (p;[)w1QufDGӴ;lۂ!X84ֽ@AݕqtB&ebc}hQi/ @YTʆԸ쾖cĤ1Zkj2t2L;udUk1=.{S46+}kSF\MJm ^j-6Vvٛp>NA?yY9u{ADxhK@o(gCyT]g\3i pI !A˘l~9)"Bt-//=."̟p@l|>+&'g fZ>7F1VI ?*g>]vYtG Cx:A@fܑpkFUUYY݋R9a+o`dx e'"C46z{ k޵,]P9&7BdKGCv!tDz5yd)E_A 7)ӏ*t -F=D z'lEi[6=sNЮy֖fhI&:\~JB$;|G7wk|GWw`%cTJ&G+Kiv:~ W8g%=8o/O _" ޥ: GbUjnQnmjܥjb"x^*GҧŞq{7a,?zۺqn҂hLԓ 9z Ө:km`ڈq׸cyL99v>#]Vc#] =θlv;b|GKJ:ozT_dOxs/.S:MCLˢΣe |l5k2<}q(mfRJ7)Zx[&Zx/>z2e*K\ҥ6l8n?aЊp<yĹ޼ϓpY}3O] ]F_9Sؗln/v_o_^^֭{< IAYKXkzj6]k IDAT {3:kt5[ RRa,t:[mHz Ҍa=ڮ]- gZVPam[wQí:ag]7p"חpNe;9&|;q/vv̚53cƬNB u$&&I$ 2偤 ?qfv ΰ3DB{ktn®,+Nw8nw8!{ć4jf Gct%];HxnaE;; E+mE9mu3mze:q.[8O6qHpt9$:\.ZR >Z ߡ}L_<1ԩؽ{'Ikk 1cJ \KX%l)o< HCKdM–:ǰ;zl{<6GpxݕgQzAT0bB`7| _ ^/!˼awn-Rjza;G_ #f9M3]=Oخr7 ޔ47k\,lyI&[-+}YmPGfg:4wr? {7%ǰ!:g ׆qzM4`5 O}?*xIȻOLJc`8=FS'q:yZᢈV/qhڀD'2[$3=+5![\m> v$aH a\x*> )^ԠNy|>)4 c܄ >TTAHHaq֤ u;協\AD ]Z/vp܄,rj#G~pp %L}}=nw="½ul6^,p90g<̙'1@R  ", ү,0(*: Ķرbrr^6]2"l76 Z >5 axe1"u7DWvqŌ:pCv:e= wjO ;dOMzUA*ȢA UBaa)Eؤd#>O>FYa x6J1JP鎕?Uj4,Sq ψzmV[5aX8PPAOJD+SjB=e*;>[ JOd:Rn0 i<_`- -<4m9}R B[]k$K^--:MKOnwEπȢ V##ԩV:+CyFKX`j&$M Lc +QZ8j;Y`ˎp." VX0`4}Ǭ&rmΨFGlWDlR ]VaàA拦ld-SϘ0&::Z䅤D :R2X9o%uE(8a1\9Wg[Yr%sʕzԼH^ N|jvثj;:Ճ-*)~nW;sUy.Rb/APJI|"}cVw|є5jhto 5q~;4,nKx-ۛ(GӚ!ow4jnkF7{6#3Oj,AD2Z߮ime7m$ՌoXF,Cn-88Tn{7?ZǺ-  ","|E8T\AqtUhvަeQrd8e+A)K⻜[³rbG,MV= I3?Aa`D1◾+m}{}dnh_ǿ|sxߐk %\T1Kܭ-<|܅ף?;B P|g?O<lycA /I. )1*S߿'OEy< &iG:'n8P4]B-aMAν3;u$>Nä;Y0 [EyI2JS>M;_Emkuo4ԣ.W7ȿo=dPWCNypG+idF\KP®.hd2᷶g#LxD®"X#@obXr97T5[ʽ9ZQ'x#ag28TkRS{:uHvZia?ѯ&mARX~ D@̍Xcr)o דj>!>W&  T .ҙ6 rv]JI>!|3H? t쓴! un ㍯ 6>_W"&dɲelٸa4(Ӽx8՗E#0#.=^شFl4 DϭO0T9nP)+;`"ll:hE3۵+4i*/- B"<:=Ӽx0w]7Zzi:_ɜ/kw1T.g~Qj_A@(]jNSгS+U#h!,~*D ǃ¾ZQ!n[1#4QY^C zȶ$)Ղ] lHYش'U3HB65+ˆ ~Uע$jְWpk~oT~]5"ZT {'UsF_Hܑ[jC#4Qә6^8R(q4/g祖"]O79"7XC-f%50[²e%wŲ}jou֫esk0Q1;n{}V:v|]H3΍ʠ 9t]h>k$G[*4|]&j:A_ͼ 4G E$IgWa'w|Yrz#'`*O<#|__qDm%Y) #+N{Zyk* An0LA{˭n`XvZxL$AǙᶘǩ=8_iI}C5H"4rO-Vǿ7/N(Ȼ_1k:>>zRG2?m4/|/\ײvBLjj*999ޥG{ލ~D.~M7dbLXVKX/#VeY)-;Ɣc(maxp*~u]RƐJ`gIˏ{:ީ.ff4M_󯥴+xW)..ɓ\.yyyL4UVxe ¥)})уsB!;ݻ1CRY^ G `}`ćUQ;x{:j^~^N=֍i=gY+OAy{>}1{5䱾h'*Y[eӹ}bSky;9eBJd ǽO j{772!sD"=gۉ#\?y?ޠ̸ MVn_ʀ]0#KzDϛ7R ,cǎaf&ĿwZ3,?SUePg$J,>|`@l |#\V z4Gu+_.ŏzFr̮u"7ʟ-|Pȳc~$L=eBΝ3҇oƒ2)L[S;QTUƻ_|67|/躿k͒Ǿ: 8qt (/o_3==Vpaa!EEE3vBdnjMoi"o*2;I̙39sI|= ߍn'Y&67b"kac+ 5٪oaddf|棸n_@swWD oW, ϳYW}?O/ʦZ~jD Z۸?`7&#!=}+?Ƨ~>y;*}wn!@U: {[VV׵uײ*]6TT@C$!z%y39gޙ3grJ [,OMI%%%^tRRRq^ws*2҈Wrks6/B3ζU ? B..(/bŏ{ \]}W[f+{ ۹ٷ?̓e#G^Qɴ2(AAAt:z@L&ti]ΚJ;[՟ۯw }^m;18ɛ|Nz#[i۶nnsq++7 AKIbA-mƾ})t-pC::l^7GxVޚ>;r/1kR>199i')38p(M"[0Q:jGUqW!8ܱ; rq*wǼN񿝿Bif35֭[=Gz=]vK2Q2m\;*\YZuuރ~f!i<*2p'/0)~C1$b}Z2}y}|f!hc䮾U});Ndnua1UTR._λo7-U x;*)j0syYT:޺E7qohbK.fl2χz=cƌa„ AO߰(f${gY}T7B;2CKjt [FGy'~qs|t# .a1=y|IV,Euhacz xX{gpY0n?j >۟a?D'?Kf۞dr01~{d]\we-.T)Tt\s5L2^z`ϏD keА:=y~ϷGֆW͸SUE;xEoYiGY4bI-!89~ٝ2UJU.w;_vQ~xXrK|7vŷ0Ql+or}EQ8bøx?Ě۰vڬDUy}84$C<;z4~DӡP(J'#σ5>H+ߗYy 7JyK)(,дZ=>2s15?FFjJkeǼ9ܶ60rL֪}>'w+M tZ2G+TcX~fPLz#6ǧ;MDԽc(f= w/@kwZT`qYn[ Na=ʕ2xгXvÆu|lө]Xoz.#]Ƽ|osAyq= ,5H ]Odzi}tX8( Kr]1|wЮ324ʪz$QzdY2κx+#_aThNWdD.xjo_0( {LUA5;2x, GU)w'9Xc40K}[^UC\XCLmр+,eW(BGil,9m>P5~tŇecujΨ0l|s\Y}),b0RVb[9߿C?r{%` 㪾K2k2*˱5ϏY[3s\?fn涯^㎡Ö4йTv:Ɋgu4n&%5lF9y{qX|(5ڙd" O Lezl?>##9Pͬ[rb5ݶSeL1=)UdrӠ4h% 7 ?]ۣ1}3_|q>m r('kLAV?56Vm_Y(, V,۾exR3Qߌ?j5wW:f57nuY;4:OMD'4v..&MT'hDC=aWUiECi :=vװ>{2SkSQTa^FeJ *J+:|f5O --@~6]m`zjZ /V:f%ߝ3U"`h@ UQI7q.F| ?<rM03k۔;% c4F!hf՟5>zdgv*C<: ܚJRwQK4 IDATh4>[7 ^sQ@gvU|~ٹKzbr! e,lɨzEGwLI=׮촚N}G`]`XG"V dsykkI6' V,u`MӰ_Ϗ}Mx` BԢW%ڽ^\Wwtx7:￘; AQtR".տ1ktlljhrJ ?q[$ Cs)&ŷm(-y}(,oٓ|NnUf52[LWq&*_H wdm|$ҕ3kgK+Ny, Zћtoxox̂L~Xv@΁6̢( AAԇ.gt-[Ƨ]H;t?O_rO!Z9MFjBK2FX!ij%FX!i!Bq8Np'BѠBv"۶Z}+B!<ׯ?h!2B!B!B!B!B!4BׯGgmz~Zk!N@B!B2 ֱgO jl6KѡCة(R-af! u;Tʐ7.wz*!Z7)CBVoG)!!hf#廙׍>t-^}礃 jUڢ`> Btj=17F5 d|Dqyܚ7O(d~TU`X]j]o\|TU%`2e|K^[Ϙz3qƷϚ}DMЎmOgu$Bl'EFI {6Žp.DUU?JjK't h)k+XہV)i_}ҚiۗϾ&;.>q m|,k|>Sn<<\l['ӏ_EV>,Z}w })46w^nV&ʻ|Xow^wyXS␡;indPz74͋E ';i|hT۷n]/k߁]Ie O!zέ~zW@7삉=Umi3m owΧy-ꉽss_ޗ\;ydtjW4`%a0j5]>hɤ:!\y-ʷkszpD46SB^ҟ:"t{"nT7c;QꗓwųO2dFªf?=篯gt9z1ֻUaGf1 oo~/f3A\|: ?ǡX24.sRo5{*5ʘ8jVOY_fPC@^XGOf1,Zϒi7"'-ܝŸS`OOV37qսsmQ*%m,>Jr9WoF+n۫#&.xi0޻< 3wQ|mv# p8;*Pw%YiR=0\-1ys+Ce0If:棍J۠w';)wnޮf~o~퍲%F̑f-TTjীfj`?2˼JcC:]Md(Dg}MWw*5 TwN ƎEs:ߒEApOڞ)((jUo:.L/߼ L|7> R>lOn T ~i&WZ3%naD7;FJ^Oe~y Ӹh8?JaʐlY#'\ur e_~Յlu.']CᮅbL|V,#YJMf;]+f DI´;\btM<|0vPEN.@1 #<3p9K }N`yhZxpl  Ruap`3. XκDxa%e=<`RFơCiDEE>*gR+v*ʩ!!bN1OGiiZkuJ!Z7)CBfʺa]oK^HX!& !4B!i"C !hTUe˖8ӲH߾4B!Z-[6ҿ6Ncu$&FX!ĹtrNUUK#,p&~[@a!3+|zGy/c&\؉ܗUÜA| V}oG!D.WʜEU* pW؎lO,\JW[qA6S]{.ΐ!8s|=}l. ޏ)pߊn8v4~;ZQ#^FMo`Mw16&17Ϟ %Ƌ{ogivxn x׶do W0o='>ߊzKLuw?i3=yx$sKOɃ{3~C-!^?؏FvHM&N{6~/]#!8ݎiq*`ll]&@ex9|=p˭[,~.xZ^ߓF.Bu>F=vW?^z>L:.?<8}`J%rq׈ OȱniŞ4ľ}ʔBRz}歩g¡?>|GN{voy7P\X3bn .1m`O6hhގ>wߣ{Hv$WQtjc2,:!aGf[FTPV+vܛ&,@SWmbh6: lut@SUpXtNEӴmWz_װYt 60U=/g;%6{fMf[fSz_|27~Z=nާv;oUoTdnネU†LjMه?4BpttǓ̀(Jy|ZZIBhllsfw%BVOD~;*++OV.zvBq+JB!8ytii B!)֡CEq\.BSC3an,B!BV볫 T-AB!BHL~\`!B! C!B!B!XB B7T*AoT1ܵn<ީz*()9Hcg<0¿jjh &Z%pҷ5RB B!ZNǘ .z[#e JEupإ o0y u]vl n GP,:.fh:7߯rh>N Qf1v.tQܒy#%]02]eF$kte>z:A',wQS-}Q v&"6W:F2re&|789KCCaVXwnU*nbܚBE?69oF.m*ON#!8|}clB;i?-O$Bq*_`K'Vْf^7kT6^Ψ+d.GW;3EvK k<ͼ6\ƥq#Ylܶh`3@r&[S{{>Irs_ǡv]utV4WAVT]袼5p`'w]n+6G!: MdzoTN(@X!)/SI&i&Cݸ" x7vi} FeYp*r_V\NС}v+*yC xaцXh䱞*A]=mz)_a=|Io A:bylNciT~Ú soaP)H}bm#|O7f>{OzTVir:B!w9͇d#pʻϞ!B!8kV|lp60Mpͽob"R^cUɩZ%p}gZ)GkRy{q曚ǮGÜy؜v m7bd|p!B!§*K'Kॷc\L &SϷs*Nd oߞq#10Fs0>ʙ#5+t!Ab4"4>bC B!e8;4]X8DŽߦa|)Jˉd¬=?Red0)^!Bq;O =nދoX{IIv~yM~LUOI =f>3+p8JH[-/|[#}u!̆<.L759`\*Nk}ocI B!ܥlٲEʛ'75F/!B!SHKM !B!8$ Z!]S_zKA!B!-Z1~݄ypWʷl&99}0$֗X3B:ᦪsook%vl&d6{ŅbjZ ƹu;uMHb`)6g\J:Q@6,?ӫ0l%| Ww!}UT:~ƥcb04PWi<=֠([%HhV5[6md"*U#3?A^Jvec:fI_>Ԙ)G/5Yװfw&%n3 jW!C5brB GS"mv]}jMg|6-ڂee\ ߸hzsBwͭ[ޚsW L@۹Thm ê4P@*>zy`(Ǟ8KĺpcoClwD_}]xH ,ęB߅?.zG\>cg :c24#C;Rm?wH+Dŋ{>wo~6E3r@z^K>PǓ^_cʝ̿pc޶s\^ϢErכŃ1/(Z;!V/dj.s7gQ8. )oȱI~_΋aL>cKw`FNO)G2t9E\4;~n/Uc@+&eS6:9鸄QLĎuƔ1UAVet\XC#if=6ב4 hHslI\=md' ڕ6Í7qFd繺yű7X6Љ\pX%<=`sE$Vueg-I8B4㻧r9ܤo}p(ߟq'<,ej|h_1)[Q/-q0ِys4]HtB9?NL4Gg<i|p_?{6-z4j_%(\ЅɿAw\ṀuwHlﺰ5 ~mb\U$:+7no|vB+!0Jw`qv6oyq~˳ubHH#7XbP "at~} y ӲXF cJ5w0h:ӍL.ysL}y}UbsХps$tP ՇݺӼ4 8Ԝy s-Վ*Mm6u4k;5is#,kÕ>y|޶=05:NJ'{K'F7.?p=a}jNk;ch4b4%8k~=Щ{<;?e3JvFsz\fDP_|_czίn2b-Rǝtclcm{s]Ķ5sqeI<ԹޔfǦ/m"4Z;?v w$,ks ]ĎH-cz3xQ@ {@И\|-LSjIxsكSo coj#'bܲVeQZГA]<]s~Ss=%sٗ]]3ց&00L45_мkV#)#H۲dS6.Umc/ Xb]1f(B]e$]LRaJ` C'\_sǜzQ5ߕr&6tNK$MJEklٲEʯ-,kNygQi,FI(!ΦX&OB!w̝𞌼h8m%B<B!BqNOB!BsB!B!$B!B!$B!B!$B!B!$B!B!$B!B!!BJEߟ@LLW%SBRDX!9e TTTPQQ%CN] 8S2DʵBrDX!9pP]2DHB2j!,, L&3v"cۈDppdB!BqIK;HYY񽈈hqՊj%2-.]vGtt'ēLF !BSǙe[z |"y-Z Mزe$$`9` !/aal۶MNAUr72r7cqZb^~~oH*5^@z^?m$Ȏ[uPd~RYvR/\|;dByG*B!8uw +]s\W0ٷ7fY`Gыlǁe|*wMsn]Ni9f^8-RR ]A[SL<(az=/H} ΈOxW hCNaĥjaJB"6TI002Af:m5,,95o+jN63B!҇o(v Xz zJ5qLxd%iFucԏIUڎ Zi2zGʎ Rm9$al޼kXŅtw  5OKJINN_'>~~bC˨\;ڿIFY^=EDQKϕdvȔOdu7rkΏ.#'q.'~-gddXFr*hzhEg0N9I eGuV9+dk51>="4Q1~{Ćv(%HL\9W/dyf6'n @O#гosn"TKXJwg-2&+ɻe2pepرX'Îj\W˛LftmكӅ29rJy&Lꉿ0j-͠IVZGJt3s cˢ9t25<ԡןdm( ϯť3bj!!:"BV* $۫*NǠ:g\tCK& !N1#_s"Z63z̰9mv/cGr9A}易ЍKظW^y[d``` mg}~ڏI+Ew "jdT7nT2Vfw)1)#L{Jg@awgJ \XV?o[E` e4A+az:Np!J*`y4pP &!CA?+Y=hٌ?~!N%czldŏߐ[a$O"C|,+G#:pХ_$s1=b˰)e8QMӧO"zErn-[6Ձа3ŔɅ[2)0!.I~%Xn`ެJ!Dfv祃"u\̈́ߓɗ~!gQXb #:ïI4Rl٢EwFS,=/>~YAg?+=L pP ^ g @z4I#hTT/X^=c3ysڛDG|^vÆ`? $O-J4RRн{tyUUIJڅ(v}'3)abY{5}ti,@ui'>TXaZ U ==]͛qO߾}-Țyf0`>>>ߤHOwؑ;6nee%6ln>skڵm4ILLl67~JJ w֍F-**b۶mӫWF-͛7oȐ!LF~:Ԭ:-[x_UFˉ :T'h&` ̾Xck/eW386A/c+ !hY~!wOu_B׮4$4M%..^@*{$*]vy}es0 ׿v=N1#"^nVt@&V^|;̾ Dڨm).mݞt{n(c?ӾNw99vYoIc>M _`+'@Md :cF>w~NLdB! ݪhF\\< M$>dnq ZH Rf`eVM= k/mf_r&Մ2Vz؞ z+mTS+1 ._^ѽB!NJ@ܽ&  V; :;ltuTW@{9.;-~MvV={z|b4=uv BzS>+qbbbc8fmڴ!((}پt7ݺuԫP%Swuom4۷2Mwcqo1όƦ_0 $$$4BΝ=vɵZ?00c6ݑǦ[QC@{s^5t{<9("""u^44ڻWhvYkL+!=g%|sOZO!']kM@,@?py ==^uq؈uNMAB.Tx0( eX0(MT0ͅyC‰|׷L&ޖH7oK߿Z^ <&hmt vn:AӈvډkunnTP5t`.&'B!Zw h"mPt v9 l2I5Bv?u7촱d_Pjhqa/d@X,>I%p8?ӫJݖ76/@X!?,Dֵ M#eǛN/a8P߻`[uI`M0}.:80Z|*iYYYGliXQQ%%%5dp]odM6M]oD^m6Me6IVVkڵkt]MnכצM&RRXXX߿ɑ].YYYʍ` 22􈈈&QPPPo/vU7:Oy9,,ɧ՛nZqZ4222[d7も4fy)b蹥*Յ:;8^=)wכԡ;w Ӓ7q]MnAf=t&rB!Dݪ]WbڢF^q]Ѿ]e4pMâT*/Ұl\ m2,(( --.]xuѻ~A]6m< BXXW}wر@ry\b4dff|TM iQQQMn , M?U )ݑ^©?M 1(Z^žU |z` <<@8//瓌FWt{Cyyd I v:@ˑ]rҹ'{R6I0N}ѪB!$Yg^4؟ʋn(dT3p'mt9Hrr2zYǂ[k!BRf(dL]^ta3qx'TUe,X4 :]ތ.UAfї 6 t{30(Af޾E*O4VaUL&&z ( ^BCfI7T=uDDsӺޞ[gj &ڹ/Lwfs}iFr~Z?W+I ,BVJٲe1M^nCa )e`e)!&ډg8-XP$vY$)Z]%9\b'%9_)N|/S~q.,[nꅖ()N D%:wg ,(P\}^<3;;yZ~K{-VZuUE !ĵ3{QZ:f/"bd>T$?{NnNvΦM'@T!nPka|l)~c6H3޷B1ݍVy(恣 $B!mDؤu9h+\µq1M!%GV1󔁻bPbϼD՚bǢȶ6n14XJ3%XS_!BH"|}|z]Ml=M0#.9NJcxB!(ꒁWB!B\/G(ׇ.NB!70Okk >_>xCǘ;w>eRhbz&nbpu K}}Ҳ`lfbι/[|~}{|Z3mЙun6Tү;CS۱#gb/`8}MT@@g]?``s%NJПKA !f7Ǹ]g4ly؆Y[&B+VLUլ|>|>74˲hh8Faa!sΗB+ y^/V0.^"rĄ72Ov܆9'_MwTxur8{{&=ʣoIS{_vg+x입2 -eW\ZGd(|{=ϫ-8GƏqϼ=Jsl qצe+b-޶{/q<rtNe¶]1?PO=aUB!S&VSdUsI?f{ ybCu|j%Yz5$iܱcDTl5K5N>EMM-:FQQ1spi-BrUlZ^o;>ҕB/:n[Ώ[zqbB,K?(&x q\u"%VtuڬजCB!bF&6|{8]r P=,WX8Fd3 ?x/|ob܁z]Drmcm#7γoVchҥ+(**9@`K]u*Ν;%x1<k֬6<N?{4YĢcoK !Å*b'h,d%휭 .%SWS\\|jf^Μ9͊+*Ι3,\J)IcP{;sp<Ƴ["l~|#Uڱѹ5vdp&c\RcL$鋇L99>|Rh1r]۬m SыWzm~=3O?Gmkl &#<BehhsZˣk8kڵx<μy)))BoͫjܿcolS;1j^K PZFɟN***QJq^bWvOg6 )i+-?3=:=[x[4ūo}'5N:9nZk<c|c.]UaB{Wby&BBA-{kq||||fͺ HMxZ)'<5ϼM0oSƙwXJFU6Yr$ 9yK.Ϙ&q8qVhӇ?1==\:Rw2=dkn5dO޹3e~9DŽBJk͑#R_xe7&.+W8| qPJQW5MM'aٲcbq8yPWTb&B!47w?]Jqq1552]eBd$!nDkͲe+PJqQ ÐXH",Bghh%Wg@CB4N>[ !KÔx&3*(Ø1RS};BJ %cܔ_Lo4tQkhc[;t/'TD/vI$|W7sC<H^B]5!Ba!Ĥ3oyyH_p;|\an㛼8˫ГgW?|ãmp_+~zz/U"^we>x#Յ]?skG(ǷaYz[a.^h9$'.Σ47߼9sڹRHoi IDATk!8]4{nF]  3ImL3 'alr;O쑹,r?e6la>Lɗs/iuSUܛwz|'x#^-N$Gt[{)kk6mO 2#4) !.4ٿ/kpv#Lr!Ε&Hk!A*aW.Ah% VY|Gmgi@7B7#F %d6%FZeЪ,R#W/ww{lUfӯ&`Uf zhgFn44ܥf1OeNB69fgʜ_-&;ZIהۖI׫mȷMf EC+Ղ"ecf !Hk!T%TyYkپu?2d;!r5T0NTΣ$U-IS`[;k=R#l%SGx0`H@NR!DVZ!a"̟;ՙS47s>[[>f>^rTB 9O緇ԃ)[;u:=jcSG'2[L!%͢B!fR"<4MܣQ!%/41\龰ږK>TL]&=X2dIZ9/Z~ʹ#t$B!Ibwy{R#7ͩZΣ`/$Ӂs J/N$Xү/!B!":C 'R͒CmǫRA]؆њ3<)hM0%' N)B!${g@".$ aLgbqTCS}0#9 rR"SJ+a' na DLl P,7FysrBiWnkl)}{8G}MU-HOJ9?MJ:<-=Fv4vL 35pP(ę8_;8C!igppp8n}v),,IR:n4CxȨ!fQwW; Ng7::\q?e7L4t86Ra}~6Z38^8Oלּ#myt^8FWwI;2,NB!䷽/^¬Y71kM8}$ΓX̜DQO#0 hǝ>5x%O7Gboݠ̝٧iM۩{ة{]9&=}o/ >8>vk DSoњuf̺Ɖ0j)౅k)L=fNu NEuPRz9fZ_4Rwqt PLJŅS/]T@yEd ow!fJ/B{6mo>˹q{嵘hm=G},X4)..fݺ X;w>eRbwE o܍YndU!}#jCE֩ZDC+x܌rۥb T[kaǧsHDӱ( I;tf!Nnb$]:>5fENnTKb1 (Q젲htYf>NnM崯}|;Ȉ͡HN4J,嚤geZ3i/ [1[ێc=-ug/OCSX!/p>X: б iNl-uu6zu6ra֔KN=Zϰ#ZvctP9|Mu/YL rtNe¶]1?PO50rV̥#JҤ7]X,Uċf˒ԅr{r?5˝œ^l+qZqbs]df KL+1C,pjГئ_JJw>}9(yZqK ?_6Wn^P+9ޏ[T cy)y.$j8`řI[_ ! pWRCX/t&_Lt=qgxz-(ZC9szfLӠ~UY#GqTex<.qawr<#_5oO5ur&i%4,db/Ͽ3bRA|m> +BOkz/RZ B!q,4?HXs! y/%Z* 3Yt9EEro{=^BMM'X>(w9d.b}xI }?s_cPQSF ڲ-l$-=-pW.wkB!0ˍ `U[2 e,Fj*Q5k֧-K;(--cMDoo/%%%R ?1AlyucLySv|(# +ΜM^7տfחw1 r !BLkJ,Nmx.&buݦHCad\C`E<v[t~yfkJw !Ws*>ܞVo#Ux\n'IS;k1]̟5MM'aٲ&Ďp ,Y\lOreWPoP7fqh݌Uy&&{oԉf_'|fW9_)! !ӑR%# q#Z댄q򺺥 I7Ms|"{_8ba[^EV??4_*NWB1h9qq0TŋH,$~^ Ywkon9=scead;8ҵrYqPp^Xqqb}:BO #˼>leӫ!`ňoffeKAȧ(Jĸ11EO[,3sgD 8<iEU^! %ߒ_vtEyȨ!fQ<:VzH"syQUVIYUf,OРX2o}y>,Œu[Ѧܡfe,wlPt7uw󂷇[U?cAߘ3,w^6ڟ(6ۜVlX'xzU:8>v:lƏ=5 :uaQ_/|$<`;;T8TϸiA5v*6_@]e5.äsTyAnqRv e^,B(,,F)0羍->W ͟Q^nǡ9 R~x`LB!7B"\XGUR |pT\'uNbJŞ/(]}lPڏ)k~L+7mk#oŸbU;Gn^S+6إNNb[n~Ov04excDksesg#G$~Pja |ǿSid[xt-؎H]~]8 xOn~C?ȯB!čW J Cc#-g]'r7UtaXsԯpUsʜ^VUBo(]@e7ٕBkMV9_B!$BM;XW؜^rӮV\!Ba!r#?%WCWg'b3 <9yB!$BDnä?y4B! !~M HA !BI-6Q1H ]o ʗ#B!$h&(t1EMw԰q4Wl4 yqllwy}xtfr1GݰCİ)}40^:v!BISxPt߄⿦//9.?'g}@cԺ-SSd}NsMOƋ}r끫%O8Ё{GkAu0'vϓ]^ibm_vZ^#^!BwgSʰ/gB^k^V}r.WV6eAqë~6,M?O R?@BT>n rr) u,{C%9M7  ;qɽ2\vifAǭ r[)vG\^B!BH"<7&ބ#S8ZTΒ`q, pۚW=0xs,Mʣ/gI0J񻽸B9f8& !~8CGG=$n*k1 C$B!㥢J\ɼ*$|$B!gݺ̝;qfԶ_H~ڵ`J",jXMSO;~NtPcH !n$Il۞~agZMDX1@nN 6ww/~8fs碕qOB!Zk)!L|([&hmK1U^sp.4_)3Grl9BH",$b[i) ; IDAT ]gSGDzO^OnٻϞ~e4u֙|Shh64PJB!2v$M'J*(UތuYfd,`ѭ,P:;;ywQ|auFi$gu%8cyH'٦MO ]qRQtneQAs bheРtYK1 2QZtZc 7ڟyUu14ҷ3DW)S~C:AVcքb'V>;YkB7(Ne 9aN,p(V ,҅'Kvzۜ=&v326oKH옡gQ6,vz˼)#F,~Lc>ڈ˕k ƣ|wŃHY>#kmxlo&, [TVU`d@^Y#<؍hԁI}Jg/?uَC֣-:.E&:5NgFW;S]\>r>fK~??k*R^>Kћ7a; 6x}|_~o/?Sk;ɗMlghٻO{?*޼ 7gѵww>ŇWp,Я_1Ob >_/bg㧿S<{>_w7xv?<58&pU|v`~M]WP4B!O|94T$]lCbA*]X,e/C$%F(x8HЎ(becko鵣di(?(N"9홽^GPw|@R͈^vO0Oz!Zy|d͝~0C.[]V1CgsĘط/|'E+۞}+6׉5SCJmI%~+(}e7?_>w.Z+Ex Л|rװ@?~7oyM 6LཏW?[:G}BU>wJ-/<<`*.eM[oBtriW_?:E$`cX?#-rȸz\Oqlk'NnQH=OtR<ۧr5R^ӝɔCx=8~02z߇9v[/g^i^~}8s{a^,~GU{{_P{32sy_6e(s碕<{l{5w jJ*(K=|[ӣ$w ߟOϚEuXyU|`խ@mI%R._=?<)u4kS?^<aC[xdFV}]TbiU >~e=|lxUGbj rAc88]>Nh J !ĕj%vϟỿw۬[g Wّй<;ɨB\g?;]8uy_ҵӯշK/{ľx??I/ٟ}-OSHw"=ui ڎTOO|׷O+-״('蚻 ʶGdL`yz>ra]BA-{٧5&,ˢ8~55rAB $¹L6 Vk)c8N비$B": _eT[]=^/|;#l&7Ʊtȑ-eWrre=@ÇjjJ!.{mKO[;DX!P<6y.wDx7ǎ^ \Pg47wZΜ9͢Eurb !ĸ҂YH",ӜK&|OO(("MWwD(#|am.uuK:NEE%g6I-$N$Ba! 5˾k௷vBqgf0oLp $B1A˝~-&Jtm+#/v#`Cj 'S]%=n'd$s?kv5~n}mp;s[zOH!* fAQ]յ϶ݵ`YW v]lRT!@=Fznn B?ϓGw9g{sΜQ^kxEjSbJ(Cс_ q]b- 6(֦iAN>'>TsQ6PmM@KI;LDin Wm!O] m  a&&B !m1]'Zm~q5YzƜQZ|0m+ZW4Yښb *.+.4]Xc)eMyIH qUDk>L&v"-~ (kҴd ڃ0m+| .]Ś$Q[]N!ٽ:ͬ;{"{:-.JBqߎ{R^AU~v^fenBB!OH \^\^pMQ,q !HIG VΕKXa YayJ+D[TT<:F E B4=&hx^/blR3?>d2a67nWvqəL&#B!DB81=& =㏱d^*?T)!ı*H! MIaQ&L*νsx8u73y@ߟ){׼ܧ03Y,K!B7wy+Nq|{K?h9;򧗥OI)'ٺob(|Oi|?*&:K=a・!Bq̕2Y[r)͑Bq&Lx˙L\L1~#gCr z {on@ fã_G3ho=^ṡT95d@ O0ӪWA1| 4^C;W x.mMA\Y0)(Zm'm*kZhej1qĴ-Pqo8H6ajPd8(V}0y&Imb:{x-=MפPiA KkuBE9S{͊؎t -+b3+oJj.X1п9!([7 kU,)_ !u _NҢ2\?cnC~{Nq{ZFp<@+=^ il޸Ukz7{7gY\masF{RI%qDSF).C^u!CTB,*eb'EJ q_mwe㦿Y7BgQJ7%eFV ?94#Q m:dv.P-O EV"^axXhlN~s2@"Y mQVj]e8Иg⥺.1\+`ы_mQHv󫞉Sת ^fU ZgRF>cFAgMl48¹HՊjZAMs5|.6Z٩ۏw Ȯ@i̓“ȊLfdui6yrƬ"޾1X}-՝UnV(8BuXX /;v2ЙFnXR,lD_grM+!8NɈO'N} h9bCJZm~G|k̪{5/}e)u4<%7&(֑qąG6妓URHuUZ(u[X ͻo^RCQ%# :˛(x^%sqŇQbZZ'oroR:x{ =9pPL nK|iU%?qBゖרۘe_]r_6&ı-;-NAE qdYlܗƊmx4/ Vf!tINЖ>X0hRTHE|=UkbZ1>+&3 IDAT *1 NJ|*UU.|mTĆa(ˬT0xl!.#ƥks4 -fr02,%BS@FL%  6MJYh^I]BVi!IRX3gQXY<1o:%ck2S\UQ !68ydYRH:Ŷ@?S|=-nC nDL ^or6hZ3 AZZTVVvu]QUɄf#88-[r2V9\T9\x,n{G*{KkݢN @%9&.3:ݹ !) ɟO{Ɵy(%s'+ӷWY%dTc_\ܸ2C#(qoA#qL[+iE֦C_=QS<"l<{V_nNz17EQhi z֞eڵKZZOrr2tڕ֭[[ GaBpg:XV}R M3U{X :dDX=+mx]Ea5F`Bqaɟ8'p]?ZVfH ?9k!ں+x4/sH+;|xxj'xHǘD:D'aPXYƊmx5;khG_ٟrKxxlM}fBՇ^&*0֑q}mNi,v~fk~q*?{W1ݠJkjauV6n֭[)((8@ngÆ ӢE  DLL(򋡇o4Nr@j0&Ak~<k8XO0n hB22uX'@g),@GL15FXR)%;p3m富؇ ,ڹ qCϡYm|b2_p MX1P^bC"`*UL_;> hс0@XGDzJ,FvÚT9aQ5 -|s\ ̖Fw!|z=  ˈ cPD`{]EQh.l~vd&s'.z #i*#{޸ `ʕl۶kd"88~ur(//lٳ<<}!99Vk➾,a#éZx5"|u7;ǕAC ~X"י su'⤊#33(||ήN(;G' R.TUb@VM1LTlv숛W@GfMqʹ`_k|Əw>?Oߣ&߾&6(7wb@<4}:D'panx;_1col]^yݛ'kz~۾ָ@Τwsg/{>:$1uWTfmiWtπx}Y37wKκk_,-!>##{Z/SN$$$`Z}+6KˉWX.Drdlbȴqgܴ,vE2hc> M '[=69s gBk&]0Fu˂<=S>Z fwN$K1mEII .Wū, d/68Z\XX!;;7Xt`sU׉hۿ?6r Y/܂:/R')t`/kDoO_ኹb5[񿱚͸lOnbrdb0v? E *K) vdjiE6",@uV̭ px\t5~tkvSoڑa?GOdx) Շ-:0k*J*uWfm^ٳ"~A%Xkڡ&B5 $҈sg2 l0K`[d2]X!BH |4/ݜv`u5>Qk ͢^A聋hµQys}|b.Y:;7оY7FۨlM/^ `PˎTV}tk__{6QPQ#oip&U@Fw_6_8eΖ5Ο]xHrgf3\y.eVM"O b=.8 !8=<iՏ3<6- u+AoG6LV&3%ث?BݜvVTʨyyᷯ%\e ,0gifWu&E]x&/Gwu߰Plx.Mיm %|0:J>]=\On|vRٞu{~'xI,M} ),EyN*c;:rA{t~0jJ_`a/_I<5l]S{l<&37?8_3GADD0J'NgóFTUk׮tMff&锕rj{me˖4tC Bo?Usp4#.&ĕ1홚 ǟ/u\sޏ&u`[^Q—߾=˸*_.-Ba!aW=7{-Mxh3**Jfau\>7w1dX)WYD^=foq>NGem,[}L7 }+ ,6=Zumf Jɼ8; FvCγsQVL^ op2KՖ;\Ԧo [weѮ E8| ٥uL7y|' 'qIT/8ȟ@?+XVINNn l"8؟ݥ<(bEIBXWˌ}[ћЊ¸+M#?C8(0n||@]q(&xBIH H| Scȣnf1[nT `ﰔaVdl6,a0黷ג6Q{$垁#1)*?l\=y ~Nh(<9SLZ3pd{*.^5IBH$|G7 e0fĶ`HL5z4Oo=.ؙKO?Mc-߻̒޾ve;saMܚF$FuK\p8_ԦO]:[xca8L- m^ +Bp?~TWtu l6T17(<a ,)J緂ݬ,bUI 1^cXLpP3 oT 9B!@XF9zFojjԴ=0GlɊ9g4]Z|F^FTmecf>v*+wg-QdU'&~UQ<RvO 2b冚)c:ǣi|fӮYsֺQ!|wwg|k>@أyXq֌Uy\ZǕ,Ї|Ѹlf+4m ߬]Z;1`v췗Ī\cu< {76n>m=E=y^CǼm UE!(ȏ*79%%43=v~Eue8wŧfUQDc4rLv^?Νif"24jNE] L !@Xp\XG7L^7iedN"!5}z^DB\2$uej4q+O@էQ9vp=vylE1 ѱT 27f+ 7y̹_obҋ؞]Y{IH13.kߛN1I,~ц}ih#bĦ}uW+/&5'G]Cf \u vЏ̬-esC\ޡ7ʊXC:DURÆ7{Wd'//:lǯyذCٓg,sbKsR=."15|6nYMhVu6*bbB!8)FBbE֬i1lEgW)Bgf+l~G ۹y|qMlЪv:3t½գ(vӡ#b HNO33Igl?X!\m0CW,)+Yc=ɺw00t޲<"܀ba+Pب{ bB^YMUcEz=tsVAz:uk8WrېȿߙU3p`-}HE"@T:wvre\=͉MRrBfFGov+̢8KDzZlxʓ @oF:ȖzSTh F^u^JD IaξlȣashS|((&+,a gUi̫Lg^e:Pne/:B՟zj֭h'Wlao qugF0bq۰K'oIJ6}t'Q ޸ &9Spl~!tuVyOJ@\ X%0j3A PXmP5i&mw%lSyy1>6 aN lJ6 h١Msԕ(1AaLehOV2s:Nj\ޡ7 IӎW~l]5.qsC]}:ŤlKlY`(Ϗ?;Sdo(B$16dUvFtF.Znw %ɻm {p, wvv,XY˥( V<7['BO`@,8-0@pX"[ uEao aul{LRc8T+Otvl{}۞:+I+M`>} q^|11s-/S¼ V(+*N|EQHكY5^+JyfD3;h^Kxfg\s(C:=ąD2iɔd\ޡ7{^ڲ.k'Kqkn}e߱^/!'αYl,߻|7>Lzq>z>f+; fG ,ݳLJ_{f>{7o]sw}{@|YKl Z̒d<dnS/]QfÏ'-&.]`'axjG&)*INB~+#ذSn%(@1 8umũ X<2Z--.l~ (x ݋ʟ?CT\l zDƖ@)PtH kFAE)acrKT+/<^AK#:(^%z^O; sbHr}|qk^.'%U|z]fxF>_qsca޲B&}6qzgOٻR^_'~q]!m-/§7=–.ZlhsLK󦳣 -:/&I:1?XLf5.۟$t·^B.3(jDjul=jE%l%bũ {YO.Lyb:fܮ Ql2MnkMr" O`B!8Ba!B!yDFB!B,{B!BL !B!+25ZH ,B!8oƑADD>>؝N('Ra!B!hij:lӦ*E B!Ba!B!Ba!B!Ba!B!Ba!B!L%F !8XVj&"@T!CJjjRB!B!XUȔB!B!/G:99YxΪY,"#8'w!\OB!9g>$$$YkWѣ\BΕT!!B;<0rCqv:S!Bs:>t;W!zH!BH , P!Uy򟿬`[-B!8CaEl*R~&[ҝKCTVB@5;n?}oMU(!ݸz\b,$K@YXFICokFfLyn[`ܻxo}^=7D$̃W9YL篥2qKXV^ n H `So T TF׼Ŭc:qۤ$*zO!q3*4kkZf䉼u,"f>>viRtFFhYlRN؊曱/ yjv#MMlz`4MlP)ذÐ2gQZQRL.n5rvꀅfC!R\d;Su C#ϤD^57߂S{e?ӑ!^_AȚ!oOk, :lvS_,FTu#J5cF~[k(MIKV- e2#z1k"~,fդXVP2 !!POXwz^&~ `Tqr.na-qy\K\/;xX2?c0ov-'Z慫Ipy*fˇE˼pYnz ߷g/"+9!&PZ0KYL4R[;=U%[N^zzמ'n5A4{z>z-6tWeE~*:Qˁ+q4t\Y+=]IK:z o9 *)6 }fG>=#cQ ;5>%%,ηQAcٖXXٝ"Ƭ? /%XrR,;:y{Jtz0򖻸oT7g|v ѱ1'p MkIǻ< vPѼ Yf`Pnw= m&ˇfC1Ƕ_NEH/` Kk(ѐ K{t{ ryx^_vuaC)s\C&,,a#0+jo͑7Ľ`a"6_%İz#^kwϠyײAټ{k!uz18ӣⵏF#gQWÎbP,hh}69콲"rmi4HydȌ4 !?=mΕ9-"%PgJ1‹R%.ïϤ1Y/a/i>|4Fބ  *`4iPPƕ Ѩsxx(q%J@8!vTuO5.Hk+ٸdS&Z%&LxMtg,cыk 3LvǴv6o/ӌ΃;!ѝu%ڎغ5y4[h*GS&V_ϻrvd Vf^ lܕݙƄUgBg =/'K5^~J \s%lmOO'Eqz8nrJ_v5]oA$6ט}Ʀ3̒0Zo8)8.w_v\˜ MΠgKf'd:ER6ӪmcTf:}?r<׋éqzZa&9>dx3x(f՟}ʎH[ox>c޷\|iw<[%52noߖ;"Z[*:կ3aa>A}C_9y*wнg6 {PR6W=c6rJǿ%!56囘DaU5i1?=MgƐ(׺‡ΑMBꡳ %_fCH]3!*a=dlVyx-!ķà.Ylͮ߾bwx.kc=-.cUeټ2Eо;}?LqDȼ[* #CR},kʲo &̔͊)-c %sƌNXs7fQ%]? wޏ3:z t_zVv2Z23-{2xlt^tV}/dswgydsj$$&` OYn Lg/MRTT} ѣ\BΕT!!'ʨ\Ƌ`0OCXU㩻aږm=rQqlڿ8Ѫތߘ{ٝ߼휙\}U&Z;!x6u{wnH8$"wջ<*aٰa/]ѿoWQbȻy构HD/c÷o1eZ2tϱ`Gh0s}N ~}w\wfɄVNv͝ʔo">(J~{f?}gys|qssk%{"0ilFB-Ŀ>oO\ph<OFLmbQO`87mr҂C@FGfHUkݛXK;).MP<fw_MYBG3LcxG%}hM>ؖsbٲTJg}- a)}HZ яq,Ȅ`%)- wȫsmi}[Yu*@bf3O/3sW7L@7GKHXC$@s+dl_8Bq2bMd8{[a3f͟gq!tUp7΀Ol1mI iwoTzz˷x TK Gw%gB!(-=jnKwb8d =gV1XwN8”i+ 6\ԇ̪P:3$F/C.;6&2~[C l5Ftd&_%\Ƣ+/eUNf!@;J*fƏZ%Ћ.M: N}`SPE-`U~"7*8pƮ59v糶CPλ3O$ԡ|skj 8TqȽcsv hy\cowl^miϞGZ0-#5B!ht/nUgu4Fbek=VW4d m`]]5 ?]ıY`{scOSUobqd\8ք1&}G2?y,9ϋ7q &0w]70[˨v0˚ҽyl__n̘T /hD܌01aD2?eMANUaYLwdVQ5 ]ǡ8;ߔSh$KHrެr?^^!B2W1Su.!!M}0B!),,׷=IgscZ,tN2;{`B;<(U_<?G pFlCBB!0At^l>a!B!SXXŋB!B{o_JKѳ;vkjj,ӳǻJB!B.hh~k߿]KfB!B experiment.csv python-sense-emu-1.2/docs/sense_emu_gui.rst000066400000000000000000000076321411441564000211150ustar00rootroot00000000000000.. _sense_emu_gui: ============== Sense Emulator ============== The Sense HAT emulator application. This GTK application provides an interactive interface for emulating the Raspberry Pi Sense HAT. Synopsis ======== :: sense_emu_gui Usage ===== .. image:: gui_overview.png :align: center The main window is divided into four parts. At the left is a visual representation of the Sense HAT. Scripts using the emulator library (:mod:`sense_emu`) to set the HAT's LEDs will display the result here. Immediately below the LEDs are the rotation buttons which rotate the view of the HAT. These buttons also affect the action of the joystick buttons (covered below). To the right of the LEDs are three sliders representing the temperature, pressure, and humidity of the emulated HAT's environment. .. note:: The emulation does not *precisely* reflect the settings of the temperature, pressure, and humidity sliders. Random errors are introduced that scale according to the sensor specifications, and as the sliders are adjusted, the sensor value will gradually drift towards the new setting at a similar rate to the sensors on the real HAT. On the far right of the window, three sliders provide the orientation of the emulated HAT in terms of yaw (rotation around the vertical Z axis), pitch (rotation around the Y axis), and roll (rotation around the X axis). Adjusting these sliders affect the accelerometer, gyroscope, and magnetometer (compass) sensors on the emulated HAT. The emulated HAT assumes gravity runs vertically in the direction of the Z axis (as in the real HAT), and that North is in the direction of the X axis. Finally, at the bottom right of the window, a series of buttons are provided to emulate the joystick on the HAT. The buttons will simulate *press*, *release*, and *hold* events generated by the real joystick. When the view of the HAT is rotated, the joystick buttons will act in the new orientation of the HAT. For example, initially the "up" button will send "up" events. After the HAT is rotated 90° the "up" button will send "right" events. After another 90° rotation, the "up" button will send "down" events, and so on. .. note:: The emulator must be run prior to starting any scripts which expect to use the :mod:`sense_emu` library. However, the emulator can be terminated (and restarted) while scripts using the library are running (obviously, when the emulator isn't running sensor errors can't be emulated and all sensor readings will appear fixed). Attempting to spawn more than one instance of the emulator will simply activate the existing instance. Preferences =========== On slower Pis in particular, you may wish to disable some aspects of the emulation for performance purposes. From the "Edit" menu, select "Preferences". In the window that appears you can control which aspects of the emulation are enabled, and what speed the screen updates will be limited to. .. image:: gui_prefs.png :align: center You can also control the appearance of angles for the orientation sliders (note that this is a purely visual preference; it doesn't affect the output of the emulated sensors in any way). Replay ====== Recordings of actual sensor readings from a Sense HAT can be replayed within the emulator. From the "File" menu, select "Replay recording". From the file selection dialog that appears, select the recording you wish to replay and click "Open". The replay will immediately begin, with progress displayed in a bar at the bottom of the main window. You can click "Stop" (next to the progress bar) to terminate playback of the recording. .. image:: gui_replay.png :align: center During playback, the sensor sliders will move according to the data in the recording but will be disabled (to prevent the user affecting the replay). At the end of the replay (or immediately after termination of playback), the sliders will be left at their present positions and re-enabled. python-sense-emu-1.2/docs/sense_play.rst000066400000000000000000000034511411441564000204230ustar00rootroot00000000000000.. _sense_play: ========== sense_play ========== Replays readings recorded from a Raspberry Pi Sense HAT, via the Sense HAT emulation library. Synopsis ======== .. code-block:: text sense_play [-h] [--version] [-q] [-v] [-l FILE] [-P] input Description =========== .. program:: sense_play .. option:: -h, --help show this help message and exit .. option:: --version show this program's version number and exit .. option:: -q, --quiet produce less console output .. option:: -v, --verbose produce more console output .. option:: -l FILE, --log-file FILE log messages to the specified file .. option:: -P, --pdb run under PDB (debug mode) Examples ======== To play back an experiment recorded from the Sense HAT, simply execute :program:`sense_play` with the filename you wish to play back: .. code-block:: console $ sense_play experiment.hat Playback will start immediately and continue in real-time (at the recording rate) until the file is exhausted. If you wish to start an emulated script at the same time as playback, you can use the shell's job control facilities: .. code-block:: console $ sense_play experiment.hat & python experiment.py If :file:`-` is specified as the input file, :program:`sense_play` will read its from stdin. This can be used to play back compressed recordings (see Examples under :program:`sense_rec`) without using any disk space for decompression: .. code-block:: console $ gunzip -c experiment.hat.gz | sense_play - .. note:: If playback is going too slowly (e.g. because the Pi is too busy with other tasks, or because the data cannot be read quickly enough from the SD card), :program:`sense_play` will skip records and print a warning to the console at the end of playback with the number of records skipped. python-sense-emu-1.2/docs/sense_rec.rst000066400000000000000000000103531411441564000202260ustar00rootroot00000000000000.. _sense_rec: ========= sense_rec ========= Records sensor readings from the Raspberry Pi Sense HAT in real time, outputting the results to a file for later playback or analysis. This is most useful for preparing records of experiments for use with the Sense HAT emulator. For example, a recording of a Sense HAT being dropped, a recording of a `HAB`_ flight, a recording of the cycle of temperature over a few days, etc. Synopsis ======== .. code-block:: text sense_rec [-h] [--version] [-q] [-v] [-l FILE] [-P] [-c CONFIG] [-d DURATION] [-f] output Description =========== .. program:: sense_rec .. option:: -h, --help show this help message and exit .. option:: --version show this program's version number and exit .. option:: -q, --quiet produce less console output .. option:: -v, --verbose produce more console output .. option:: -l FILE, --log-file FILE log messages to the specified file .. option:: -P, --pdb run under PDB (debug mode) .. option:: -c FILE, --config FILE the Sense HAT configuration file to use (default: :file:`/etc/RTIMULib.ini`) .. option:: -d SECS, --duration SECS the duration to record for in seconds (default: record until terminated with :kbd:`Control-C`) .. option:: -i SECS, --interval SECS the delay between each reading in seconds (default: the IMU polling interval, typically 0.003 seconds) .. option:: -f, --flush flush every record to disk immediately; reduces chances of truncated data on power loss, but greatly increases disk activity Examples ======== To record an experiment with the Sense HAT, simply execute :program:`sense_rec` with the filename you wish to record the results: .. code-block:: console $ sense_rec experiment.hat By default, the recording will continue indefinitely. Press :kbd:`Control-C` to terminate the recording. If you want to record for a specific duration, you can use the :option:`--duration` option to specify the number of seconds: .. code-block:: console $ sense_rec --duration 10 short_experiment.hat This tool can be run simultaneously with scripts that use the Sense HAT. Simply start your script in one terminal, then open another to start :program:`sense_rec`. Alternatively, you can use the shell's job control facilities to start recording in the background: .. code-block:: console $ sense_rec experiment.hat & $ python experiment.py ... $ kill %1 .. warning:: Be aware that other scripts attempting to use the HAT's sensors will likely obtain different readings than they would have if run standalone. Some of the HAT's sensors are affected by their query-rate, and :program:`sense_rec` drives all sensors at close to their maximum rate. If :file:`-` is specified as the output file, :program:`sense_rec` will write its output to stdout. This can be used to reduce the disk space required for long output by piping the output through a compression tool like :program:`gzip`: .. code-block:: console $ sense_rec - | gzip -c - > experiment.hat.gz When compressed in this manner the data typically uses approximately 3Kb per second (without :program:`gzip` the recording will use approximately 10Kb of disk space per second). Use :program:`gunzip` to de-compress the data for playback or analysis: .. code-block:: console $ gunzip -c experiment.hat.gz | sense_play - Another method of reducing the data usage is increasing the interval between readings (the default is the IMU polling interval which is an extremely short 3ms). Obviously a longer interval will reduce the "fidelity" of the recording; you will only see the sensors update at each interval during playback, however it can be extremely useful for very long recordings. For example, to record with a 1 second interval between readings for 24 hours: .. code-block:: console $ sense_rec -i 1 -d $((24*60*60)) one_day_experiment.hat Finally, you can use pipes in conjunction with :program:`sense_csv` to produce CSV output directly: .. code-block:: console $ sense_rec - | sense_csv - experiment.csv Be warned that CSV data is substantially larger than the binary format (CSV data uses approximately 25Kb per second at the default interval). .. _HAB: https://en.wikipedia.org/wiki/High-altitude_balloon python-sense-emu-1.2/icons/000077500000000000000000000000001411441564000157075ustar00rootroot00000000000000python-sense-emu-1.2/icons/Makefile000066400000000000000000000023731411441564000173540ustar00rootroot00000000000000PNGS=16x16/sense_emu_gui.png 24x24/sense_emu_gui.png 32x32/sense_emu_gui.png 48x48/sense_emu_gui.png 64x64/sense_emu_gui.png 128x128/sense_emu_gui.png XPMS=xpm/sense_emu_gui.xpm ICONS=ico/sense_emu_gui.ico DIRS=16x16 24x24 32x32 48x48 64x64 128x128 xpm ico all: $(PNGS) $(XPMS) $(ICONS) clean: rm -f $(PNGS) $(XPMS) $(ICONS) -rmdir $(DIRS) $(DIRS): mkdir $@ 16x16/%.png: scalable/%.svg 16x16 convert -background none $< -resize 16x16 $@ 24x24/%.png: scalable/%.svg 24x24 convert -background none $< -resize 24x24 $@ 32x32/%.png: scalable/%.svg 32x32 convert -background none $< -resize 32x32 $@ 48x48/%.png: scalable/%.svg 48x48 convert -background none $< -resize 48x48 $@ 64x64/%.png: scalable/%.svg 64x64 convert -background none $< -resize 64x64 $@ 128x128/%.png: scalable/%.svg 128x128 convert -background none $< -resize 128x128 $@ # Starting from a large res PNG and resizing downward seems to produce the best # "lower resolution" XPMs and ICOs xpm/%.xpm: 128x128/%.png xpm convert $< -resize 32x32 $@ ico/%.ico: 128x128/%.png ico convert $< -bordercolor white -border 0 \ \( -clone 0 -resize 16x16 \) \ \( -clone 0 -resize 32x32 \) \ \( -clone 0 -resize 48x48 \) \ \( -clone 0 -resize 64x64 \) \ -delete 0 -alpha off -colors 256 $@ python-sense-emu-1.2/icons/scalable/000077500000000000000000000000001411441564000174555ustar00rootroot00000000000000python-sense-emu-1.2/icons/scalable/sense_emu_gui.svg000066400000000000000000001032431411441564000230300ustar00rootroot00000000000000 image/svg+xml python-sense-emu-1.2/sense_emu/000077500000000000000000000000001411441564000165575ustar00rootroot00000000000000python-sense-emu-1.2/sense_emu/RTIMU.py000066400000000000000000000170601411441564000200350ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import mmap from time import time import numpy as np from .pressure import init_pressure, PRESSURE_DATA, PressureData, PRESSURE_FACTOR, TEMP_FACTOR, TEMP_OFFSET from .humidity import init_humidity, HUMIDITY_DATA, HumidityData from .imu import init_imu, IMU_DATA, IMUData, ACCEL_FACTOR, GYRO_FACTOR, COMPASS_FACTOR, ORIENT_FACTOR class Settings: def __init__(self, path): self.path = path class RTIMU: def __init__(self, settings): self.settings = settings self._fd = init_imu() self._map = mmap.mmap(self._fd.fileno(), 0, access=mmap.ACCESS_READ) self._last_data = None self._imu_data = { 'accel': (0.0, 0.0, 0.0), 'accelValid': False, 'compass': (0.0, 0.0, 0.0), 'compassValid': False, 'fusionPose': (0.0, 0.0, 0.0), 'fusionPoseValid': False, 'fusionQPose': (0.0, 0.0, 0.0, 0.0), 'fusionQPoseValid': False, 'gyro': (0.0, 0.0, 0.0), 'gyroValid': False, 'humidity': float('nan'), 'humidityValid': False, 'pressure': float('nan'), 'pressureValid': False, 'temperature': float('nan'), 'temperatureValid': False, 'timestamp': 0, } def _read(self): ( type, name, timestamp, ax, ay, az, gx, gy, gz, cx, cy, cz, ox, oy, oz, ) = IMU_DATA.unpack_from(self._map) return IMUData( type, name, timestamp, np.array((ax, ay, az)), np.array((gx, gy, gz)), np.array((cx, cy, cz)), np.array((ox, oy, oz)), ) def IMUInit(self): self._last_data = self._read() return self._last_data.type != 0 def IMUGetPollInterval(self): return 10 # 3 on the actual board def IMUGetGyroBiasValid(self): raise NotImplementedError def IMURead(self): data = self._read() if data.timestamp == self._last_data.timestamp: return False else: self._last_data = data self._imu_data = { 'accel': tuple(data.accel / ACCEL_FACTOR), 'accelValid': True, 'compass': tuple((data.compass / COMPASS_FACTOR) * 100), # convert Gauss to uT 'compassValid': True, 'fusionPose': tuple(data.orient / ORIENT_FACTOR), 'fusionPoseValid': True, 'fusionQPose': (0.0, 0.0, 0.0, 0.0), 'fusionQPoseValid': False, 'gyro': tuple(data.gyro / GYRO_FACTOR), 'gyroValid': True, 'humidity': float('nan'), 'humidityValid': False, 'pressure': float('nan'), 'pressureValid': False, 'temperature': float('nan'), 'temperatureValid': False, 'timestamp': data.timestamp, } return True def IMUType(self): return self._read().type # 6 in real unit def IMUName(self): return self._read().name.decode('ascii') # "LSM9DS1" in real unit def getAccel(self): return self._imu_data['accel'] def getAccelCalibrationValid(self): raise NotImplementedError def getAccelResiduals(self): raise NotImplementedError def getCompass(self): return self._imu_data['compass'] def getCompassCalibrationEllipsoidValid(self): raise NotImplementedError def getCompassCalibrationValid(self): raise NotImplementedError def getFusionData(self): return self._imu_data['fusionPose'] def getGyro(self): return self._imu_data['gyro'] def getIMUData(self): return self._imu_data def getMeasuredPose(self): raise NotImplementedError def getMeasuredQPose(self): raise NotImplementedError def setCompassEnable(self, value): pass def setGyroEnable(self, value): pass def setAccelEnable(self, value): pass class RTPressure(object): def __init__(self, settings): self.settings = settings self._fd = init_pressure() self._map = mmap.mmap(self._fd.fileno(), 0, access=mmap.ACCESS_READ) self._last_read = 0.0 self._last_data = None self._p_ref = None def _read(self): now = time() if now - self._last_read > 0.04: self._last_read = now self._last_data = PressureData(*PRESSURE_DATA.unpack_from(self._map)) return self._last_data def pressureInit(self): d = self._read() self._p_ref = d.P_REF return d.type != 0 def pressureRead(self): if self._p_ref is None: return (0, 0.0, 0, 0.0) else: d = self._read() return ( d.P_VALID, d.P_OUT / PRESSURE_FACTOR, d.T_VALID, d.T_OUT / TEMP_FACTOR + TEMP_OFFSET, ) def pressureType(self): return self._read().type def pressureName(self): return self._read().name.decode('ascii') class RTHumidity(object): def __init__(self, settings): self.settings = settings self._fd = init_humidity() self._map = mmap.mmap(self._fd.fileno(), 0, access=mmap.ACCESS_READ) self._last_read = 0.0 self._last_data = None self._humidity_m = None self._humidity_c = None self._temp_m = None self._temp_c = None def _read(self): now = time() if now - self._last_read > 0.13: self._last_read = now self._last_data = HumidityData(*HUMIDITY_DATA.unpack_from(self._map)) return self._last_data def humidityInit(self): d = self._read() try: self._humidity_m = (d.H1 - d.H0) / (d.H1_OUT - d.H0_OUT) self._humidity_c = d.H0 - self._humidity_m * d.H0_OUT self._temp_m = (d.T1 - d.T0) / (d.T1_OUT - d.T0_OUT) self._temp_c = d.T0 - self._temp_m * d.T0_OUT return True except ZeroDivisionError: return False def humidityRead(self): if self._temp_m is None: return (0, 0.0, 0, 0.0) else: d = self._read() return ( d.H_VALID, d.H_OUT * self._humidity_m + self._humidity_c, d.T_VALID, d.T_OUT * self._temp_m + self._temp_c, ) def humidityType(self): return self._read().type def humidityName(self): return self._read().name.decode('ascii') python-sense-emu-1.2/sense_emu/__init__.py000066400000000000000000000055001411441564000206700ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see "The Raspberry Pi Sense HAT Emulator library" import sys from .sense_hat import SenseHat, SenseHat as AstroPi from .stick import ( SenseStick, InputEvent, DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_MIDDLE, ACTION_PRESSED, ACTION_RELEASED, ACTION_HELD, ) __project__ = 'sense-emu' __version__ = '1.2' __author__ = 'Raspberry Pi Foundation' __author_email__ = 'info@raspberrypi.org' __url__ = 'http://sense-emu.readthedocs.io/' __platforms__ = ['ALL'] __classifiers__ = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: X11 Applications :: GTK', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', 'Operating System :: POSIX :: Linux', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Topic :: Scientific/Engineering', ] __keywords__ = [ 'raspberrypi', 'sense', 'hat' ] __requires__ = [ 'numpy', 'Pillow', ] __extra_requires__ = { 'doc': ['sphinx'], 'test': ['pytest', 'coverage', 'mock'], } if sys.version_info[:2] == (3, 2): # Particular versions are required for Python 3.2 compatibility __extra_requires__['doc'].extend([ 'Jinja2<2.7', 'MarkupSafe<0.16', ]) __extra_requires__['test'][1] = 'coverage<4.0dev' __entry_points__ = { 'console_scripts': [ 'sense_rec = sense_emu.record:app', 'sense_play = sense_emu.play:app', 'sense_csv = sense_emu.dump:app', ], 'gui_scripts': [ 'sense_emu_gui = sense_emu.gui:main', ], } python-sense-emu-1.2/sense_emu/common.py000066400000000000000000000047541411441564000204330ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import io import errno from struct import Struct from collections import namedtuple # Structures for sense_rec and sense_play HEADER_REC = Struct( '=' # native order, standard sizing '8s' # magic number ("SENSEHAT") 'b' # version number (1) '7x' # padding 'd' # initial timestamp ) DATA_REC = Struct( '=' # native order, standard sizing 'd' # timestamp 'dd' # pressure+temp readings 'dd' # humidity+temp readings 'ddd' # raw accelerometer readings 'ddd' # raw gyro readings 'ddd' # raw compass readings 'ddd' # calculated pose ) DataRecord = namedtuple('DataRecord', ( 'timestamp', 'pressure', 'ptemp', 'humidity', 'htemp', 'ax', 'ay', 'az', 'gx', 'gy', 'gz', 'cx', 'cy', 'cz', 'ox', 'oy', 'oz', )) def clamp(value, min_value, max_value): """ Return *value* clipped to the range *min_value* to *max_value* inclusive. """ return min(max_value, max(min_value, value)) def slow_pi(): """ Returns ``True`` if the local hardware is a Raspberry Pi with a slow processor, specifically a BCM2835. This is used to determine defaults for the simulation's processing. """ # FIXME this won't work with modern kernels that all like the hardware as # BCM2835; use /proc/device-tree/model and parse SoC bits try: cpu = '' with io.open('/proc/cpuinfo', 'r') as f: for line in f: if line.startswith('Hardware'): cpu = line.split(':', 1)[1].strip() break return cpu in ('BCM2835', 'BCM2708') except IOError as e: if e.errno == errno.ENOENT: return False raise python-sense-emu-1.2/sense_emu/dump.py000066400000000000000000000071731411441564000201060ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see import sys import os import csv import logging import argparse import datetime as dt from time import time from . import __version__ from .i18n import _ from .terminal import TerminalApplication, FileType from .common import HEADER_REC, DATA_REC, DataRecord class DumpApplication(TerminalApplication): def __init__(self): super(DumpApplication, self).__init__( version=__version__, description=_("Converts a Sense HAT recording to CSV format, for " "the purposes of debugging or analysis.")) self.parser.add_argument( '--timestamp-format', action='store', default='%Y-%m-%dT%H:%M:%S.%f', metavar='FMT', help=_('the format to use when outputting the record timestamp ' '(default: %(default)s)')) self.parser.add_argument( '--header', action='store_true', default=False, help=_('if specified, output column headers')) self.parser.add_argument('input', type=FileType('rb')) self.parser.add_argument('output', type=FileType('w', encoding='utf-8')) def source(self, f): logging.info(_('Reading header')) magic, ver, offset = HEADER_REC.unpack(f.read(HEADER_REC.size)) if magic != b'SENSEHAT': raise IOError(_('Invalid magic number at start of input')) if ver != 1: raise IOError(_('Unrecognized file version number (%d)') % ver) logging.info( _('Dumping recording taken at %s'), dt.datetime.fromtimestamp(offset).strftime('%c')) offset = time() - offset while True: buf = f.read(DATA_REC.size) if not buf: break elif len(buf) < DATA_REC.size: raise IOError(_('Incomplete data record at end of file')) else: yield DataRecord(*DATA_REC.unpack(buf)) def main(self, args): writer = csv.writer(args.output) if args.header: writer.writerow(( 'timestamp', 'pressure', 'pressure_temp', 'humidity', 'humidity_temp', 'accel_x', 'accel_y', 'accel_z', 'gyro_x', 'gyro_y', 'gyro_z', 'compass_x', 'compass_y', 'compass_z', 'orient_x', 'orient_y', 'orient_z', )) for rec, data in enumerate(self.source(args.input)): writer.writerow(( dt.datetime.fromtimestamp(data.timestamp).strftime(args.timestamp_format), data.pressure, data.ptemp, data.humidity, data.htemp, data.ax, data.ay, data.az, data.gx, data.gy, data.gz, data.cx, data.cy, data.cz, data.ox, data.oy, data.oz, )) logging.info(_('Converted %d records'), rec) app = DumpApplication() python-sense-emu-1.2/sense_emu/examples/000077500000000000000000000000001411441564000203755ustar00rootroot00000000000000python-sense-emu-1.2/sense_emu/examples/advanced/000077500000000000000000000000001411441564000221425ustar00rootroot00000000000000python-sense-emu-1.2/sense_emu/examples/advanced/joystick_events.py000066400000000000000000000013741411441564000257440ustar00rootroot00000000000000from sense_emu import SenseHat from signal import pause x = y = 4 hat = SenseHat() def update_screen(): hat.clear() hat.set_pixel(x, y, 255, 255, 255) def clamp(value, min_value=0, max_value=7): return min(max_value, max(min_value, value)) def move_dot(event): global x, y if event.action in ('pressed', 'held'): x = clamp(x + { 'left': -1, 'right': 1, }.get(event.direction, 0)) y = clamp(y + { 'up': -1, 'down': 1, }.get(event.direction, 0)) update_screen() hat.stick.direction_up = move_dot hat.stick.direction_down = move_dot hat.stick.direction_left = move_dot hat.stick.direction_right = move_dot hat.stick.direction_any = update_screen pause() python-sense-emu-1.2/sense_emu/examples/advanced/sensor_menu.py000066400000000000000000000046031411441564000250540ustar00rootroot00000000000000from sense_emu import * import numpy as np # Draw the foreground (fg) into a numpy array Rd = (255, 0, 0) Gn = (0, 255, 0) Bl = (0, 0, 255) Gy = (128, 128, 128) __ = (0, 0, 0) fg = np.array([ [Rd, Rd, Rd, __, Gn, Gn, __, __], [__, Rd, __, __, Gn, __, Gn, __], [__, Rd, __, __, Gn, Gn, __, __], [__, Rd, __, __, Gn, __, __, __], [Bl, __, Bl, __, __, Gy, __, __], [Bl, Bl, Bl, __, Gy, __, Gy, __], [Bl, __, Bl, __, Gy, __, Gy, __], [Bl, __, Bl, __, __, Gy, Gy, __], ], dtype=np.uint8) # Mask is a boolean array of which pixels are transparent mask = np.all(fg == __, axis=2) def display(hat, selection): # Draw the background (bg) selection box into another numpy array left, top, right, bottom = { 'T': (0, 0, 4, 4), 'P': (4, 0, 8, 4), 'Q': (4, 4, 8, 8), 'H': (0, 4, 4, 8), }[selection] bg = np.zeros((8, 8, 3), dtype=np.uint8) bg[top:bottom, left:right, :] = (255, 255, 255) # Construct final pixels from bg array with non-transparent elements of # the menu array hat.set_pixels([ bg_pix if mask_pix else fg_pix for (bg_pix, mask_pix, fg_pix) in zip( (p for row in bg for p in row), (p for row in mask for p in row), (p for row in fg for p in row), ) ]) def execute(hat, selection): if selection == 'T': hat.show_message('Temperature: %.1fC' % hat.temp, 0.05, Rd) elif selection == 'P': hat.show_message('Pressure: %.1fmbar' % hat.pressure, 0.05, Gn) elif selection == 'H': hat.show_message('Humidity: %.1f%%' % hat.humidity, 0.05, Bl) else: return True return False def move(selection, direction): return { ('T', DIRECTION_RIGHT): 'P', ('T', DIRECTION_DOWN): 'H', ('P', DIRECTION_LEFT): 'T', ('P', DIRECTION_DOWN): 'Q', ('Q', DIRECTION_UP): 'P', ('Q', DIRECTION_LEFT): 'H', ('H', DIRECTION_RIGHT): 'Q', ('H', DIRECTION_UP): 'T', }.get((selection, direction), selection) hat = SenseHat() selection = 'T' while True: display(hat, selection) event = hat.stick.wait_for_event() if event.action == ACTION_PRESSED: if event.direction == DIRECTION_MIDDLE: if execute(hat, selection): break else: selection = move(selection, event.direction) hat.clear() python-sense-emu-1.2/sense_emu/examples/basic/000077500000000000000000000000001411441564000214565ustar00rootroot00000000000000python-sense-emu-1.2/sense_emu/examples/basic/humidity.py000066400000000000000000000004321411441564000236630ustar00rootroot00000000000000from sense_emu import SenseHat sense = SenseHat() green = (0, 255, 0) white = (255, 255, 255) while True: humidity = sense.humidity humidity_value = 64 * humidity / 100 pixels = [green if i < humidity_value else white for i in range(64)] sense.set_pixels(pixels) python-sense-emu-1.2/sense_emu/examples/basic/temperature.py000066400000000000000000000003251411441564000243650ustar00rootroot00000000000000from sense_emu import SenseHat sense = SenseHat() red = (255, 0, 0) blue = (0, 0, 255) while True: temp = sense.temp pixels = [red if i < temp else blue for i in range(64)] sense.set_pixels(pixels) python-sense-emu-1.2/sense_emu/examples/intermediate/000077500000000000000000000000001411441564000230475ustar00rootroot00000000000000python-sense-emu-1.2/sense_emu/examples/intermediate/bar_graph.py000066400000000000000000000043201411441564000253450ustar00rootroot00000000000000import numpy as np from time import sleep from sense_emu import SenseHat def clamp(value, min_value, max_value): """ Returns *value* clamped to the range *min_value* to *max_value* inclusive. """ return min(max_value, max(min_value, value)) def scale(value, from_min, from_max, to_min=0, to_max=8): """ Returns *value*, which is expected to be in the range *from_min* to *from_max* inclusive, scaled to the range *to_min* to *to_max* inclusive. If *value* is not within the expected range, the result is not guaranteed to be in the scaled range. """ from_range = from_max - from_min to_range = to_max - to_min return (((value - from_min) / from_range) * to_range) + to_min def render_bar(screen, origin, width, height, color): """ Fills a rectangle within *screen* based at *origin* (an ``(x, y)`` tuple), *width* pixels wide and *height* pixels high. The rectangle will be filled in *color*. """ # Calculate the coordinates of the boundaries x1, y1 = origin x2 = x1 + width y2 = y1 + height # Invert the Y-coords so we're drawing bottom up max_y, max_x = screen.shape[:2] y1, y2 = max_y - y2, max_y - y1 # Draw the bar screen[y1:y2, x1:x2, :] = color def display_readings(hat): """ Display the temperature, pressure, and humidity readings of the HAT as red, green, and blue bars on the screen respectively. """ # Calculate the environment values in screen coordinates temperature_range = (0, 40) pressure_range = (950, 1050) humidity_range = (0, 100) temperature = scale(clamp(hat.temperature, *temperature_range), *temperature_range) pressure = scale(clamp(hat.pressure, *pressure_range), *pressure_range) humidity = scale(clamp(hat.humidity, *humidity_range), *humidity_range) # Render the bars screen = np.zeros((8, 8, 3), dtype=np.uint8) render_bar(screen, (0, 0), 2, round(temperature), color=(255, 0, 0)) render_bar(screen, (3, 0), 2, round(pressure), color=(0, 255, 0)) render_bar(screen, (6, 0), 2, round(humidity), color=(0, 0, 255)) hat.set_pixels([pixel for row in screen for pixel in row]) hat = SenseHat() while True: display_readings(hat) sleep(0.1) python-sense-emu-1.2/sense_emu/examples/intermediate/joystick_loop.py000066400000000000000000000012111411441564000263040ustar00rootroot00000000000000from sense_emu import SenseHat x = y = 4 hat = SenseHat() def update_screen(): hat.clear() hat.set_pixel(x, y, 255, 255, 255) def clamp(value, min_value=0, max_value=7): return min(max_value, max(min_value, value)) def move_dot(event): global x, y if event.action in ('pressed', 'held'): x = clamp(x + { 'left': -1, 'right': 1, }.get(event.direction, 0)) y = clamp(y + { 'up': -1, 'down': 1, }.get(event.direction, 0)) update_screen() while True: for event in hat.stick.get_events(): move_dot(event) update_screen() python-sense-emu-1.2/sense_emu/examples/intermediate/line_graph.py000066400000000000000000000034751411441564000255420ustar00rootroot00000000000000import numpy as np from time import sleep from sense_emu import SenseHat def clamp(value, min_value, max_value): """ Returns *value* clamped to the range *min_value* to *max_value* inclusive. """ return min(max_value, max(min_value, value)) def scale(value, from_min, from_max, to_min=0, to_max=7): """ Returns *value*, which is expected to be in the range *from_min* to *from_max* inclusive, scaled to the range *to_min* to *to_max* inclusive. If *value* is not within the expected range, the result is not guaranteed to be in the scaled range. """ from_range = from_max - from_min to_range = to_max - to_min return (((value - from_min) / from_range) * to_range) + to_min def display_readings(hat): """ Display the temperature, pressure, and humidity readings of the HAT as red, green, and blue bars on the screen respectively. """ temperature_range = (0, 40) pressure_range = (950, 1050) humidity_range = (0, 100) temperature = 7 - round(scale(clamp(hat.temperature, *temperature_range), *temperature_range)) pressure = 7 - round(scale(clamp(hat.pressure, *pressure_range), *pressure_range)) humidity = 7 - round(scale(clamp(hat.humidity, *humidity_range), *humidity_range)) # Scroll screen 1 pixel left, clear the right column, and render new points screen = np.array(hat.get_pixels(), dtype=np.uint8).reshape((8, 8, 3)) screen[:, :-1, :] = screen[:, 1:, :] screen[:, 7, :] = (0, 0, 0) screen[temperature, 7, :] += np.array((255, 0, 0), dtype=np.uint8) screen[pressure, 7, :] += np.array((0, 255, 0), dtype=np.uint8) screen[humidity, 7, :] += np.array((0, 0, 255), dtype=np.uint8) hat.set_pixels([pixel for row in screen for pixel in row]) hat = SenseHat() hat.clear() while True: display_readings(hat) sleep(1) python-sense-emu-1.2/sense_emu/examples/intermediate/plumb_line.py000066400000000000000000000011041411441564000255430ustar00rootroot00000000000000from time import sleep from sense_emu import SenseHat from PIL import Image, ImageDraw hat = SenseHat() hat.clear() origin = (7, 7) while True: a = hat.get_accelerometer_raw() # Use the old trick of drawing something too big then down-sizing to get an # anti-aliased line img = Image.new('RGB', (15, 15)) draw = ImageDraw.Draw(img) dest = (origin[0] + a['x'] * 7.0, origin[1] + a['y'] * 7.0) draw.line([origin, dest], fill=(255, 255, 255), width=3) img = img.resize((8, 8), Image.BILINEAR) hat.set_pixels(list(img.getdata())) sleep(0.04) python-sense-emu-1.2/sense_emu/examples/intermediate/rainbow.py000066400000000000000000000023151411441564000250630ustar00rootroot00000000000000from colorsys import hsv_to_rgb from time import sleep from sense_emu import SenseHat # Hues represent the spectrum of colors as values between 0 and 1. The range # is circular so 0 represents red, ~0.2 is yellow, ~0.33 is green, 0.5 is cyan, # ~0.66 is blue, ~0.84 is purple, and 1.0 is back to red. These are the initial # hues for each pixel in the display. hues = [ 0.00, 0.00, 0.06, 0.13, 0.20, 0.27, 0.34, 0.41, 0.00, 0.06, 0.13, 0.21, 0.28, 0.35, 0.42, 0.49, 0.07, 0.14, 0.21, 0.28, 0.35, 0.42, 0.50, 0.57, 0.15, 0.22, 0.29, 0.36, 0.43, 0.50, 0.57, 0.64, 0.22, 0.29, 0.36, 0.44, 0.51, 0.58, 0.65, 0.72, 0.30, 0.37, 0.44, 0.51, 0.58, 0.66, 0.73, 0.80, 0.38, 0.45, 0.52, 0.59, 0.66, 0.73, 0.80, 0.87, 0.45, 0.52, 0.60, 0.67, 0.74, 0.81, 0.88, 0.95, ] hat = SenseHat() def scale(v): return int(v * 255) while True: # Rotate the hues hues = [(h + 0.01) % 1.0 for h in hues] # Convert the hues to RGB values pixels = [hsv_to_rgb(h, 1.0, 1.0) for h in hues] # hsv_to_rgb returns 0..1 floats; convert to ints in the range 0..255 pixels = [(scale(r), scale(g), scale(b)) for r, g, b in pixels] # Update the display hat.set_pixels(pixels) sleep(0.04) python-sense-emu-1.2/sense_emu/gschemas.compiled000066400000000000000000000014541411441564000220730ustar00rootroot00000000000000GVariantX($XHx%L(,org.raspberrypi.sense_emu_gui( vdj vL}~ vuNv(nsn v vv va v %window-maximized(b)window-width(i) .path/org/raspberrypi/sense_emu_gui/sorientation-scalebalancecbalancecirclemodulo(s(yau))window-height(i)simulate-env(b)editor-commandpython3 -c "from idlelib.pyshell import main; main()"(s)screen-fpsr<(i(y(ii)))simulate-imu(b)python-sense-emu-1.2/sense_emu/gui.py000066400000000000000000001074511411441564000177250ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see import io import os import sys import atexit import struct import math import errno import shlex import subprocess import webbrowser import datetime as dt from time import time, sleep from threading import Thread, Lock, Event import gi gi.require_version('cairo', '1.0') gi.require_version('Gdk', '3.0') gi.require_version('GdkPixbuf', '2.0') gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GdkPixbuf, Gio, GLib, GObject, cairo import numpy as np import pkg_resources from . import __project__, __version__, __author__, __author_email__, __url__ from .i18n import init_i18n, _ from .screen import ScreenClient from .imu import IMUServer from .pressure import PressureServer from .humidity import HumidityServer from .stick import StickServer, SenseStick from .lock import EmulatorLock from .common import HEADER_REC, DATA_REC, DataRecord, slow_pi def main(): init_i18n() app = EmuApplication() app.run(sys.argv) def load_image(filename, format='png'): loader = GdkPixbuf.PixbufLoader.new_with_type(format) loader.write(pkg_resources.resource_string(__name__, filename)) loader.close() return loader.get_pixbuf() def load_ui(filename): return pkg_resources.resource_string(__name__, filename) class EmuApplication(Gtk.Application): def __init__(self, *args, **kwargs): super(EmuApplication, self).__init__( *args, application_id='org.raspberrypi.sense_emu_gui', flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, **kwargs) GLib.set_application_name(_('Sense HAT Emulator')) self.window = None def do_startup(self): # super-call needs to be in this form?! Gtk.Application.do_startup(self) # Get the emulator lock and terminate if something already has it self.lock = EmulatorLock('sense_emu_gui') try: self.lock.acquire() except: dialog = Gtk.MessageDialog( message_type=Gtk.MessageType.ERROR, title=_('Error'), text=_( 'Another process is currently acting as the Sense HAT ' 'emulator'), buttons=Gtk.ButtonsType.CLOSE) try: dialog.run() finally: dialog.destroy() self.quit() return def make_action(action_id, handler, param_type=None): action = Gio.SimpleAction.new(action_id, param_type) action.connect('activate', handler) self.add_action(action) make_action('example', self.on_example, GLib.VariantType.new('s')) make_action('play', self.on_play) make_action('prefs', self.on_prefs) make_action('help', self.on_help) make_action('about', self.on_about) make_action('quit', self.on_quit) builder = Gtk.Builder(translation_domain=__project__) builder.add_from_string( pkg_resources.resource_string(__name__, 'menu.ui').decode('utf-8')) self.set_menubar(builder.get_object('app-menu')) # Construct the open examples sub-menu for directory, label in [ # I18N: Easy examples ('basic', _('Simple')), # I18N: Intermediate skill examples ('intermediate', _('Intermediate')), # I18N: Difficult examples ('advanced', _('Advanced')), ]: examples = Gio.Menu.new() # NOTE: The use of literal "/" below is correct; resource paths # are not file-system paths and always use "/" for example in sorted(pkg_resources.resource_listdir( __name__, 'examples/{directory}'.format(directory=directory))): if example.endswith('.py'): examples.append( example.replace('_', '__'), Gio.Action.print_detailed_name( 'app.example', GLib.Variant.new_string( '{directory}/{example}'.format( directory=directory, example=example)))) builder.get_object('example-submenu').append_submenu(label, examples) # Construct the settings database and tweak initial value of # simulate-imu and simulate-env if we're running on a slow Pi, and the # user hasn't explicitly set a value yet if pkg_resources.resource_exists(__name__, 'gschemas.compiled'): source = Gio.SettingsSchemaSource.new_from_directory( os.path.dirname(pkg_resources.resource_filename(__name__, 'gschemas.compiled')), Gio.SettingsSchemaSource.get_default(), True) else: source = Gio.SettingsSchemaSource.get_default() schema = Gio.SettingsSchemaSource.lookup( source, self.props.application_id, False) assert schema is not None self.settings = Gio.Settings.new_full(schema, None, None) if self.settings.get_user_value('simulate-imu') is None: enable_simulators = not slow_pi() self.settings.set_boolean('simulate-imu', enable_simulators) self.settings.set_boolean('simulate-env', enable_simulators) # Construct the emulator servers self.imu = IMUServer(simulate_world=self.settings.get_boolean('simulate-imu')) self.pressure = PressureServer(simulate_noise=self.settings.get_boolean('simulate-env')) self.humidity = HumidityServer(simulate_noise=self.settings.get_boolean('simulate-env')) self.screen = ScreenClient() self.stick = StickServer() # Connect the settings to the components self.settings.connect('changed', self.settings_changed) def settings_changed(self, settings, key): if key == 'simulate-env': self.pressure.simulate_noise = settings.get_boolean(key) self.humidity.simulate_noise = settings.get_boolean(key) elif key == 'simulate-imu': self.imu.simulate_world = settings.get_boolean(key) elif key == 'orientation-scale': # Force the orientation sliders to redraw self.window.yaw_scale.queue_draw() self.window.pitch_scale.queue_draw() self.window.roll_scale.queue_draw() elif key == 'screen-fps': self.window.screen_widget.screen_update_delay = 1 / settings.get_int(key) def do_shutdown(self): if self.lock.mine: self.lock.release() if self.window: self.window.destroy() self.window = None self.stick.close() self.screen.close() self.humidity.close() self.pressure.close() self.imu.close() Gtk.Application.do_shutdown(self) def do_activate(self): if not self.window and self.lock.mine: self.window = MainWindow(application=self) # Force a read of settings specific to the main window self.settings_changed(self.settings, 'screen-fps') self.settings_changed(self.settings, 'orientation-scale') # Position the window according to the settings self.window.set_default_size( self.settings.get_int('window-width'), self.settings.get_int('window-height') ) if self.settings.get_boolean('window-maximized'): self.window.maximize() if self.window: self.window.present() def do_command_line(self, command_line): options = command_line.get_options_dict() # do stuff with switches self.activate() return 0 def on_help(self, action, param): local_help = '/usr/share/doc/python-sense-emu-doc/html/index.html' remote_help = 'https://sense-emu.readthedocs.io/' if os.path.exists(local_help): webbrowser.open('file://' + local_help) else: webbrowser.open(remote_help) def on_about(self, action, param): logo = load_image('sense_emu_gui.svg', format='svg') about_dialog = Gtk.AboutDialog( transient_for=self.window, authors=['{author} <{email}>'.format(author=__author__, email=__author_email__)], license_type=Gtk.License.GPL_2_0, logo=logo, version=__version__, website=__url__) about_dialog.run() about_dialog.destroy() def on_example(self, action, param): # NOTE: The use of a bare "/" below is correct: resource paths are # *not* file-system paths and always use "/" path separators filename = param.unpack() source = pkg_resources.resource_stream( __name__, '/'.join(('examples', filename))) # Write to a filename in the user's home-dir with the timestamp # appended to ensure uniqueness (ish) filename = os.path.splitext(os.path.basename(filename))[0] filename = '{filename}-{timestamp:%Y-%m-%d-%H-%M-%S}.py'.format( filename=filename, timestamp=dt.datetime.now()) filename = os.path.join(os.path.expanduser('~'), filename) target = io.open(filename, 'w', encoding='utf-8') # Write a note at the top of the file to explain things target.write("""\ # This file has been written to your home directory for convenience. It is # saved as "{filename}" """.format(filename=filename)) target.write(source.read().decode('utf-8')) cmd = self.settings.get_string('editor-command') try: cmd % 'foo' except TypeError: cmd = cmd + ' %s' subprocess.Popen(shlex.split(cmd % shlex.quote(filename))) def on_play(self, action, param): open_dialog = Gtk.FileChooserDialog( transient_for=self.window, title=_('Select the recording to play'), action=Gtk.FileChooserAction.OPEN) open_dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) open_dialog.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT) try: response = open_dialog.run() open_dialog.hide() if response == Gtk.ResponseType.ACCEPT: self.window.play(open_dialog.get_filename()) finally: open_dialog.destroy() def on_prefs(self, action, param): prefs_dialog = PrefsDialog( transient_for=self.window, title=_('Preferences'), settings=self.settings) try: prefs_dialog.run() finally: prefs_dialog.destroy() def on_quit(self, action, param): self.quit() class ScreenWidget(Gtk.DrawingArea): __gtype_name__ = 'ScreenWidget' def __init__(self, *args, **kwargs): super(ScreenWidget, self).__init__(*args, **kwargs) self.set_has_window(True) self.set_size_request(265, 265) # Load graphics assets self._board_full = load_image('sense_emu.png') self._board_scaled = self._board_full self._orient_full = load_image('orientation.png') self._orient_image = self._orient_full self._grid_full = load_image('pixel_grid.png') self._grid_scaled = self._grid_full # Set up a thread to constantly refresh the pixels from the screen # client object self.screen_update_delay = 0.04 self._size_lock = Lock() self._ratio = 1.0 self._rotation = 0 self._show_orientation = False self._draw_pending = Event() self._draw_image = None self._draw_timestamp = 0.0 self._stop = Event() self._update_thread = Thread(target=self._update_run) self._update_thread.daemon = True self.connect('realize', self.realized) self.connect('size-allocate', self.resized) self.connect('draw', self.drawn) def realized(self, widget): self._update_thread.start() def resized(self, widget, rect): with self._size_lock: if self._rotation in (0, 180): ratio = min( rect.width / self._board_full.props.width, rect.height / self._board_full.props.height) else: ratio = min( rect.width / self._board_full.props.height, rect.height / self._board_full.props.width) ratio = min(ratio, 1.0) # never resize larger than native if ratio != self._ratio: # Only resize if necessary (plenty of resizes wind up with the # same ratio) self._board_scaled = self._board_full.scale_simple( self._board_full.props.width * ratio, self._board_full.props.height * ratio, GdkPixbuf.InterpType.BILINEAR) self._grid_scaled = self._grid_full.scale_simple( self._grid_full.props.width * ratio, self._grid_full.props.height * ratio, GdkPixbuf.InterpType.BILINEAR) self._orient_scaled = self._orient_full.scale_simple( self._orient_full.props.width * ratio, self._orient_full.props.height * ratio, GdkPixbuf.InterpType.BILINEAR) self._ratio = ratio def drawn(self, widget, cr): if self._draw_image is None: return with self._size_lock: img = self._draw_image if self._show_orientation: img = img.copy() self._orient_scaled.composite( img, 0, 0, img.props.width, img.props.height, 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 215) img = img.rotate_simple(self._rotation) rect = self.get_allocation() Gdk.cairo_set_source_pixbuf(cr, img, (rect.width - img.props.width) // 2, (rect.height - img.props.height) // 2) cr.paint() self._draw_pending.clear() @GObject.Property(type=object) def client(self): return self._screen_client @client.setter def client(self, value): self._screen_client = value @GObject.Property(type=int, default=0) def rotation(self): return self._rotation @rotation.setter def rotation(self, value): self._rotation = value self.resized(self, self.get_allocation()) self._force_update() @GObject.Property(type=bool, default=False) def orientation(self): return self._show_orientation @orientation.setter def orientation(self, value): self._show_orientation = value self._force_update() def _force_update(self): self._draw_pending.clear() self._draw_timestamp = 0 # Wait for the background thread to update the pixels image (this # should never take more than a second) self._draw_pending.wait(1) self.props.window.invalidate_rect(None, False) def _update_run(self): # This method runs in the background _update_thread while True: # Only update the screen if do_draw's finished the last update; # this effectively serves to "drop frames" if the system's too # busy if self._draw_pending.wait(self.screen_update_delay): # The wait period above enforces the maximum update rate; if # a draw is still pending, wait on the stop event instead if self._stop.wait(self.screen_update_delay): break else: # Only update if the screen's modification timestamp indicates # that the data has changed since last time ts = self._screen_client.timestamp if ts > self._draw_timestamp: with self._size_lock: img = self._board_scaled.copy() pixels = GdkPixbuf.Pixbuf.new_from_bytes( GLib.Bytes.new(self._screen_client.rgb_array.tostring()), colorspace=GdkPixbuf.Colorspace.RGB, has_alpha=False, bits_per_sample=8, width=8, height=8, rowstride=8 * 3) pixel_rect = Gdk.Rectangle() pixel_rect.x = int(126 * self._ratio) pixel_rect.y = int(155 * self._ratio) pixel_rect.width = int(512 * self._ratio) pixel_rect.height = pixel_rect.width pixels.composite( img, pixel_rect.x, pixel_rect.y, pixel_rect.width, pixel_rect.height, pixel_rect.x, pixel_rect.y, # Why 8.1? With 8.0 (which is what it should be), # registration errors crop up at the far right (try # it and see); no idea why 8.1 is required to # correct them, but I'm too knackered to argue with # Gdk any more... pixel_rect.width / 8.1, pixel_rect.height / 8.1, GdkPixbuf.InterpType.NEAREST, 255) self._grid_scaled.composite( img, pixel_rect.x, pixel_rect.y, pixel_rect.width, pixel_rect.height, pixel_rect.x, pixel_rect.y, 1, 1, GdkPixbuf.InterpType.NEAREST, 255) self._draw_image = img self._draw_timestamp = ts self._draw_pending.set() # Schedule a redraw when the app is next idle; like Gtk # methods, Gdk methods must only be called from the main # thread (otherwise the app locks up) try: GLib.idle_add(self.props.window.invalidate_rect, None, False) except AttributeError: # Our Gdk window has been destroyed; don't whinge, just # exit the thread as we're obviously shutting down break def do_destroy(self): self._stop.set() self._update_thread.join() @Gtk.Template(string=load_ui('main_window.ui')) class MainWindow(Gtk.ApplicationWindow): __gtype_name__ = 'MainWindow' screen_box = Gtk.Template.Child() gyro_grid = Gtk.Template.Child() roll_scale = Gtk.Template.Child() pitch_scale = Gtk.Template.Child() yaw_scale = Gtk.Template.Child() roll = Gtk.Template.Child() pitch = Gtk.Template.Child() yaw = Gtk.Template.Child() environ_box = Gtk.Template.Child() humidity = Gtk.Template.Child() pressure = Gtk.Template.Child() temperature = Gtk.Template.Child() joystick_box = Gtk.Template.Child() left_button = Gtk.Template.Child() right_button = Gtk.Template.Child() up_button = Gtk.Template.Child() down_button = Gtk.Template.Child() enter_button = Gtk.Template.Child() screen_rotate_label = Gtk.Template.Child() screen_rotate_clockwise = Gtk.Template.Child() screen_rotate_anticlockwise = Gtk.Template.Child() play_box = Gtk.Template.Child() play_label = Gtk.Template.Child() play_progressbar = Gtk.Template.Child() def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) # Set the window icon icon = load_image('sense_emu_gui.png') self.props.icon = icon # Set up the objects for the playback thread self._play_update_lock = Lock() self._play_update_id = 0 self._play_event = Event() self._play_thread = None self._play_restore = (True, True, True) # Set up the custom screen widget self.screen_widget = ScreenWidget(visible=True, client=self.props.application.screen) self.screen_box.pack_start(self.screen_widget, True, True, 0) self.screen_widget.show() # Set initial positions on sliders (and add some marks) self.pitch_scale.add_mark(0, Gtk.PositionType.BOTTOM, None) self.roll_scale.add_mark(0, Gtk.PositionType.BOTTOM, None) self.yaw_scale.add_mark(0, Gtk.PositionType.BOTTOM, None) self.roll.props.value = self.props.application.imu.orientation[0] self.pitch.props.value = self.props.application.imu.orientation[1] self.yaw.props.value = self.props.application.imu.orientation[2] self.humidity.props.value = self.props.application.humidity.humidity self.pressure.props.value = self.props.application.pressure.pressure self.temperature.props.value = self.props.application.humidity.temperature # Set up attributes for the joystick buttons self._stick_held_lock = Lock() self._stick_held_id = 0 self.left_button.direction = SenseStick.KEY_LEFT self.right_button.direction = SenseStick.KEY_RIGHT self.up_button.direction = SenseStick.KEY_UP self.down_button.direction = SenseStick.KEY_DOWN self.enter_button.direction = SenseStick.KEY_ENTER self._stick_map = { Gdk.KEY_Return: self.enter_button, Gdk.KEY_Left: self.left_button, Gdk.KEY_Right: self.right_button, Gdk.KEY_Up: self.up_button, Gdk.KEY_Down: self.down_button, } # Set up attributes for the screen rotation controls self.screen_rotate_clockwise.angle = -90 self.screen_rotate_anticlockwise.angle = 90 self._stick_rotations = { SenseStick.KEY_LEFT: SenseStick.KEY_UP, SenseStick.KEY_UP: SenseStick.KEY_RIGHT, SenseStick.KEY_RIGHT: SenseStick.KEY_DOWN, SenseStick.KEY_DOWN: SenseStick.KEY_LEFT, SenseStick.KEY_ENTER: SenseStick.KEY_ENTER, } @Gtk.Template.Callback() def window_resized(self, widget, rect): if not self.is_maximized(): self.props.application.settings.set_int('window-width', rect.width) self.props.application.settings.set_int('window-height', rect.height) @Gtk.Template.Callback() def window_state_changed(self, widget, event): if event.type == Gdk.EventType.WINDOW_STATE: self.props.application.settings.set_boolean( 'window-maximized', event.new_window_state & Gdk.WindowState.MAXIMIZED) return False def do_destroy(self): try: self._play_stop() except AttributeError: # do_destroy gets called multiple times, and subsequent times lacks # the Python-added instance attributes pass Gtk.ApplicationWindow.do_destroy(self) @Gtk.Template.Callback() def format_pressure(self, scale, value): return '%.1fmbar' % value @Gtk.Template.Callback() def pressure_changed(self, adjustment): if not self._play_thread: self.props.application.pressure.set_values( self.pressure.props.value, self.temperature.props.value, ) @Gtk.Template.Callback() def format_humidity(self, scale, value): return '%.1f%%' % value @Gtk.Template.Callback() def humidity_changed(self, adjustment): if not self._play_thread: self.props.application.humidity.set_values( self.humidity.props.value, self.temperature.props.value, ) @Gtk.Template.Callback() def format_temperature(self, scale, value): return '%.1f°C' % value @Gtk.Template.Callback() def temperature_changed(self, adjustment): if not self._play_thread: self.pressure_changed(adjustment) self.humidity_changed(adjustment) @Gtk.Template.Callback() def format_orientation(self, scale, value): mode = self.props.application.settings.get_string('orientation-scale') return '%.1f°' % ( value if mode == 'balance' else value + 180 if mode == 'circle' else value % 360 if mode == 'modulo' else 999 # should never happen ) @Gtk.Template.Callback() def orientation_changed(self, adjustment): if not self._play_thread: self.props.application.imu.set_orientation(( self.roll.props.value, self.pitch.props.value, self.yaw.props.value, )) @Gtk.Template.Callback() def stick_key_pressed(self, button, event): try: button = self._stick_map[event.keyval] except KeyError: return False else: self.stick_pressed(button, event) return True @Gtk.Template.Callback() def stick_key_released(self, button, event): try: button = self._stick_map[event.keyval] except KeyError: return False else: self.stick_released(button, event) return True @Gtk.Template.Callback() def stick_pressed(self, button, event): # When a button is double-clicked, GTK fires two pressed events for the # second click with no intervening released event (so there's one # pressed event for the first click, followed by a released event, then # two pressed events for the second click followed by a single released # event). This isn't documented, so it could be a bug, but it seems # more like a deliberate behaviour. Anyway, we work around the # redundant press by detecting it with the non-zero stick_held_id and # ignoring the redundant event button.grab_focus() with self._stick_held_lock: if self._stick_held_id: return True self._stick_held_id = GLib.timeout_add(250, self.stick_held_first, button) self._stick_send(button.direction, SenseStick.STATE_PRESS) button.set_active(True) return True @Gtk.Template.Callback() def stick_released(self, button, event): with self._stick_held_lock: if self._stick_held_id: GLib.source_remove(self._stick_held_id) self._stick_held_id = 0 self._stick_send(button.direction, SenseStick.STATE_RELEASE) button.set_active(False) return True def stick_held_first(self, button): with self._stick_held_lock: self._stick_held_id = GLib.timeout_add(50, self.stick_held, button) self._stick_send(button.direction, SenseStick.STATE_HOLD) return False def stick_held(self, button): self._stick_send(button.direction, SenseStick.STATE_HOLD) return True def _stick_send(self, direction, action): tv_usec, tv_sec = math.modf(time()) tv_usec *= 1000000 r = self.screen_widget.props.rotation // 90 while r: direction = self._stick_rotations[direction] r -= 1 event_rec = struct.pack(SenseStick.EVENT_FORMAT, int(tv_sec), int(tv_usec), SenseStick.EV_KEY, direction, action) self.props.application.stick.send(event_rec) @Gtk.Template.Callback() def rotate_screen(self, button): self.screen_widget.props.rotation = (self.screen_widget.props.rotation + button.angle) % 360 self.screen_rotate_label.props.label = '%d°' % self.screen_widget.props.rotation @Gtk.Template.Callback() def toggle_orientation(self, button): self.screen_widget.props.orientation = not self.screen_widget.props.orientation def _play_run(self, f): err = None try: # Calculate how many records are in the file; we'll use this later # when updating the progress bar rec_total = (f.seek(0, io.SEEK_END) - HEADER_REC.size) // DATA_REC.size f.seek(0) skipped = 0 for rec, data in enumerate(self._play_source(f)): now = time() if data.timestamp < now: skipped += 1 continue else: if self._play_event.wait(data.timestamp - now): break self.props.application.pressure.set_values(data.pressure, data.ptemp) self.props.application.humidity.set_values(data.humidity, data.htemp) self.props.application.imu.set_imu_values( (data.ax, data.ay, data.az), (data.gx, data.gy, data.gz), (data.cx, data.cy, data.cz), (data.ox, data.oy, data.oz), ) # Again, would be better to use custom signals here but # attempting to do so just results in seemingly random # segfaults during playback with self._play_update_lock: if self._play_update_id == 0: self._play_update_id = GLib.idle_add(self._play_update_controls, rec / rec_total) except Exception as e: err = e finally: f.close() # Must ensure that controls are only re-enabled *after* all pending # control updates have run with self._play_update_lock: if self._play_update_id: GLib.source_remove(self._play_update_id) self._play_update_id = 0 # Get the main thread to re-enable the controls at the end of # playback GLib.idle_add(self._play_controls_finish, err) def _play_update_controls(self, fraction): with self._play_update_lock: self._play_update_id = 0 self.play_progressbar.props.fraction = fraction if not math.isnan(self.props.application.humidity.temperature): self.temperature.props.value = self.props.application.humidity.temperature if not math.isnan(self.props.application.pressure.pressure): self.pressure.props.value = self.props.application.pressure.pressure if not math.isnan(self.props.application.humidity.humidity): self.humidity.props.value = self.props.application.humidity.humidity self.yaw.props.value = math.degrees(self.props.application.imu.orientation[2]) self.pitch.props.value = math.degrees(self.props.application.imu.orientation[1]) self.roll.props.value = math.degrees(self.props.application.imu.orientation[0]) return False @Gtk.Template.Callback() def play_stop_clicked(self, button): self._play_stop() def _play_stop(self): if self._play_thread: self._play_event.set() self._play_thread.join() self._play_thread = None def _play_source(self, f): magic, ver, offset = HEADER_REC.unpack(f.read(HEADER_REC.size)) if magic != b'SENSEHAT': raise IOError(_('%s is not a Sense HAT recording') % f.name) if ver != 1: raise IOError(_('%s has unrecognized file version number') % f.name) offset = time() - offset while True: buf = f.read(DATA_REC.size) if not buf: break elif len(buf) < DATA_REC.size: raise IOError(_('Incomplete data record at end of %s') % f.name) else: data = DataRecord(*DATA_REC.unpack(buf)) yield data._replace(timestamp=data.timestamp + offset) def _play_controls_setup(self, filename): # Disable all the associated user controls while playing back self.environ_box.props.sensitive = False self.gyro_grid.props.sensitive = False # Disable simulation threads as we're going to manipulate the # values precisely self._play_restore = ( self.props.application.pressure.simulate_noise, self.props.application.humidity.simulate_noise, self.props.application.imu.simulate_world, ) self.props.application.pressure.simulate_noise = False self.props.application.humidity.simulate_noise = False self.props.application.imu.simulate_world = False # Show the playback bar self.play_label.props.label = _('Playing %s') % os.path.basename(filename) self.play_progressbar.props.fraction = 0.0 self.play_box.props.visible = True def _play_controls_finish(self, exc): # Reverse _play_controls_setup self.play_box.props.visible = False ( self.props.application.pressure.simulate_noise, self.props.application.humidity.simulate_noise, self.props.application.imu.simulate_world, ) = self._play_restore self.environ_box.props.sensitive = True self.gyro_grid.props.sensitive = True self._play_thread = None # If an exception occurred in the background thread, display the # error in an appropriate dialog if exc: dialog = Gtk.MessageDialog( transient_for=self, message_type=Gtk.MessageType.ERROR, title=_('Error'), text=_('Error while replaying recording'), buttons=Gtk.ButtonsType.CLOSE) dialog.format_secondary_text(str(exc)) dialog.run() dialog.destroy() def play(self, filename): self._play_stop() self._play_controls_setup(filename) self._play_thread = Thread(target=self._play_run, args=(io.open(filename, 'rb'),)) self._play_event.clear() self._play_thread.start() @Gtk.Template(string=load_ui('prefs_dialog.ui')) class PrefsDialog(Gtk.Dialog): __gtype_name__ = 'PrefsDialog' close_button = Gtk.Template.Child() env_check = Gtk.Template.Child() imu_check = Gtk.Template.Child() screen_fps = Gtk.Template.Child() editor_entry = Gtk.Template.Child() orientation_balance = Gtk.Template.Child() orientation_circle = Gtk.Template.Child() orientation_modulo = Gtk.Template.Child() def __init__(self, *args, **kwargs): self.settings = kwargs.pop('settings') super(PrefsDialog, self).__init__(*args, **kwargs) self.settings.bind( 'simulate-env', self.env_check, 'active', Gio.SettingsBindFlags.DEFAULT) self.settings.bind( 'simulate-imu', self.imu_check, 'active', Gio.SettingsBindFlags.DEFAULT) self.settings.bind( 'screen-fps', self.screen_fps, 'value', Gio.SettingsBindFlags.DEFAULT) self.settings.bind( 'editor-command', self.editor_entry, 'text', Gio.SettingsBindFlags.DEFAULT) self.orientation_balance.value = 'balance' self.orientation_circle.value = 'circle' self.orientation_modulo.value = 'modulo' s = self.settings.get_string('orientation-scale') for c in self.orientation_balance.get_group(): c.props.active = (c.value == s) @Gtk.Template.Callback() def close_clicked(self, button): self.response(Gtk.ResponseType.ACCEPT) @Gtk.Template.Callback() def orientation_changed(self, button): if button.props.active: self.settings.set_string('orientation-scale', button.value) python-sense-emu-1.2/sense_emu/humidity.py000066400000000000000000000154631411441564000207760ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import sys import os import io import mmap import errno from struct import Struct from collections import namedtuple from random import Random from time import time from threading import Thread, Event from math import isnan import numpy as np from .common import clamp # See HTS221 data-sheet for details of register values HUMIDITY_FACTOR = 256 TEMP_FACTOR = 64 HUMIDITY_DATA = Struct( '@' # native mode 'B' # humidity sensor type '6p' # humidity sensor name 'B' # H0 'B' # H1 'H' # T0 'H' # T1 'h' # H0_OUT 'h' # H1_OUT 'h' # T0_OUT 'h' # T1_OUT 'h' # H_OUT 'h' # T_OUT 'B' # H_VALID 'B' # T_VALID ) HumidityData = namedtuple('HumidityData', ( 'type', 'name', 'H0', 'H1', 'T0', 'T1', 'H0_OUT', 'H1_OUT', 'T0_OUT', 'T1_OUT', 'H_OUT', 'T_OUT', 'H_VALID', 'T_VALID') ) def humidity_filename(): """ Return the filename used to represent the state of the emulated sense HAT's humidity sensor. On UNIX we try ``/dev/shm`` then fall back to ``/tmp``; on Windows we use whatever ``%TEMP%`` contains """ fname = 'rpi-sense-emu-humidity' if sys.platform.startswith('win'): # just use a temporary file on Windows return os.path.join(os.environ['TEMP'], fname) else: if os.path.exists('/dev/shm'): return os.path.join('/dev/shm', fname) else: return os.path.join('/tmp', fname) def init_humidity(): """ Opens the file representing the state of the humidity sensor. The file-like object is returned. If the file already exists we simply make sure it is the right size. If the file does not already exist, it is created and zeroed. """ try: # Attempt to open the humidity sensor's file and ensure it's the right # size fd = io.open(humidity_filename(), 'r+b', buffering=0) fd.seek(HUMIDITY_DATA.size) fd.truncate() except IOError as e: # If the humidity device's file doesn't exist, create it with # reasonable initial values if e.errno == errno.ENOENT: fd = io.open(humidity_filename(), 'w+b', buffering=0) fd.write(b'\x00' * HUMIDITY_DATA.size) else: raise return fd class HumidityServer: def __init__(self, simulate_noise=True): self._random = Random() self._fd = init_humidity() self._map = mmap.mmap(self._fd.fileno(), 0, access=mmap.ACCESS_WRITE) data = self._read() if data.type != 2: self._write(HumidityData(2, b'HTS221', 0, 100, 0, 100, 0, 25600, 0, 6400, 0, 0, 0, 0)) self._humidity = 45.0 self._temperature = 20.0 else: self._humidity = data.H_OUT / HUMIDITY_FACTOR self._temperature = data.T_OUT / TEMP_FACTOR self._noise_thread = None self._noise_event = Event() self._noise_write() # The queue lengths are selected to accurately represent the response # time of the sensors self._humidities = np.full((10,), self._humidity, dtype=np.float) self._temperatures = np.full((31,), self._temperature, dtype=np.float) self.simulate_noise = simulate_noise def close(self): if self._fd: self.simulate_noise = False self._map.close() self._fd.close() self._fd = None self._map = None def _perturb(self, value, error): """ Return *value* perturbed by +/- *error* which is derived from a gaussian random generator. """ # We use an internal Random() instance here to avoid a threading issue # with the gaussian generator (could use locks, but an instance of # Random is easier and faster) return value + self._random.gauss(0, 0.2) * error def _read(self): return HumidityData(*HUMIDITY_DATA.unpack_from(self._map)) def _write(self, value): HUMIDITY_DATA.pack_into(self._map, 0, *value) @property def humidity(self): return self._humidity @property def temperature(self): return self._temperature def set_values(self, humidity, temperature): self._humidity = humidity self._temperature = temperature if not self._noise_thread: self._noise_write() @property def simulate_noise(self): return self._noise_thread is not None @simulate_noise.setter def simulate_noise(self, value): if value and not self._noise_thread: self._noise_event.clear() self._noise_thread = Thread(target=self._noise_loop) self._noise_thread.daemon = True self._noise_thread.start() elif self._noise_thread and not value: self._noise_event.set() self._noise_thread.join() self._noise_thread = None self._noise_write() def _noise_loop(self): while not self._noise_event.wait(0.13): self._noise_write() def _noise_write(self): if self.simulate_noise: self._humidities[1:] = self._humidities[:-1] self._humidities[0] = self._perturb(self.humidity, ( 3.5 if 20 <= self.humidity <= 80 else 5.0)) self._temperatures[1:] = self._temperatures[:-1] self._temperatures[0] = self._perturb(self.temperature, ( 0.5 if 15 <= self.temperature <= 40 else 1.0 if 0 <= self.temperature <= 60 else 2.0)) humidity = self._humidities.mean() temperature = self._temperatures.mean() else: humidity = self.humidity temperature = self.temperature self._write(self._read()._replace( H_VALID=not isnan(humidity), T_VALID=not isnan(temperature), H_OUT=0 if isnan(humidity) else int(clamp(humidity, 0, 100) * HUMIDITY_FACTOR), T_OUT=0 if isnan(temperature) else int(clamp(temperature, -40, 120) * TEMP_FACTOR), )) python-sense-emu-1.2/sense_emu/i18n.py000077500000000000000000000054131411441564000177160ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import sys import locale import gettext as _gettext import atexit import pkg_resources from . import __project__ def init_i18n(languages=None): # Ensure any resources we extract get cleaned up interpreter shutdown atexit.register(pkg_resources.cleanup_resources) # Figure out where the language catalogs are; this will extract them # if the package is frozen localedir = pkg_resources.resource_filename(__name__, 'locale') try: # Use the user's default locale instead of C locale.setlocale(locale.LC_ALL, '') except locale.Error as e: # If locale is not supported, use C which should at least provide # consistency. In this case, don't set a gettext domain to prevent # translation of strings locale.setlocale(locale.LC_ALL, 'C') else: # Set translation domain for GNU's gettext (needed by GTK's Builder) try: locale.bindtextdomain(__project__, localedir) locale.textdomain(__project__) except AttributeError: if sys.platform.startswith('win'): try: # We're on Windows; try and use intl.dll instead import ctypes libintl = ctypes.cdll.LoadLibrary('intl.dll') except OSError: # intl.dll isn't available; give up return else: libintl.bindtextdomain(__project__, localedir) libintl.textdomain(__project__) libintl.bind_textdomain_codeset(__project, 'UTF-8') else: # We're on something else (Mac OS X most likely); no idea what # to do here yet return # Finally, set translation domain for Python's built-in gettext _gettext.bindtextdomain(__project__, localedir) _gettext.textdomain(__project__) gettext = _gettext.gettext ngettext = _gettext.ngettext _ = gettext python-sense-emu-1.2/sense_emu/imu.py000066400000000000000000000272731411441564000177360ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import sys import os import io import mmap import time import errno import subprocess from random import Random from struct import Struct from collections import namedtuple from threading import Thread, Event import numpy as np from .common import clamp # See LSM9DS1 data-sheet for details of register values ACCEL_FACTOR = 4081.6327 GYRO_FACTOR = 57.142857 COMPASS_FACTOR = 7142.8571 ORIENT_FACTOR = 5214.1892 IMU_DATA = Struct( '@' # native mode 'B' # IMU sensor type '20p' # IMU sensor name 'Q' # timestamp 'hhh' # OUT_X_G, OUT_Y_G, OUT_Z_G 'hhh' # OUT_X_XL, OUT_Y_XL, OUT_Z_XL 'hhh' # OUT_X_M, OUT_Y_M, OUT_Z_M 'hhh' # Orientation X, Y, Z ) IMUData = namedtuple('IMUData', ( 'type', 'name', 'timestamp', 'accel', 'gyro', 'compass', 'orient')) def imu_filename(): """ Return the filename used to represent the state of the emulated sense HAT's IMU sensors. On UNIX we try ``/dev/shm`` then fall back to ``/tmp``; on Windows we use whatever ``%TEMP%`` contains. """ fname = 'rpi-sense-emu-imu' if sys.platform.startswith('win'): # just use a temporary file on Windows return os.path.join(os.environ['TEMP'], fname) else: if os.path.exists('/dev/shm'): return os.path.join('/dev/shm', fname) else: return os.path.join('/tmp', fname) def init_imu(): """ Opens the file representing the state of the IMU sensors. The file-like object is returned. If the file already exists we simply make sure it is the right size. If the file does not already exist, it is created and zeroed. """ try: # Attempt to open the IMU's device file and ensure it's the right size fd = io.open(imu_filename(), 'r+b', buffering=0) fd.seek(IMU_DATA.size) fd.truncate() except IOError as e: # If the IMU device's file doesn't exist, create it with reasonable # initial values if e.errno == errno.ENOENT: fd = io.open(imu_filename(), 'w+b', buffering=0) fd.write(b'\x00' * IMU_DATA.size) else: raise return fd # Find the best available time-source for the timestamp() function. The best # source will preferably be monotonic, and high-resolution try: _time = time.monotonic # 3.3+ (only guaranteed in 3.5+) except AttributeError: _time = time.perf_counter # 3.3+ def timestamp(): """ Returns a timestamp as an integer number of microseconds after some arbitrary basis (only comparisons of consecutive calls are meaningful). """ return int(_time() * 1000000) # Some handy array definitions V = lambda x, y, z: np.array((x, y, z)) O = V(0, 0, 0) X = V(1, 0, 0) Y = V(0, 1, 0) Z = V(0, 0, 1) class IMUServer: def __init__(self, simulate_world=True): self._random = Random() self._fd = init_imu() self._map = mmap.mmap(self._fd.fileno(), 0, access=mmap.ACCESS_WRITE) data = self._read() self._gravity = Z self._north = 0.33 * X if data.type != 6: self._write(IMUData(6, b'LSM9DS1', timestamp(), O, O, O, O)) self._accel = O self._gyro = O self._compass = O self._orientation = O self._position = O else: self._accel = data.accel / ACCEL_FACTOR self._gyro = data.gyro / GYRO_FACTOR self._compass = data.compass / COMPASS_FACTOR self._orientation = O # XXX calc orientation from accel and gravity self._position = O # XXX calc position from compass and north self._world_thread = None self._world_event = Event() self._world_iter = self._world_state() self._world_write() # These queue lengths were arbitrarily selected to smooth the action of # the orientation sliders in the GUI; they bear no particular relation # to the hardware self._gyros = np.full((10, 3), self._gyro, dtype=np.float) self._accels = np.full((10, 3), self._accel, dtype=np.float) self._comps = np.full((10, 3), self._compass, dtype=np.float) self.simulate_world = simulate_world def close(self): if self._fd: self.simulate_world = False self._map.close() self._fd.close() self._fd = None self._map = None def _read(self): ( type, name, timestamp, ax, ay, az, gx, gy, gz, cx, cy, cz, ox, oy, oz, ) = IMU_DATA.unpack_from(self._map) return IMUData( type, name, timestamp, V(ax, ay, az), V(gx, gy, gz), V(cx, cy, cz), V(ox, oy, oz), ) def _write(self, value): value = ( value.type, value.name, value.timestamp, value.accel[0], value.accel[1], value.accel[2], value.gyro[0], value.gyro[1], value.gyro[2], value.compass[0], value.compass[1], value.compass[2], value.orient[0], value.orient[1], value.orient[2], ) IMU_DATA.pack_into(self._map, 0, *value) def _perturb(self, value, error): """ Return *value* perturbed by +/- *error* which is derived from a gaussian random generator. """ # We use an internal Random() instance here to avoid a threading issue # with the gaussian generator (could use locks, but an instance of # Random is easier and faster) return V( value[0] + self._random.gauss(0, 0.2) * error, value[1] + self._random.gauss(0, 0.2) * error, value[2] + self._random.gauss(0, 0.2) * error, ) def set_orientation(self, orientation, position=None): if position is None: position = O self._orientation = V(*orientation) self._position = V(*position) if not self.simulate_world: self._world_write() def set_imu_values(self, accel, gyro, compass, orientation, position=None): assert not self.simulate_world self._accel = V(*accel) self._gyro = V(*gyro) self._compass = V(*compass) self._orientation = V(*orientation) if position is None: position = O self._position = V(*position) self._world_write(direct=True) @property def accel(self): return self._accel @property def gyro(self): return self._gyro @property def compass(self): return self._compass @property def orientation(self): return self._orientation @property def position(self): return self._position @property def simulate_world(self): return self._world_thread is not None @simulate_world.setter def simulate_world(self, value): if value and not self._world_thread: self._world_event.clear() self._world_thread = Thread(target=self._world_loop) self._world_thread.daemon = True self._world_thread.start() elif self._world_thread and not value: self._world_event.set() self._world_thread.join() self._world_thread = None self._world_write() def _world_state(self): """ An infinite generator which expects to be passed (position, orientation) states by the caller and yields (timestamp, accel, gyro, compass) states back. Used by either the simulation thread (if it's running), or by set_orientation (if it's not). """ then = timestamp() position = self._position orientation = self._orientation accel = gyro = compass = O while True: now = timestamp() new_position = self._position new_orientation = self._orientation time_delta = (now - then) / 1000000 if time_delta >= 0.016: # Gyro reading is simply the rate of change of the orientation gyro = (new_orientation - orientation) / time_delta # Construct a rotation matrix for the orientation; see # https://en.wikipedia.org/wiki/Euler_angles#Rotation_matrix x, y, z = np.deg2rad(new_orientation) c1, c2, c3 = np.cos((z, y, x)) s1, s2, s3 = np.sin((z, y, x)) R = np.array([ [c1 * c2, c1 * s2 * s3 - c3 * s1, s1 * s3 + c1 * c3 * s2], [c2 * s1, c1 * c3 + s1 * s2 * s3, c3 * s1 * s2 - c1 * s3], [-s2, c2 * s3, c2 * c3], ]) accel = R.T.dot(self._gravity) # transpose for passive rotation compass = R.T.dot(self._north) then = now position = new_position orientation = new_orientation # XXX Simulate acceleration from position yield now, accel, gyro, compass def _world_loop(self): while not self._world_event.wait(0.016): self._world_write() def _world_write(self, direct=False): if direct: now = timestamp() else: now, accel, gyro, compass = next(self._world_iter) if self.simulate_world: self._gyros[1:, :] = self._gyros[:-1, :] self._gyros[0, :] = self._perturb(gyro, 1.0) gyro = self._gyros.mean(axis=0) self._accels[1:, :] = self._accels[:-1, :] self._accels[0, :] = self._perturb(accel, 0.1) accel = self._accels.mean(axis=0) self._comps[1:, :] = self._comps[:-1, :] self._comps[0, :] = self._perturb(compass, 2.0) compass = self._comps.mean(axis=0) self._gyro = gyro self._accel = accel self._compass = compass orient = np.deg2rad(self._orientation) self._write(self._read()._replace( timestamp=now, accel=V( int(clamp(self._accel[0], -8, 8) * ACCEL_FACTOR), int(clamp(self._accel[1], -8, 8) * ACCEL_FACTOR), int(clamp(self._accel[2], -8, 8) * ACCEL_FACTOR), ), gyro=V( int(clamp(self._gyro[0], -500, 500) * GYRO_FACTOR), int(clamp(self._gyro[1], -500, 500) * GYRO_FACTOR), int(clamp(self._gyro[2], -500, 500) * GYRO_FACTOR), ), compass=V( int(clamp(self._compass[0], -4, 4) * COMPASS_FACTOR), int(clamp(self._compass[1], -4, 4) * COMPASS_FACTOR), int(clamp(self._compass[2], -4, 4) * COMPASS_FACTOR), ), orient=V( int(clamp(orient[0], -180, 180) * ORIENT_FACTOR), int(clamp(orient[1], -180, 180) * ORIENT_FACTOR), int(clamp(orient[2], -180, 180) * ORIENT_FACTOR), ) )) python-sense-emu-1.2/sense_emu/locale/000077500000000000000000000000001411441564000200165ustar00rootroot00000000000000python-sense-emu-1.2/sense_emu/locale/README000066400000000000000000000017051411441564000207010ustar00rootroot00000000000000To construct new translations, just use the standard gettext tools to create new .po files in this directory. For example, to create a translation for French: $ msginit -lfr_FR Fill out the .po file accordingly, then the Makefile will take care of construction of the .mo files during the package build. To update the .pot template when source has changed, use the main Makefile in the project root: $ make sense_emu/locales/sense-emu.pot To update an existing translation with updates from the .pot file, again, use the main Makefile: $ make sense_emu/locales/en_US.po To force updating everything (the .pot template, all .po files, and construction of .mo files for testing), use "i18n" target of the main Makefile: $ make i18n Note that .mo files shouldn't be stored in the repo (the .gitignore already excludes them), and likewise the .pot and .po files won't be included in package builds, just the .mo outputs (see MANIFEST.in for details). python-sense-emu-1.2/sense_emu/locale/en_US.po000066400000000000000000000307251411441564000213760ustar00rootroot00000000000000# English translations for PACKAGE package. # Copyright (C) 2016 THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Dave Jones , 2016. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-09-03 12:23+0100\n" "PO-Revision-Date: 2016-08-17 10:49+0100\n" "Last-Translator: Dave Jones \n" "Language-Team: English\n" "Language: en_US\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: sense_emu/dump.py:37 msgid "" "Converts a Sense HAT recording to CSV format, for the purposes of debugging " "or analysis." msgstr "" "Converts a Sense HAT recording to CSV format, for the purposes of debugging " "or analysis." #: sense_emu/dump.py:41 #, python-format msgid "" "the format to use when outputting the record timestamp (default: %(default)s)" msgstr "" "the format to use when outputting the record timestamp (default: %(default)s)" #: sense_emu/dump.py:45 msgid "if specified, output column headers" msgstr "if specified, output column headers" #: sense_emu/dump.py:50 sense_emu/play.py:44 msgid "Reading header" msgstr "Reading header" #: sense_emu/dump.py:53 sense_emu/play.py:47 msgid "Invalid magic number at start of input" msgstr "Invalid magic number at start of input" #: sense_emu/dump.py:55 sense_emu/play.py:49 #, python-format msgid "Unrecognized file version number (%d)" msgstr "Unrecognized file version number (%d)" #: sense_emu/dump.py:57 #, python-format msgid "Dumping recording taken at %s" msgstr "Dumping recording taken at %s" #: sense_emu/dump.py:65 sense_emu/play.py:59 msgid "Incomplete data record at end of file" msgstr "Incomplete data record at end of file" #: sense_emu/dump.py:91 #, python-format msgid "Converted %d records" msgstr "Converted %d records" #: sense_emu/gui.py:76 sense_emu/main_window.ui:77 msgid "Sense HAT Emulator" msgstr "Sense HAT Emulator" #: sense_emu/gui.py:90 sense_emu/gui.py:841 msgid "Error" msgstr "Error" #: sense_emu/gui.py:92 msgid "Another process is currently acting as the Sense HAT emulator" msgstr "Another process is currently acting as the Sense HAT emulator" #. I18N: Easy examples #: sense_emu/gui.py:121 msgid "Simple" msgstr "Simple" #. I18N: Intermediate skill examples #: sense_emu/gui.py:123 msgid "Intermediate" msgstr "Intermediate" #. I18N: Difficult examples #: sense_emu/gui.py:125 msgid "Advanced" msgstr "Advanced" #: sense_emu/gui.py:268 msgid "Select the recording to play" msgstr "Select the recording to play" #: sense_emu/gui.py:283 msgid "Preferences" msgstr "Preferences" #: sense_emu/gui.py:792 #, python-format msgid "%s is not a Sense HAT recording" msgstr "%s is not a Sense HAT recording" #: sense_emu/gui.py:794 #, python-format msgid "%s has unrecognized file version number" msgstr "%s has unrecognized file version number" #: sense_emu/gui.py:801 #, python-format msgid "Incomplete data record at end of %s" msgstr "Incomplete data record at end of %s" #: sense_emu/gui.py:821 #, python-format msgid "Playing %s" msgstr "Playing %s" #: sense_emu/gui.py:842 msgid "Error while replaying recording" msgstr "Error while replaying recording" #: sense_emu/play.py:39 msgid "" "Replays readings recorded from a Raspberry Pi Sense HAT, via the Sense HAT " "emulation library." msgstr "" "Replays readings recorded from a Raspberry Pi Sense HAT, via the Sense HAT " "emulation library." #: sense_emu/play.py:51 #, python-format msgid "Playing back recording taken at %s" msgstr "Playing back recording taken at %s" #: sense_emu/play.py:82 msgid "Skipping records to catch up" msgstr "Skipping records to catch up" #: sense_emu/play.py:96 #, python-format msgid "Skipped %d records during playback" msgstr "Skipped %d records during playback" #: sense_emu/play.py:97 #, python-format msgid "Finished playback of %d records" msgstr "Finished playback of %d records" #: sense_emu/record.py:35 msgid "" "Records the readings from a Raspberry Pi Sense HAT in real time, outputting " "the results to the specified file." msgstr "" "Records the readings from a Raspberry Pi Sense HAT in real time, outputting " "the results to the specified file." #: sense_emu/record.py:40 #, python-format msgid "the Sense HAT configuration file to use (default: %(default)s)" msgstr "the Sense HAT configuration file to use (default: %(default)s)" #: sense_emu/record.py:44 msgid "" "the duration to record for in seconds (default: record until terminated with " "Ctrl+C)" msgstr "" "the duration to record for in seconds (default: record until terminated with " "Ctrl+C)" #: sense_emu/record.py:49 msgid "" "the delay between each reading in seconds (default: the IMU polling " "interval, typically 0.003 seconds)" msgstr "" #: sense_emu/record.py:53 msgid "" "flush every record to disk immediately; reduces chances of truncated data on " "power loss, but greatly increases disk activity" msgstr "" "flush every record to disk immediately; reduces chances of truncated data on " "power loss, but greatly increases disk activity" #: sense_emu/record.py:62 msgid "" "unable to import RTIMU; ensure the Sense HAT library is correctly installed" msgstr "" "unable to import RTIMU; ensure the Sense HAT library is correctly installed" #: sense_emu/record.py:65 msgid "configuration filename must end with .ini" msgstr "configuration filename must end with .ini" #: sense_emu/record.py:67 #, python-format msgid "Reading settings from %s" msgstr "Reading settings from %s" #: sense_emu/record.py:69 msgid "Initializing sensors" msgstr "Initializing sensors" #: sense_emu/record.py:72 msgid "Failed to initialize Sense HAT IMU" msgstr "Failed to initialize Sense HAT IMU" #: sense_emu/record.py:75 msgid "Failed to initialize Sense HAT pressure sensor" msgstr "Failed to initialize Sense HAT pressure sensor" #: sense_emu/record.py:78 msgid "Failed to initialize Sense HAT humidity sensor" msgstr "Failed to initialize Sense HAT humidity sensor" #: sense_emu/record.py:83 msgid "Starting recording" msgstr "Starting recording" #: sense_emu/record.py:93 #, python-format msgid "%d records written" msgstr "%d records written" #: sense_emu/record.py:129 #, python-format msgid "Finishing recording after %d records" msgstr "Finishing recording after %d records" #: sense_emu/terminal.py:82 #, python-format msgid "argument \"-\" with mode %r" msgstr "argument \"-\" with mode %r" #: sense_emu/terminal.py:87 #, python-format msgid "can't open '%(name)s': %(error)s" msgstr "can't open '%(name)s': %(error)s" #: sense_emu/terminal.py:132 msgid "specify the configuration file to load" msgstr "specify the configuration file to load" #: sense_emu/terminal.py:138 msgid "produce less console output" msgstr "produce less console output" #: sense_emu/terminal.py:141 msgid "produce more console output" msgstr "produce more console output" #: sense_emu/terminal.py:144 msgid "log messages to the specified file" msgstr "log messages to the specified file" #: sense_emu/terminal.py:151 msgid "run under PDB (debug mode)" msgstr "run under PDB (debug mode)" #: sense_emu/terminal.py:184 #, python-format msgid "Reading configuration from %s" msgstr "Reading configuration from %s" #: sense_emu/terminal.py:197 #, python-format msgid "unable to locate [%s] section in configuration" msgstr "unable to locate [%s] section in configuration" #: sense_emu/terminal.py:232 msgid "Try the --help option for more information." msgstr "Try the --help option for more information." #. Title above the emulated Sense HAT screen #: sense_emu/main_window.ui:104 msgid "Screen" msgstr "Screen" #: sense_emu/main_window.ui:122 msgid "" "Rotate the Sense HAT 90 degrees clockwise; after rotation the joystick " "buttons will produce directions appropriate to the HAT's orientation" msgstr "" "Rotate the Sense HAT 90 degrees clockwise; after rotation the joystick " "buttons will produce directions appropriate to the HAT's orientation" #: sense_emu/main_window.ui:137 msgid "" "Rotate the Sense HAT 90 degrees counter-clockwise; after rotation the " "joystick buttons will produce directions appropriate to the HAT's orientation" msgstr "" "Rotate the Sense HAT 90 degrees counter-clockwise; after rotation the " "joystick buttons will produce directions appropriate to the HAT's orientation" #: sense_emu/main_window.ui:164 msgid "" "Click to toggle an overlay showing the positive direction of the yaw, pitch, " "and roll rotations" msgstr "" "Click to toggle an overlay showing the positive direction of the yaw, pitch, " "and roll rotations" #: sense_emu/main_window.ui:203 msgid "Temperature" msgstr "Temperature" #: sense_emu/main_window.ui:245 msgid "Pressure" msgstr "Pressure" #: sense_emu/main_window.ui:288 msgid "Humidity" msgstr "Humidity" #. Title above emulated Sense HAT joystick buttons #: sense_emu/main_window.ui:453 msgid "Joystick" msgstr "Joystick" #: sense_emu/main_window.ui:479 msgid "Pitch" msgstr "Pitch" #: sense_emu/main_window.ui:491 msgid "Roll" msgstr "Roll" #: sense_emu/main_window.ui:503 msgid "Yaw" msgstr "Yaw" #: sense_emu/main_window.ui:562 msgid "Orientation" msgstr "Orientation" #. Cancels playback of a recording #: sense_emu/main_window.ui:606 msgid "Stop" msgstr "Stop" #: sense_emu/menu.ui:5 msgid "_File" msgstr "_File" #: sense_emu/menu.ui:8 msgid "_Open example" msgstr "_Open example" #: sense_emu/menu.ui:12 msgid "_Replay recording..." msgstr "_Replay recording..." #: sense_emu/menu.ui:19 msgid "_Quit" msgstr "_Quit" #: sense_emu/menu.ui:25 msgid "_Edit" msgstr "_Edit" #: sense_emu/menu.ui:29 msgid "_Preferences..." msgstr "_Preferences..." #: sense_emu/menu.ui:34 msgid "_Help" msgstr "_Help" #: sense_emu/menu.ui:38 msgid "Contents" msgstr "Contents" #: sense_emu/menu.ui:44 msgid "_About..." msgstr "_About..." #: sense_emu/prefs_dialog.ui:93 msgid "Simulate" msgstr "Simulate" #: sense_emu/prefs_dialog.ui:106 msgid "Screen updates" msgstr "Screen updates" #: sense_emu/prefs_dialog.ui:116 msgid "Environment sensors" msgstr "Environment sensors" #: sense_emu/prefs_dialog.ui:120 msgid "" "When checked, the emulator will continually simulate \"noise\" on the " "environment sensors (the temperature, pressure, and humidity sliders)" msgstr "" "When checked, the emulator will continually simulate \"noise\" on the " "environment sensors (the temperature, pressure, and humidity sliders)" #: sense_emu/prefs_dialog.ui:130 msgid "Inertial measurement unit" msgstr "Inertial measurement unit" #: sense_emu/prefs_dialog.ui:134 msgid "" "When checked, the emulator will constantly simulate accelerometer (gravity " "induced), gyroscope (rate of change), and magnetometer (relative North) " "values based on the yaw, pitch, and roll sliders" msgstr "" "When checked, the emulator will constantly simulate accelerometer (gravity " "induced), gyroscope (rate of change), and magnetometer (relative North) " "values based on the yaw, pitch, and roll sliders" #: sense_emu/prefs_dialog.ui:146 msgid "" "Sets the maximum rate at which the emulated pixel display can update. The " "real HAT updates at 60fps but emulation at this speed can cause issues on " "slower Pi's" msgstr "" "Sets the maximum rate at which the emulated pixel display can update. The " "real HAT updates at 60fps but emulation at this speed can cause issues on " "slower Pi's" #: sense_emu/prefs_dialog.ui:166 msgid "fps" msgstr "fps" #: sense_emu/prefs_dialog.ui:185 msgid "Orientation scale" msgstr "Orientation scale" #: sense_emu/prefs_dialog.ui:200 msgid "" "When selected, the orientation sliders will have a minimum of -180°, a mid-" "point at 0°, and a maximum of 180°" msgstr "" "When selected, the orientation sliders will have a minimum of -180°, a mid-" "point at 0°, and a maximum of 180°" #: sense_emu/prefs_dialog.ui:216 msgid "" "When selected, the orientation sliders will have a minimum of 0°, a mid-" "point at 180°, and a maximum of 360°" msgstr "" "When selected, the orientation sliders will have a minimum of 0°, a mid-" "point at 180°, and a maximum of 360°" #: sense_emu/prefs_dialog.ui:233 msgid "" "When selected, the orientation sliders will have a minimum of 180°, a mid-" "point at 0° (immediately after 359°), and a maximum of 180°" msgstr "" "When selected, the orientation sliders will have a minimum of 180°, a mid-" "point at 0° (immediately after 359°), and a maximum of 180°" #: sense_emu/prefs_dialog.ui:248 msgid "Editor command" msgstr "" #: sense_emu/prefs_dialog.ui:261 #, python-format msgid "editor %s" msgstr "" #~ msgid "No such attribute %r" #~ msgstr "No such attribute %r" python-sense-emu-1.2/sense_emu/locale/sense-emu.pot000066400000000000000000000216201411441564000224440ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-09-03 12:23+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: sense_emu/dump.py:37 msgid "" "Converts a Sense HAT recording to CSV format, for the purposes of debugging " "or analysis." msgstr "" #: sense_emu/dump.py:41 #, python-format msgid "" "the format to use when outputting the record timestamp (default: %(default)s)" msgstr "" #: sense_emu/dump.py:45 msgid "if specified, output column headers" msgstr "" #: sense_emu/dump.py:50 sense_emu/play.py:44 msgid "Reading header" msgstr "" #: sense_emu/dump.py:53 sense_emu/play.py:47 msgid "Invalid magic number at start of input" msgstr "" #: sense_emu/dump.py:55 sense_emu/play.py:49 #, python-format msgid "Unrecognized file version number (%d)" msgstr "" #: sense_emu/dump.py:57 #, python-format msgid "Dumping recording taken at %s" msgstr "" #: sense_emu/dump.py:65 sense_emu/play.py:59 msgid "Incomplete data record at end of file" msgstr "" #: sense_emu/dump.py:91 #, python-format msgid "Converted %d records" msgstr "" #: sense_emu/gui.py:76 sense_emu/main_window.ui:77 msgid "Sense HAT Emulator" msgstr "" #: sense_emu/gui.py:90 sense_emu/gui.py:841 msgid "Error" msgstr "" #: sense_emu/gui.py:92 msgid "Another process is currently acting as the Sense HAT emulator" msgstr "" #. I18N: Easy examples #: sense_emu/gui.py:121 msgid "Simple" msgstr "" #. I18N: Intermediate skill examples #: sense_emu/gui.py:123 msgid "Intermediate" msgstr "" #. I18N: Difficult examples #: sense_emu/gui.py:125 msgid "Advanced" msgstr "" #: sense_emu/gui.py:268 msgid "Select the recording to play" msgstr "" #: sense_emu/gui.py:283 msgid "Preferences" msgstr "" #: sense_emu/gui.py:792 #, python-format msgid "%s is not a Sense HAT recording" msgstr "" #: sense_emu/gui.py:794 #, python-format msgid "%s has unrecognized file version number" msgstr "" #: sense_emu/gui.py:801 #, python-format msgid "Incomplete data record at end of %s" msgstr "" #: sense_emu/gui.py:821 #, python-format msgid "Playing %s" msgstr "" #: sense_emu/gui.py:842 msgid "Error while replaying recording" msgstr "" #: sense_emu/play.py:39 msgid "" "Replays readings recorded from a Raspberry Pi Sense HAT, via the Sense HAT " "emulation library." msgstr "" #: sense_emu/play.py:51 #, python-format msgid "Playing back recording taken at %s" msgstr "" #: sense_emu/play.py:82 msgid "Skipping records to catch up" msgstr "" #: sense_emu/play.py:96 #, python-format msgid "Skipped %d records during playback" msgstr "" #: sense_emu/play.py:97 #, python-format msgid "Finished playback of %d records" msgstr "" #: sense_emu/record.py:35 msgid "" "Records the readings from a Raspberry Pi Sense HAT in real time, outputting " "the results to the specified file." msgstr "" #: sense_emu/record.py:40 #, python-format msgid "the Sense HAT configuration file to use (default: %(default)s)" msgstr "" #: sense_emu/record.py:44 msgid "" "the duration to record for in seconds (default: record until terminated with " "Ctrl+C)" msgstr "" #: sense_emu/record.py:49 msgid "" "the delay between each reading in seconds (default: the IMU polling " "interval, typically 0.003 seconds)" msgstr "" #: sense_emu/record.py:53 msgid "" "flush every record to disk immediately; reduces chances of truncated data on " "power loss, but greatly increases disk activity" msgstr "" #: sense_emu/record.py:62 msgid "" "unable to import RTIMU; ensure the Sense HAT library is correctly installed" msgstr "" #: sense_emu/record.py:65 msgid "configuration filename must end with .ini" msgstr "" #: sense_emu/record.py:67 #, python-format msgid "Reading settings from %s" msgstr "" #: sense_emu/record.py:69 msgid "Initializing sensors" msgstr "" #: sense_emu/record.py:72 msgid "Failed to initialize Sense HAT IMU" msgstr "" #: sense_emu/record.py:75 msgid "Failed to initialize Sense HAT pressure sensor" msgstr "" #: sense_emu/record.py:78 msgid "Failed to initialize Sense HAT humidity sensor" msgstr "" #: sense_emu/record.py:83 msgid "Starting recording" msgstr "" #: sense_emu/record.py:93 #, python-format msgid "%d records written" msgstr "" #: sense_emu/record.py:129 #, python-format msgid "Finishing recording after %d records" msgstr "" #: sense_emu/terminal.py:82 #, python-format msgid "argument \"-\" with mode %r" msgstr "" #: sense_emu/terminal.py:87 #, python-format msgid "can't open '%(name)s': %(error)s" msgstr "" #: sense_emu/terminal.py:132 msgid "specify the configuration file to load" msgstr "" #: sense_emu/terminal.py:138 msgid "produce less console output" msgstr "" #: sense_emu/terminal.py:141 msgid "produce more console output" msgstr "" #: sense_emu/terminal.py:144 msgid "log messages to the specified file" msgstr "" #: sense_emu/terminal.py:151 msgid "run under PDB (debug mode)" msgstr "" #: sense_emu/terminal.py:184 #, python-format msgid "Reading configuration from %s" msgstr "" #: sense_emu/terminal.py:197 #, python-format msgid "unable to locate [%s] section in configuration" msgstr "" #: sense_emu/terminal.py:232 msgid "Try the --help option for more information." msgstr "" #. Title above the emulated Sense HAT screen #: sense_emu/main_window.ui:104 msgid "Screen" msgstr "" #: sense_emu/main_window.ui:122 msgid "" "Rotate the Sense HAT 90 degrees clockwise; after rotation the joystick " "buttons will produce directions appropriate to the HAT's orientation" msgstr "" #: sense_emu/main_window.ui:137 msgid "" "Rotate the Sense HAT 90 degrees counter-clockwise; after rotation the " "joystick buttons will produce directions appropriate to the HAT's orientation" msgstr "" #: sense_emu/main_window.ui:164 msgid "" "Click to toggle an overlay showing the positive direction of the yaw, pitch, " "and roll rotations" msgstr "" #: sense_emu/main_window.ui:203 msgid "Temperature" msgstr "" #: sense_emu/main_window.ui:245 msgid "Pressure" msgstr "" #: sense_emu/main_window.ui:288 msgid "Humidity" msgstr "" #. Title above emulated Sense HAT joystick buttons #: sense_emu/main_window.ui:453 msgid "Joystick" msgstr "" #: sense_emu/main_window.ui:479 msgid "Pitch" msgstr "" #: sense_emu/main_window.ui:491 msgid "Roll" msgstr "" #: sense_emu/main_window.ui:503 msgid "Yaw" msgstr "" #: sense_emu/main_window.ui:562 msgid "Orientation" msgstr "" #. Cancels playback of a recording #: sense_emu/main_window.ui:606 msgid "Stop" msgstr "" #: sense_emu/menu.ui:5 msgid "_File" msgstr "" #: sense_emu/menu.ui:8 msgid "_Open example" msgstr "" #: sense_emu/menu.ui:12 msgid "_Replay recording..." msgstr "" #: sense_emu/menu.ui:19 msgid "_Quit" msgstr "" #: sense_emu/menu.ui:25 msgid "_Edit" msgstr "" #: sense_emu/menu.ui:29 msgid "_Preferences..." msgstr "" #: sense_emu/menu.ui:34 msgid "_Help" msgstr "" #: sense_emu/menu.ui:38 msgid "Contents" msgstr "" #: sense_emu/menu.ui:44 msgid "_About..." msgstr "" #: sense_emu/prefs_dialog.ui:93 msgid "Simulate" msgstr "" #: sense_emu/prefs_dialog.ui:106 msgid "Screen updates" msgstr "" #: sense_emu/prefs_dialog.ui:116 msgid "Environment sensors" msgstr "" #: sense_emu/prefs_dialog.ui:120 msgid "" "When checked, the emulator will continually simulate \"noise\" on the " "environment sensors (the temperature, pressure, and humidity sliders)" msgstr "" #: sense_emu/prefs_dialog.ui:130 msgid "Inertial measurement unit" msgstr "" #: sense_emu/prefs_dialog.ui:134 msgid "" "When checked, the emulator will constantly simulate accelerometer (gravity " "induced), gyroscope (rate of change), and magnetometer (relative North) " "values based on the yaw, pitch, and roll sliders" msgstr "" #: sense_emu/prefs_dialog.ui:146 msgid "" "Sets the maximum rate at which the emulated pixel display can update. The " "real HAT updates at 60fps but emulation at this speed can cause issues on " "slower Pi's" msgstr "" #: sense_emu/prefs_dialog.ui:166 msgid "fps" msgstr "" #: sense_emu/prefs_dialog.ui:185 msgid "Orientation scale" msgstr "" #: sense_emu/prefs_dialog.ui:200 msgid "" "When selected, the orientation sliders will have a minimum of -180°, a mid-" "point at 0°, and a maximum of 180°" msgstr "" #: sense_emu/prefs_dialog.ui:216 msgid "" "When selected, the orientation sliders will have a minimum of 0°, a mid-" "point at 180°, and a maximum of 360°" msgstr "" #: sense_emu/prefs_dialog.ui:233 msgid "" "When selected, the orientation sliders will have a minimum of 180°, a mid-" "point at 0° (immediately after 359°), and a maximum of 180°" msgstr "" #: sense_emu/prefs_dialog.ui:248 msgid "Editor command" msgstr "" #: sense_emu/prefs_dialog.ui:261 #, python-format msgid "editor %s" msgstr "" python-sense-emu-1.2/sense_emu/lock.py000066400000000000000000000132541411441564000200660ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import sys import os import io import errno from time import time, sleep if sys.platform.startswith('win'): import ctypes kernel32 = ctypes.windll.kernel32 DWORD = ctypes.c_ulong PROCESS_QUERY_INFORMATION = 0x0400 ERROR_ACCESS_DENIED = 0x5 ERROR_INVALID_PARAMETER = 0x57 STILL_ACTIVE = 259 def pid_exists(pid): h = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid) try: if not h: if kernel32.GetLastError() == ERROR_ACCESS_DENIED: # If access is denied, there's obviously a process denying # access... return True elif kernel32.GetLastError() == ERROR_INVALID_PARAMETER: # Invalid parameter is no such process return False raise OSError('unable to get handle for pid %d' % pid) out = DWORD() if kernel32.GetExitCodeProcess(h, ctypes.byref(out)): return out.value == STILL_ACTIVE raise OSError('unable to query exit code for pid %d' % pid) finally: kernel32.CloseHandle(h) else: def pid_exists(pid): if pid == 0: return True try: os.kill(pid, 0) except OSError as e: if e.errno == errno.ESRCH: return False elif e.errno == errno.EPERM: return True else: raise else: return True def lock_filename(): """ Return the filename used as a lock-file by applications that can drive the emulation (currently sense_emu_gui and sense_play). On UNIX we try ``/dev/shm`` then fall back to ``/tmp``; on Windows we use whatever ``%TEMP%`` contains """ fname = 'rpi-sense-emu-pid' if sys.platform.startswith('win'): # just use a temporary file on Windows return os.path.join(os.environ['TEMP'], fname) else: if os.path.exists('/dev/shm'): return os.path.join('/dev/shm', fname) else: return os.path.join('/tmp', fname) class EmulatorLock: def __init__(self, name): self._filename = lock_filename() self.name = name # XXX not currently used def __enter__(self): self.acquire() return self def __exit__(self, exc_type, exc_value, exc_tb): self.release() def acquire(self, timeout=None): """ Acquire the emulator lock. This is expected to be called by anything wishing to drive the emulator's registers (sense_emu_gui and sense_play currently). """ if self._is_stale(): self._break_lock() self._write_pid() def release(self): """ Release the emulator lock (presumably after :meth:`acquire`). """ self._break_lock() def wait(self, timeout=None): """ Wait for a process to acquire the lock. This is expected to be called by anything wishing to read the registers and wanting to ensure there's something driving them (i.e. any consumer of SenseHat). Returns ``True`` if the lock was acquired before *timeout* seconds elapsed, or ``False`` otherwise. If *timeout* is ``None`` (the default) wait indefinitely. """ # XXX Either add a "launch" param to this method, or add a launch # method to the class so that consumers can use the lock to launch # an appropriate emulation end = time() if timeout is not None: end += timeout while not self._is_held() or self._is_stale(): if time() > end: return False sleep(0.1) return True @property def mine(self): """ Returns True if the current process holds the lock. """ return self._read_pid() == os.getpid() def _is_held(self): return os.path.exists(self._filename) def _is_stale(self): # True if the lock file exists, but the PID it references doesn't pid = self._read_pid() if pid is not None: return not pid_exists(pid) else: return False def _break_lock(self): # Unconditionally delete the file try: os.unlink(self._filename) except OSError as e: if e.errno != errno.ENOENT: raise def _read_pid(self): try: lockfile = io.open(self._filename, 'rb') except IOError: return None else: try: return int(lockfile.readline().decode('ascii').strip()) except ValueError: return None finally: lockfile.close() def _write_pid(self): with io.open(self._filename, 'x', encoding='ascii') as lockfile: lockfile.write('%d\n' % os.getpid()) python-sense-emu-1.2/sense_emu/main_window.ui000066400000000000000000000735331411441564000214440ustar00rootroot00000000000000 100 50 1 10 -180 180 1 15 260 1260 1000 1 100 -180 180 1 15 -30 105 25 1 10 -180 180 1 15 python-sense-emu-1.2/sense_emu/menu.ui000066400000000000000000000031641411441564000200660ustar00rootroot00000000000000 _File
_Open example app.play _Replay recording... <Primary>p
app.quit _Quit <Primary>q
_Edit
app.prefs _Preferences...
_Help
app.help Contents
app.about _About...
python-sense-emu-1.2/sense_emu/org.raspberrypi.sense_emu_gui.gschema.xml000066400000000000000000000066161411441564000266760ustar00rootroot00000000000000 'balance' The scale used to display orientation angles The scale used to display the yaw, pitch, and roll angles used to specify the Sense HAT's orientation. Internally these are always -180 to 180 degrees, but the interface can display alternate ranges such as 0 to 360 degrees. true Controls the environment simulation thread When enabled, the temperature, pressure, and humidity settings will be constantly perturbed with a gaussian error. This costs a certain amount of processor time, and is disabled by default on lower end Pi models. true Controls the IMU simulation thread When enabled, the orientation settings (yaw, pitch, and roll), along with the derived IMU settings (gyroscope, accelerometer, and compass) will be constantly perturbed with a gaussian error. This costs a certain amount of processor time, and is disabled by default on lower end Pi models. 25 Limits the screen update rate This is the maximum rate at which the screen will update. The actual HAT updates at 60fps, but this speed causes issues in the emulation on lower end Pi models, hence the default is 25fps (which is sufficient for most emulation purposes). -1 The width of the main window Stores the width of the main window between invocations. -1 The height of the main window Stores the height of the main window between invocations. false The maximized state of the main window Stores whether or not the main window is maximized. 'python3 -c "from idlelib.pyshell import main; main()"' The command to launch the script editor When an example script is opened from the menu, this is the command that is executed to open the script editor (which may also be the environment that runs the script in the case of applications like IDLE and Thonny). If %s appears in this string, the script filename will be substituted for it; otherwise the script filename will be appended to the command line. python-sense-emu-1.2/sense_emu/orientation.png000066400000000000000000001143421411441564000216250ustar00rootroot00000000000000PNG  IHDR'5CsBIT|d pHYs4}4} tEXtSoftwarewww.inkscape.org< IDATxwU?i)I/3I&Bi`) Y]]u՟k]]uwu]{A; H H!!eR'{`$sd~<<~Ϲ~̼IpyðL&3)N$&IȏK"Q5L^#ޔ|sɲ$IE>erE+^&j{Qv=#'F #bJDԥ\x:"I&wUK[RoÉW?,%gG@"bL5@=77g U81"'I8hK"LeDH2dm-\D>ү>[åĂχ6af-y$hʚH*E$"S/'B ؗsok|kso\ȷlo$"K2ɏ\)gą_3I?"lzd#lHl!dP(Q5ښ")r[GnۺI#Go?#[4z>N4^8&ɿ'複D6##;`x$#&m"iMm~!r͛:{s$|gUKV.'‰ o0:*"<$֎ڑT+k˵lmӪ57ENδd>_(_NLtʠLG"FĀBsꨨٺT֔B?[Eڴ<ۋM۔$ɗ*Z+kPK%8ê6$|*4'S3(*7DŠz+`OѺiU_m[&d2%NLb)|{1P9ArHcdj487D%Ѷ酂G>:˿sُUκNԿUsէHDt ?,*GLJ@/m/.֍M||Om,G=e 'J "H9J*u}LmYWhxI>^z?vNLxۄ\.$YIEUT<8*G >(Ѷqel_T:4G7**?>JX`;gZ3EqUԎQG-|k(P^\kz:Z~07-z^ '/k<12ˇUTEC#[;7>Km^GåLrђӟ'\>H11Q=G=$S?*"m |\R7nUӼ{3{2H&\>+IӢrHt$DEH۲.;M"yC̺Mnp"i]f+aFT Sm*opdM/Fv>O"9~M5.x9S{N\ƣW5㎎L͠ IeMT "ߺ˅$I2QS?32UWu.4;ku9SlUT5̌Tt3A&cgDΡmI6tubKSg#bI&fFW0Ienr|>52iON"#}XTwZڴ"|){;{o;'FuԀ~o9p`ة!G$~t;-: 'N"uчYjʗ$1?:{_c7W0}$ ""6m+n()K^zw9ϑF`^0R+1%QbDpb G;WVFn]S A#ExGɱ:b 32O)d* M)VEI>K=ê /N޶U2UG \uc#фM6]^hnp⴨H?>9");`$Dn'" #o ;'TlȞ8UԎL$1㹻N$C,41:q$*LQ>>]‰o{DDsꨨ{bH*vy'No`p]KB;]pb{fU%u\!p߽ceqZTx3Xm1|쀡T֔FTL!lxʎ;É|ۛ֎>CO$DDƝI&@Ov#Ύ_g""_1Јa$"I`%UWHMlA/xe7d-_u@kސψk8W2Y8la>i/8z@LM]L͠]߉|興L[GĄ'2G$T׶{.o~^zPKLMmIeL&2vT=U}FR=p׹L>?T 'ޑ$If"r)cI@_TL$1zIS쮹C>#3ȏ9!SI-{a@߰{D22D2dlEu}ɮLDl2a25xG)w9@dv3QsP8$v9QK|y[%T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'T 'Te""v@ߕM;^s)Eذp(LD5"ז^5@Gvs۶1r&kӪ2LZG)5?\xۢe)%\&[=R@a˲mxjS"bѺae$TTELFڴ"v񋖚+++;[UOEz*Le$U#TD_d"SQLeɦ]P/\:eo'\VA$C#;hTdkGGdҮ; ;_ `k5kU݌I$#bL ܷ#߲%6"SYiݏu$%Fe'F6^N"9&"vlȨ}x${@sd8_9~PĐ.l-j"dȟOTFuH*k_#g..lcƋ#$&Tcj(sN<yM=h7UmM"9~P>榨4&"@1NOnktWKG=|KsD_r(H8у5<}p[$qƎg QQI"`%aM4=xY1'\KdL.WNdR@FѲA#kM$'z-G';bC8C$e׭WU W=$I[#bˎkN%W-igw>hݞ^1Nv"kȵY =)wyRl&W‰D>`#R%R%R%R%R%R%R%R%R%R%R%R%R%R%R%R%R%R%R%R%R%R%RUvXqYC)8㽿XyC~! WtڇͽG?#GGIg߾=8ltc\.\h~=˱zԢ5+S#_?L&Ο@<״>'[44NtX}SgƨEǯ*Q =%㉕K_|ԩ=Yֺɓ%` WڱpQ_7l?cܐqCC8@(!$q}"#OLR5+Eg\.~<֢<9*2=^*[qb;)r@'5|ؼuqƴ{YC[yCp^y{s;y/c^|d[c[k @y 'U?~hik-8v܄cQ^s8rc[[/*NЫ^ظ>~ǒH#OouZ$Q;o5HOEp78`p![wi~Ï/8֖/݄44U5;mk׮_\.[ z݂U+OI06u]Z ɹl꽪ui1mDC Xjb[[Klln'V>-_h^<>Osc. }!# nj?(5./:5}!_d4Nx5nj|Kcꈆ.'$7%7%{~I|oƊ5]zc/)C4etSaA5cQd ZmZˏ}U\\ h1.^}x""bCuq91{Uؿe$^uoqGzk;?+2봸.n[4,Ǣ]V3fĻN8;L1w~?yzhں̆73m'"ĉvk~1acs>ݭJWtiޠq?8uf| p0)}]#D{*c^gܣkӳ@{"$""jk+m 7W?=#lZsmq͜ V8|8U߲GMΛ~b\y4= ڷvi >9gl#J5ԭ]GXuce`iO7Ge|v_3hh|Kolߺwq>ӿ7q6+t-?-y+ɓo"u1vuB51VXTo޿:.uzƗza;0=o̠_]MkVnX7=1YKjUYpKt2 IDAT뷖 9#auLqָFڥNt) ;ox9aRt ?amH\>OZ?}؍ߋWӾ7~?nz^lb<;>UYg7C\>λ;^/[ec\3x7?ǟ-'$>y%qڔ] zpT2Ν~O?`|.~WRϭ-OO=ԭ,q\bTN?gYp6!dwhTf+:7Ko(eզqc}Z>qۂhĔS)ܩ@﫩ocTNݼ1ㅍ󶵶ď5^6ėλ2B(zpTai^<؛:-H"$qᑧsSu賋K׭O+`؀Al&Gt11WivJb0,+DkӶ_h|kj6CGJ($$wvً{=+7w\ܚʪ(`ޡ<0~8nQ=q{k""7]SÖmfV摈Rp5X6alf?)8#M[7w7zDSC(vnߝ}S\?k=6.?8xԸ.ꈆ隸nݽX);\ykt-O>}ڝ7MMɳ.lR#/x5#7J= RsU⠑S.{CXfww|Xt`8I߉BÂU+≕yOԑ1b`]Tp)#UfXuƯ7N:#>vE1~.*[{1~b|Dk5>otލ͎Oh+w?G缻hߩn{gwfj0:HS<8eNJ_;61~R$E"ʪ}Kǽ<$+qHʮbnz^[Ïb]~EG?1t@m/VwUe+NܺWn?'>{O:w1'Ho?Vn\yE&[pkWO?\ nc:6 Q=GU^R8Qh; %N7ѕFbik͵/3^OO#ѵM]O1yx}/W7ƴ %ai|/s|ロJΩV.! Uɜۺ<ܒ9UQ_7, ?c-PhG^-m1gقXeS<²)w"׈Md_yK|k?b< ?}hU]I8#|8~{g~KcnX{; ,i#vx>{W޻c߉»'X9؃˞}Msy{skkw;U3:2?_̽3^X**lݖMsN?Um ?7'lkmͽW>{̠K>_=oK6M-`3#:(9q¡w^r81w3;wQWo3z꡸_eVw:_|- Cيl~YJ{9OpL^ظ.nyrn7l{ws'vwlAvIQ䋮}zz%)f@UM|ơ]eiӿ`xK~}Ixω-cE}p}eԌxp3=y'L<$~ؼ%濰4[2?n?g?w.|юBG3X r(t~vJFtCXcXjں9/7 .9K,SE]iחsؘ2Uӷi @4mdC+xOZͻ;\h,]Ԇa}GjlA\~l1vS(X߉ CFDuQ_Ƒ=ʭ(-Oe3JΝ8lt|;^WvÏ/\5uۯXQ]=Ywh$Æeos>'$>Enk[ٵX v(3"bf;,|Xq]!-zXgoY[;;k^^oI$J_%k_(SEݓh(I8@sE[ܰ6.?Ggu[6sp1aب5Xᅍcы]`GŌI;loT#ǟӹqB\0e `u1yx}{~[Ɗog⋷?@mc9o8s| ߉EkVY ̆It8f|Ɲt;oI" 4xɹx;5Ŝ09jwf/_p_M.OD'N/,џ;xx8x[>W?2VwY߿~#(3}Emo#"n~rNX Gd8vxe0ᐘ8lT?D!;qS-SU󦟴/R!0[]Ɗ_ 'SJu_bWmZK׭|ʈxGuxpzSS5YxgG3,8U_olܶ伷5nje$9ѸiNЧ 0X>rUXp $qȨWcwR4KTdU_]rN&Ig_ٌ/""mXcԝcnC˟-:<V bQv$7ϟ7<:䜩##O-SEb wn=ep>e-EO1SG4M9Yֵcx G0vi^Dtyށ7_K־PrO=7-SElӦL/:ޖoNЧZ*c'wk!'ϺxoV/ k;Ȋűy{s,uh{] FT[oo8aPMi畱*}όbHE]DڴNЧbs]ZgΛ?XmW*+G;f/ZqOlptywsc2U9}ꌒ7LpNЧ #"nX|5lxDgrt\Oǡ;6l_eٕ w s/>+7+9Ƕ֖.y וeIxg"}S|>\X@PvPn7?`;cyw#--1̆q'Ĕ]ykK;u7#-֚?M?x_7M|o):u_E.c1E}nqټQLmuL[ 6.* {yup>)z̙QSŤc⣯KkFe 5.^Bݼ1(x 5֭5$,k~6p)1mdᦧ$o?/7̕ƃK2UBg~7+x;o?>zwƇO??.=[Zcwk57ﺱG⭿7ۛ>oGθ0.?n;!_GD;K"'xb:cX7̥ܳ<ox{9N?)vіϕ2t Pㆌ(:>w3%cS׽3P↬δsahjׯǟ6]D]L{_`}V[._?RA.|xO_K_|ݳxG&=zyFS<"AxV8Qo?V4):>vp;2VǗ>ίNC6ĐcQqʓ^?#q}1sbPMK`qc]E[~߷d~ܷd~L^O88f4LQCbpQ<q%6⬯S74q]|>g*kΦ{朥 {恮5̹ds8[W쯎(S˞)S%=cxɯKfǟǍN>K8>cӶi>k֭Ʋ{6r^@vcŇN??*2ق:xV|(b7ED4+-c5=߀G'Nݼ讞8X@:93WDK[kyujkߚv}]pã32{X@y ?0+:e%m\ћvH"~bv-3N+Oʤ ]pi?&[ִwMDD,XUpbmq?ץي^?1>`+<ジyz2c]ю~״(/9Ήhik=vOo|"XTtnMeUח:]T*Nxh*(qGOcĞzax5sJ\%Ï/['袻=NaEǶl77{EoZWiv\6 .zqsS<3&E6+0x8b2Vs߰6~]EOtX vTWTFQe|'۰ז'O,c%}p_li#T @ )a9hXyCу~eor(@7,XcsT @y\rxDDc/SL/86߀T/EWM^L>";9&#$I"Ǻ-bUsŏotN@/o`ߺ)2A^\-mQ-eA#ǖ"76_0wzSjxq=y7'.;A5qx}xC蝿;xx|㢿~)VDOYec!kT @ytsbsbEǺVoFllV,[<s?SI&w=4^pȨ1a]>fB׹іϥT=a]'İtݪ2U: 7oo.S%=/dbfäc-m]ξ4Yt1nz^|I8x &R5U]ov=9േSSgħξn)}2-9޲|tXݯ ;͎;%]?r|^#:ix gwZJ8=,$>p8qё;?:;ٕ{lp"h;y|o\hx{\-K||gw蜷NvDhևN??߰6e|zlq۵ֶhLԯ*~QqÑSֶ^{ErK6w4.%[~͎^n}u7F {&ZΘbS> 5UӏfM{Z37,Xm{gݚ]g=9Wl{]\R OxzԐ.gɫnwxYً** UqQﷵ֪Zmpt:+REPPK$!aB:?$s>uy]ɹsDMr^1QԡZ0_ϱ=[jڶ}?U͞zGڴKw~^u^uN}NҶqRzuzo7c4v@ϱy<۟iFAa׫tSޒJmhּ?5Q_2I[B~N*ՏHul ҈i>ۼ.wJy睕:`\}d!:HMm1{/ 鳭B_oiD^͟ ѱ;ljqcZ,zx֍*}"PU'k<=٬1hɀ\D;Bzwj֦]͚T꼮zk{魖7*i/2מs~xoIu O\UD >ߣvh^IdN 9Ihp7>i ǯ _Z\VËhޤϴd@C/u[4$]yf1)ѥ__&<ӠGuU C~_-7ARKhbh2((Լ5-y+(p3of. u` ŢN#ͮ?]~zgr[b+E&mڜ~4ų'Hq&w6oO6y55{-ML^[$a}5M_4knF4΀6q&}k\Mym5!IOx;8Y4pme5UzeӊfWP\~e}hDX=5ﻺ_ViM&bWvLŢ~BáLڨVMHzisHfXil-%eQImڡRḺktky30wiB^5!I װMuPfӾdpaL.UAFyopʿpڟom&7~.vJԷoyVpi>D|7O4EI'VN<:smB Ar|3vMqJO-<+R]?M!ҝz}'N`ŪshsfٷꎅO;  E"nu6U3 vJu㋿SE]MH{weRU_)mVqPL9X!\$塴5jprm}^kd}?~4d$YV[evG|<oV 34}KǙKw w{\#U#T DFceKmtgҋ _c|S\iCe>OkڹFm[mg!cpe*[_4d</X8q(;[WlȢƞ1]~ΖG5y,_RsSD8@L=4b[. [&\|mu5:X~,@(/ֽSF+ig,ϚYɗjAbJ򪪾֐9Z Z [:i#:']_9WP\^h늎-%M6KyAڻg[ͮGդaYT1ÉzCh-Z=RQ1sPDȢ/9":oST~nQe1Z[i"Kl7j mٕjٵK]9bZV=;({ CVbVNL3 Wa9ow٣Th9r |w}J '$)'K*i延hbmI1q|褐:RUC;7SN}uu"0ʴcԼ*#K:?KZ~>_ >C[e\RVSMڵr:$L^ysN7dm/mHזٕ9gΒ-5"b41o)s)=uE;ԯKwSj'E䞃.hfy7y4 wҙܲRu5>۲;~#CgӁ=~:߈f'Izjٶn#]m9}Ga Qף}NJP[jW!m=\vvBw[0w@;WVS#Ul]_ܚ9p }{>ow :NIuvjdb@uKZ7zs~+4>Գ-#±q  TP\m%Ul4|.4MVR&rj/#Xo8XG y^\L׏=ꄁ9O+X3;t 𒗛\[j|f}#}ܱ1 PIw9#<ܡY\Dwb]5rZ*FK\8p2R+;;"Xg}ը5^mEs~NTol,V7ஹ~W#FKo2eX%uW4[o]8pvժ}7ݥm4z닕]zt. +%6% vMM0`7TYMU*auk,洭nGRuտt[I{@8@U5Msif1Nndl]gJOH^"Te5UuA]s$U?>ٲ[m!u{{>ÕeͪAuB3'0+C'*1&gjgm/)•O]kO,^xD\5OξIjtOE"h]>޵5YtNzWvZ?7d4>Ԕ^ӆBoZk.<^Z|²Jcն<8TqLoQp21j7w=4wM3?g9_}?YGh{ cՏf 瞷is.WqIRuCc䊚fR@e_PE Ղ۾~N]#(rrSh@f-*Xۤ%kh|uT$GŨYz+;@yixn WSuH L7|rR}x.I{moSܿnmc7lG+'D"h.{Å$ K :Z]gD^v o^qÉ-߽3 Μ$NK.]OBt~se+;;ꀇ`Jc5/+>ضob!#XQxX-M=T/]S=:(,vؤU gD^n9bT4.` v́3_1bj ct䎇;54;p{FRhG[lл82:tWx_@l:[l;JJՄ庥k5##dKجV~mmz,%6A;;j{U+Vsew?\vC$IGk'F8@+Ocb+Nq ;;ԣCI%"S!nD.+%gYl?J/]S0':Ȑa'h%\BЃٷ*:Ȼfnӈn]UnW*huf։D⢢5o}[qD0.*jT]E8@+QV[^O~cs9PE=0ZM5$`?u{lX}֠qꖒjKOH/ևwFϼV]5QVM'{ߤAaXtϹW겡}v}cck)=ngFC_|>7c,%Ʒhj‰p 0&I{(lT>ح6=<&]=ꬠ}Wnmx˧3{F Ţ齇WݥWo.4^v͐9o"<'h\norAo@A#JϹ] +O< jb]?;[a#f%뭛Гs 5\#B.[?x/*,+ wlN?{f4D=}55tnUP}p{=g31of;'=_ϺQiYaG#,'hu UENj^^|w4r{!Iku ڗG9W(e؜}3jɳ9s"<'hŎT/.{'ܡƝ/ZG߭ɝm =%s{F[ԅDŽQ]od+_Tσ\_R٬TI+'T IJ2OҀ>wtʑnӻ[hpڹY{Qj\bKm:w"6_{8("[Ql]d 99]4oT&C{NE:)Qi &bQ~97fޭ΂?*@Ǵn3\d Ekz<DZJS%DhHv~K فziG## @$x^-۹Y5?/fkΰjpA<T>]W?\wxȇy_>yW-zV&- h=tV/;+7>ܱQ^=9 y ėG/Vup"'m9W+.m 1(M9HsOQ#JJJ!e颁cC!I뵿jj(Ry]wzfh+;[h`Vx ;Ma#!# @?7TNj;gXG#RZBW 3uۤY7פpzggZ׀rShb-j SX7?_/ޢU>nGD8F39^Z<Մ=vMC骑5pEEp14ևuKuys54;w׻z7uϨ&LxM.HL%C@Q.=>?W٪y ;_iNL_){k+-[;7i ɚ?Pǜ9},ۥc5FpDNlD5rsU:puKIEM?\sMvݶZss2ZquVao#ZYHM/ˆNԄϻhW7}=ǚkg۫șPҜ>͟,#}ezyIhXUZ[CkkGA.=Y9]O%1VeMD4XY/\e$s蝭ºz0T]6t b_"&]׌:KMHѱ]Y_ݥJk*TYWUרd{6$e%*+)UIN鬴n Umc]DϬ~_Ux ^y3+n)iMz^qqvҁR9cRrLd8p;ŗOܭÕea # @kǝyç(hp9TVӺ.->YOͻSr.I<^?rn٥ g9a ΜFuF}@*k+klͪkpr/ש] QAQߌnMWq)ݤV镋TUÁaD8h.mؾAi:PM=T)5.>}~h>6ݤy]Nhz|nu,:/M@8*mZ]m1qJSJlc˥եFUۭ矣FջuBj*}Jk*`PGŨ[JS:ըFUx<^o|KnڎVWx|G8ZVU:P^*I~ u)9,N4k{~m/y~&^&](alGЁ==-c*9J '‰pneO?keuk1vTTTTTTT l+m0N'?ޓbb%]h_x(Sg0NCK&]hCLE8LE8LE8LE8LE8LE8LE8LE8LE8Le7о>r\B.~}x4$ʱrE4D0NCu9Z+m0000* IDAT00000000000TeLFX h+'8{Uec1./+{fk=m}0b,mogSNSNɫׯ' xObH!0Eff9 ɿ%?YR>أLp _}ndb5z:s[bh(.:&[\4^$bHA񓲅z}WSst!YX蘢]n[]n%iW[lQrW&+5o6IźL0Y+gnyJzwzu묗\cd9OlBH5WG%xÎ,th qaYSΊa\j< եlq ,^E7Cw:E-)* Vz 6k_ՠC婫ץ^aI$՝x,"2t^oclTTz;tTMsMNlC.PŝfMQtf0:T3$=kZ^_uFO$gwu,Aȯd:92~hNtkn˴x-Y(%\EVYUI]-L4Zo^jKPT@h{Wѿo}{9܏㮝&dQ \KN.qIHl r5*R&rW=!W-zc׾sgx=eƝ#g6rs>rVȖӺY蔣1F0ОyruCY1"ʾG!$OC\< ڟ3WNXr~̎UdYc$kG8_*-JG,Y?킻d/="s_=?jeןZdM6-{9 >;@`OM=(.vI_:̫ ^YlmU}&@dY? """""""RQeUfezy9i. r֪<^Iҵ}ar5- qNmb% """"""""""""""ڝ{2RLv ]?ppppppppppppppp2&cLPOh;.v g5kdz>pmH]h#x;ppppppppnv}r5w6EZ8b. rP?}wX$iúmy&WZ;uC(,}z@8 ezOB@A8LE8LE8LE8LE8LE8LE8LE8LE8LE8LE8LE8LE8LE8 ~Ɵ 8bp@ !NJOHVj|bXGE+>:VdyW*>3C9rN .\jSy]J+TV[r4֛NR+rS33-Ky(31U)J .TW#*,+Qa/QaY+oX!JOHy5OC_J7C9I͐zVPBP[vpKb& ѭ2R.)dѱOcs|JkkվmZSMʎX!h )ժ94 M^].P:($HUVޢoЪ}tLpvjhT>7R.)b$hΰɚ3l Z{]NNft1JOH6%FAcuѠ_M˵٥'ho:K"%ǢE&U$%QgQgi=ZiݺZ ;4gd]7uKI \5*,+ё*<*<ҚJU^ unW!Xѱrw(>:F )JKTZBⓕNghvf.3>?Rmc@NFD3lnp$:ֶ#UP\]+2j2UIo$f(sw顁Y9Ͱiь9eL=C=CU26h: Z9ŪˇMm/4ZKm>G[UAqQpRj*a٬VJ!]4&UZ|HםS.ֵgo?>ii'he֠n}g>[By^c!VvGR-h{m/9VHzuո~sּ?mcuWi)/kվ/ G8@+O+FLbmnGkm{_7*}x.=rYԡakw!.=-UbtsAqD5y/* U3NʌG\rsϕ^r@lZ_ӱJ ڕ_F\閰j;_;_V\T.0FsM֐yM~#5{/οbapVj։ɳZqkzq2+ Ct۔jʦze 9&kyd.T}+:pV 5.Qfߤ;e%*.Sumz䊷ufYJ Y54;_/x\G+\-&떒_CfzS/oHO\l֍Z/SϬ~_9]7;/b`f^}g^r ̕4-`>z~!^y斕:著7KL4CuC"ϴ`r{C:43)U]cM x'0ɐyz+#!/wV?}l/0@Ymy/a'ޡ9\ &IП}OI!l+p{=zr[ZkaѶ#5Gvmw艹whPVn ",5.QoIu~ %:')3a)z0Jdu503'lsHUJkӽ[VGoW~_<n֜kDfS҃]o獿.<^{C<͍?;a4_~]_鷗ެ齇?^v>ꝍ-mD.ҐyA-پQ-s6{ ɺ~9p0[meBKuPUW^/j{nwR#T ufɝ{^3nH=ۏЂ Gt=k_W^L1ip9?o] o]=,=͈@{@8@]5rZlHkvDIݪOn5t N|S1tˇNx8!I{J+;\ϬAؚ@0N~]mop9OE*1C=}-%M#`E}G"<E{C.Puk\8pL +"T9ǵxjh'~ J`Jk*p^]eX/p0IS$+4[̱.mv-`(p0K}ϱcg.2Ѩ`⢀)D8@$l?ph z֍fYFClU@@8@:4Q<^߶XG%FǪ_kpuh aE qlxk+ϯ9ͨ`G/V p0ip9wKIU#AcO{l1J36qD>WlTǝԣSzp0{X^ߺ775Ow}7;4y) ϕF'?-+` N&:\Q}t>kBcKoQ-WvZ֤3 oF7.7|.`ű@{@8@m/9෭[Jw)IZ,zn24.5ᘟpbx^uކh@ya%fmq@3'5zbw&ҤAǻb4#2\E]Jkyfͮˆ3c5u9vذy֨W,ݱ9"uОNF 襃'!uۤ CoFa꒘bTyx`ox:ŵ.e>V#$Fpư@{C8@}۬V7,JY3lQjizr-#;禎mhMq䋕@yF8@gϼSeZRRX,ٱQ u>ۆeԂyCI0ӫ7ݧTuzw&T#5g5F8@WVK6^zBbxFnJWns8}wJו#͛o.)` -=Ę8d?kBnZ:hV h2ףsac^9blo}gEd%lY:0gj*RU}Vc╝ҳk%|fן.wJU k-W _] (ikE蔡a=5 MAY!5덿1vZd//Ncs_X^Z4luN!?_o; "bXF[s~հQr΅OjQ˜fYk֠q=j BkϱÆwI-#z^\5G-(-eٷ}z{rhoj# B{d6^RL7Ұº#{ؐ꽂0YIz뜾#]WCO}Nj@L"`XvO`lǽr4eeݺ!uڻ:Ȓ,*eYI iP$1)w|0iIH`(!Bzu,8dKV$d#[Nu~7z"1e~k==Fwjv&!A.Y. rr;wg[nz?M7/4'Hܙ7*KU9Uz.N /W^Ҳy:T^ť* 'W>˜7=j;z~۱QIs}+t{ϗDžhҍܡޤ SМ |.f&mO~kDҠ- y9򬧾H8 o~J}kn{oٿ/ɩ|s$ʹ%ufg$ud 1QQX2OߧO?]\Td$ɹK>uw-=\[9ѧ|Wu-)H@f9@T];&§N9'ekM7}=Z?3<4'Hl@?pEu\Z$eM'v6D8շOOq22 ]i72~sRtz؝jV֤+ZlzA8|Y3Ҳ9YŹ3ەf[ZŸ~S-]Ūmo36* @s9nN>>.[~ze8E[; 038C_eiݯ_Zh$ũdќ ɢnLjVw i1_t8w;Uh}E%s ,*֖&@s4jޯZ{ۨ@m"%,*SEa(7r]q3GHP4zoMp49 kkKCܷcT_t`Ȑ-A/IDATViv˻6Swl[kvoMm 4'8Jy֪]M]ZW5qR[7h?ʕ1fI7giL&W=>G궧֯j^K538C5mtG8ꌵtH+ hN⃘ mtE:bɰڋ;ҽ=D hNYZVU VՖ4 ]ҊH+ hN⃘@@N*_e*YYٚ!uO*U{o2{==6JF,b4'`KJ?ЌtǑ$UߴN d #q]#9Es >I}zy bqV-1$I_=u9ҊH+ hN0&#Y:nN* JrFUw_>CjЦ:طW'G`yź'󕗝3gbQmiޭw?nZmM H @tLYt:o 9$A@Ukj}M ]hN|~s/gs|&22:c:}qz%ꏆ.hN rrOϫNFF-?S˫u÷#id@Ҙjqi\5Ok>9@Wٕ'ꋄ(Fь|kR~ɍߗg2 2ًNԅǿs;ŽnjۛUѢP߸ZXVN0=ќ cQ5uQ}/Yc'[Y5 =hNa❐QWW8K 猪oiޣfF1Sce J4'0nϿB'] /Z;=:ߪ=a>֛:>㨼XeZ6{V_s,?1ּ L@lNXٓfg*K+L'#~IQIIK8:=Mt ;^5L71p$I-05﮿u;;5dqd|Y7:Pp3jK֚> 9EZ zv$iN1zkomcHz#!}?ޮtG0FNn yֻEZ$m9H7 ?Iუ` Ǣ;~۩67Nk`$_^ٰkcͭoZU>%iZ93L ްCȑt󃯭 ף==ZliۺJ{ǟW&'+whiA3rKYp͂9Z`ւtg IgvGRAW'(7o~ͽ)Y=_{vr`2F%K2JoZm=y5'+WTgذ7'l_VC6 HC<No%cNYNPeafO};e^ݬWj7iwGkbBI+/Dnチ֮Z]UT?&EW-*vFt`f/Iq0X7<1N@1Xqȏ Z;!>>"O2/P"#G_q~0T^Uy:UvE SۻO=6XmS/Gs ŎTQ760%9~e[7dcÚmWإWW'GKϭk̿v~/XҵΧ1 #x2{ʫ+7BYMy8`*" 5rCcM间6ȐzrIs3(dH;u69j`цӑ Ҍ?[OUtvEnr$-52KL'kʎ;+Y`*>mW4>ԸOR@ w-ǧ@1 .\%yƟ\p: z^Y`}_1묵 &>$pp*?Qw}F{dG a#dɗW*'@vxt煺aeߐ$$Ykta_@=X\YC޶a׎qmNOC)ԥ)S^PYe鎑Tښ՞TOFt)iTs:mN=PjuUy:%7*#rR7wyc$mO?` qP5;J30G*Ӓ`zs{ZtpvSWS* n^p49Q,}NR!OA<0y>yCKM/ \ 6'ߵ>jyVν1qIZ_pX=:4$ b KwVW$9WP ҲtubQcm]3/IzUǽ bi4xmyڳ՜\]yU''T7l쭽܃?Pw]0X)TlwfI9BX߽x8] 7qEu7_՜&قƘJ^G `rB}zf]|i  e~KDHFsdyGgȹȂ$'7Gܓ 4t;O?2U8 @:jecᡥ愤z :1 lKr'-Q,?C95SsMLu6Qx_!5gWwsGqߜE>č*ں5a@rv=~+Ӑ$ReOկzc6'tEj$ -$X>Ld5IU2"$?;G%sjU円08bP"=Cm瞶g}̄: uZx==-)KlWe;YƌE &W8K?=[ TN K9qM뒘$ 7l٘[1Ƅ4$ V/7Pziܨ$Li_XziPޢG|.1XލrڇW'#$ԮZ5UQ{Lƥ/7~w"n,QXpc#T@G#~͡~Q?Hzhh٧psǸ `pvmhؑ(Piݾ/i$g`r+NE$2Ttۊ픍G[cB}4>Ը%yRݷ(~.xiysU)_ ּPb4ufy\ںS+-_{8Skn4''8Syϕ8~#cb=-u?֬}VYYoQR$[Z?EYzEi^NNWW%ǛcYT_0O&LqBN6RsbdXӺ$}lIaa9?45I?:a$'{|e 6$Sn"79 I h,z[CR1)ќEe1Ůэ*5?Ш-))| w'E^/?Xݎq;ow{kR-r*wT~r#';_`LN|9vl,"/)/-~sszkJKZhsNlN p“<㭖ե>rru̻/c?֓ܘl_6//'//铍dc _@ql ^Lu} Pޖqcn|8jE9:Z{y@12/{^y'vݵ%݁Qۜzuug3IE@tXmu^{iǽ;v;T"LD image/svg+xml Yaw Roll Pitch python-sense-emu-1.2/sense_emu/pixel_grid.png000066400000000000000000000520201411441564000214120ustar00rootroot00000000000000PNG  IHDRxbKGD pHYs4}4} tIME StEXtCommentCreated with GIMPW IDATxiս?o-=ݳ0l5 Q1DQ\I^ch57Q51F%F *. ;80 0Lﵝ455~ċs}tTPiEGyǢ毘z66y(2TmwOU2Y2_~( #>zx}p`}s kyE_-j%iď}W֑=_|jjYo|ڔáa׶75˾g*ߣ^{D9v5Nm?쏆-*v;t'6kk|~^U_Qhլ7.0뵜H]7 hgsc;y`/tϾtٿ)M u9Gv6ԬvW2c.Σ6,t(陫(~[/}͏'j/^{ݹ)N;BVm>^n8S3ujRW);QAohJ5#o^\1ڻB?ܭ[;^9˶V{LSKOVۑW=[&ct ~#C %Sr]_Koq'"KW[~Wx\`L~q'"ZP~F ܙM+U6 ODM7.t7;%/c.2>ן<. ]{[CU\ \/|ؖZ$T]c1cZϵHX{f&kߛ dk?p>D*V3sm |WoqwCEA0Nq8ݴhS9M;i |͋8;Y@@@@@@@@@@@@@@@@@@@Lfr[nhkQ-NYNDDY< MX{4\uP^9 Zr~g&j3MFxTx܇{:\0Mܻ^k;8d̢7Z Pi ew6(ƹ cJ6qsڃ}2{ih nCcIƈѻ*"kꋄ@G0 }s>O|uߟ[~ǭydϭ2ڮ_ M/C274* w0W!` ߴ&\F[m}F}ouXsÓGj:Sx >O|EoTWo9;}87iʟ(yZΈ.6-3;d%͌0bpldfT&#]&"ʈ;#b؛|BS'g˴|1߾i ";h|nvj`]95Aʠ*?#*U%#"LJ)?")t"$F5~;&T٪}oх|15鋙|sD)`S6k'"N#`$]]owsd2CyV:Iʤ9r1xY=2{W8|ҙvfx֔OD4Mʢrc^ nZ(FjFlվQ]*%=1-mzhFQ}ZF:lmCo1j-S$^;ۮMb#iֱoZufW*lm^wٺQ)eY6W~B]n˿衕ʸjd3-v~veb[ǽIk7'je'IϙGmzZ⾵jؤ`v>)*4SV)0"."":ӑsf[mdGJv3\N[+@c$[OP>vBC)bc5!\)&dcD~'X)DOn3:(nJI`&0!8)Gf4kP"zI~"LO"'*%\F[7a&4)Io1  =bddJT[wk1@)$Q䵵v3B!8g1aq[ۦ Nʵ{A$ksa15T&liifɗPwa|rSI?RVęAf_  P 7HsPqFNH/f!oT43(^@7rEvh_^,&翛p#8/EFg(c`>7( PIfnjGގB @S;kW =bID͠ouO'ޙ)^5k7S:[ B?W:Kah݁'ݳChI%Ϲb悏DG؁Qwnx?j8+=?_~.{oht[.OsϏ]yO=Au{ ~s*WWz3 ^j?ܰ'>^eoB^P<*K?Zw{J V4pl?/kNnpUTW =pWK&cfϻzqaONm4şH VL=㯝K]9kf-j'":o.YYigOǂKZ*ˬTUw?i'"S[s9dy߸ş(ۓnzMWxCK9şȫO/KX{⯖:_XsW&wU\;m?8bVO_k O 6`FEcY4t$0Y͉gV2Օ凁y7;z=%Ag$=%k9OE)://%-nݞvŜ})N'V[s8̔6[8>S#QtOJI:NC#-0eTǸhb"KEym81sޢv{on ۙn`@Z@W)̤@wExz;%&:\{c_眄׵ڛ]zuyKǵy;%)Q{}oG x/ՉFv}=elu]<0ԾBoϳU>o7={c  X,&۩!˦pDDv]E(kz 3:^w_͔ܽQeoʼ.$jn;ve.hfעj,~knٷԗ~KN-,kNq(F<LLvISqDDvǍvaڪ=bꙏPQLvV3\Qk'"&eL9ֱ?s7-q~o5#j_(.S.Ĵ5 "R͗G:j_](ت}O|{l:^7Q6̭Z J=ӫ.[ pAg~ ?SI!RmcgJe\_+`;zK]$zhqoҚýZIbsQr浻VoZ767@@jl3J(@0!|dOadϵ[ƌ>q{ DDƿc?=v1ǵ6cλs~yoǜy;qmy;>1ڇ{^|nj>4 {l: Wn>\H=?y̑:DZ=ֹviyRiIŌgW1kʦ# h)NMuW/w(H}bѽvv4YhnDIF =vc@獛Q9%GlE߯!}dO8]#Ot욏J%{.TWJH]3tסi(ō嵟7n^ngլU[jM&K_}7+?no^/1-{?xNs0joqz34>W_y-DDyƭ4۪^m'"JU쮥W,s9%~|U[M*=OD$ }oeO< TW_w"vL/Mn/ݱ-DD h^2a毬BO."Ig-ZiKV~ş0=KӇeT^k+ ʢdUv^kE9ov$ļ>I}7²?&?Ekou~Zf,ۓnr<X~}Q);/";,OxEr."ɒdڨ1i11kWdgŸGw='[yw??Ev oi`^U ]NHf0 L\9y>wkOk&l3TkWu=9IJ6o&gj+loff7hXhηx]7 :Ѹbܯy!8VE͗Y]#~k[Dm&cTޘ׏~wwzAk?mvʼQo'>~#MܽM~ ,ݏkΈo]wEOx aS_ؽDfl;'Q{_$Dm]{ #];>tx΍{7=+ lIymּVcܭs{ PO9R3肉kK3MMW";+VmݓMP݇#5FcX<3EYɪ;ퟪg<.J`L uOSmtz"]}F\M:qJNG(%WP$/A0{S(Z{wJ2]8̻&lvHnj#ٟt(DD-IE[W Ebh[+l>G;[=EAf1J8SihzҁHVZ""-ɉKwT>SS"}l8oڞc1?stœJJe%}r$%?0ez9vOnV5 d;z& >m]3Ӌn>gzX̷N;W΋M2yT딊= IE1S."y@^>Tf(ϪS'I4Gγ5/A٪}'*nj7jMDDӤ,):Z%"[ofVݥbj]ۅF=^AD4VJ([Ǿ^kKeߡ[ϖs)omWk&1ݴ朹UkיA^gze.fAg~ ?SI!RmcgJe\_+`;zK]$zhqoҚýZIbsQr浻VoZ76@pA@@> ע1fP*gK\L%N3MDb̤54B̤k7l"^w9>`Ȃ'w ]#S&" 2hdڮE CIv3'n0FQc2c%t{ȴ_{ M^l`qL}!#jsL8n0qtrZp_+Y,nae;E615gzV 0V>`ό~a/OQY<םl?z=sWosK3sH_ x׉] IDATxoߞw EQ"!G\ǝo>g֟ 0*=+T0!I#KCclj깿ݾ혦Zp{;]Y\Gz1M4UEB}]qE;v6Fza5u4- e3ex#vdGmpqЯq_1VL=H }͇߯|9+u/>ͯc[jߔ_2~U߻Dj/_ηV'j/嶯]/٦v&jOw{DfX_/*ʼolIꟅU#qy.^P~F3o۰ @w.|5oeO[4QU~W<[ƒ|X?WםA^ow\wZ_;{-<.DDמ9˧_ODt܎u7;mLvAC9+q'"[9lVş(˓f^4̄wvU SjZz/tju WqM<%?f!R(;D$F jZҢ ۫}_vppREn19k9(ޞ.teDɝf>;95ΈooġِE*r)'^kHzO "ɝW{[e dٻQѯKHc ji2,#=J;?>[h!"Rtr͙]v {w9u>xPì@rGg#wVޘ5g ]DĈ{7pS6֊!oG֥$\:҄ި:WD;ǮP'U趻V%oU[FIm0-nQZe63N.+=N2e쥎b۔LM{E=avdRg]zCDD:JZŔdbujعr^ly01z*^㷛&S*H$I/>F`vED4Q(k?PUkN2igk ^VLU GYOT:ݮ oԚ򉈦IY4Sαuϫ\MKEߨ5[͈; KԺ֨+Ji4_e׫~M`t;v#`r>Ex$F9ۜ3j:3H+?`LһlZRʠ|[?UDW)[3zh2CɯuF֯5D-s:MZyξ">W]Z.å+-yAy_jd%_ yµgh_y.9d{+nşȗjjo&vX$×~kO?ɻx?5)+wn>wf |q?{;N\{Rko뜜'_7 }|lo]UU;߾Ҫ[E8=Um>ю`wDέn/MaU7xyDw7Z!|to EB_w>]5tziֹ<֮:V}5~ז_POY.<˧pNjzŜvTtI "x44)$ILhj<0 #ە UO7]6i sR` hVSed4?vQLťhI.k 7K|{%dw bѨ1ɲl8NHSUYuαnUuISUh[5{M n8#'p*K$Qd3"VɄxgQFDNI kTM$3(v%<۞ֵ5uZDz沕*g95kv 0""i{%9ԛ j 7 tsng?I)ڽťh wμ <ᦇ8jXD7](¼""ZWp7e쥎b۔LM{E=Qa&T٪}oх|15}oZ?v(e딊= I'1S."y@^tgZ;uIs<[czdW8|ҙvfx֔OD4Mʢrc^ nZ(FjFlվQ]*%]iikDDc4/uz&0QfkzUl9W"vuoMmΙ[V [0zW]J)ΒmW~B]n˿衕ʸj%Vw ;?H2Gޤ5͓{2~Ḥ̂F6kweqZnlഃ0B  `sC {(#6m@u>??cG#5{wís?v w{l=k|ylǁGFoR|qa~pB@@>çrtofPdkč~ .u{qf~judz]uH2]PgLv #X8wK큟n[hQŴG&nEIGXuշh;g(UQT";#y6ˇʨgM|t ph\v[M6_oXP>t}n2Si^|OK*#ǛQ\蚼`<:mNܢ?tG⪧Ju9tz8 ӳ4jţMk~koEd={N/ TfOûnOGz䲛SWYћO[\_w7/٦vZڿ\| pU{|GZB$sUU" LUW6/UqsεW<}a+;}yo?Q'ݼmfZ܋grWp_s嬅WzWW}_D!OjܶKdyW|şʙ & Q{K[xw?ѼSږUչqW9ڿV}\rE-ոe%SYqJ.MV F y!Y^x=52^g^kE*O>`ı)F?"U g7BY4qOxG_︗(W$k ,IVJSdGo6$s^3dkwΐXs^{ OC-<pB@@@@@@@@@@@@@@@@@@@@@i@5bfx&ZL q|u͗M5tkOOX\{LW{*bv%Qc4Cvv8V sĪPk^k?ޘkS!+⼸uӠV- t4_Bp {=qd>noHw-0wwCЪoojqV{X fUw ^j 4Kgѽl_[=kz hly_ܽ~E0_muVHMw}ܸ?rt4<[>xu}W~ PO9stof2Mw]S#{Lt+}DDjGdfd!]zw`Ӳ^.X@44h/LZsS;NrN򸺉r3e$!9zG(ݿku :^LT(zg1J"R.gx@؈ē>vATgoe2;J;suD,@+?21d$J_azZҵ=/R;TݣzluC3r͌zc̘&RؙQMDdDz_hy#;ZC ؚw(մ/J)ʶU6Aկ Plʪ""Μ)gzI.?3Gi?:횽%،oX8~\\kμ$4eoR$Z!=Mf(cuսC 6i>}_7q-UuؙvDPA"" fIJQLQzk:cc·_>+OtE*;/;z8cs\G mZ:ڋœJ2zI+qjߥw=DDt%_LiMv=,[֏+&J|*^㷛&S*H$nWcl]DDr16śʳjIR&͑lqij_(kJg۵ZS>4)f9y֟+iFUbGawZtdZxX)ˣlzޯ .u~?bl>[էHv](GtrsV_gzUzlgJ)ΒmW~B]n˿衕ʸj%Vw ;?H2Gޤ5͓{2~Ḥ̂F6kweqZuN;AZ4 7 / G'fu|Xag ٮ=ԿՌPS3͘^H8ts10^Xu92c>vLkL۵pဩzݮ׌g{c&Ȱ]{˶sDk-d?8d0llZ;y|Ø3C#N/vk0ÜdƸ)鵢vk c"9l0c$& ,Zd} _   Hx{.~9!lM{j󽛏`}.s}(3{)+{=d%{=cAcQGv_ԇ^M3rߝbIG|5x[#jܲ/%;Ec8e1kt]|lCZu;܋=fG)NE)h*zhڏ:JHןe6R#vƘPӞ jkW .v+T7-Lh tg<n?qhP9s.v~_vn}ggx\d*D}y<.DD)^IOԮ|yXS+];\< $ЪY jS@QfNPj/ j/ivI(#b?&dm1]{ kyڋ3r'j+Hˢ7Zҳ4k:& 5ػ{H,ҍuEA4Nq-H0>-jx@x(OFZ苆d^hka5&H07h19< VXp=`E=T`Vy 3d4);GMy/-Mo>f^k?ZOwem l8`-NG:Ɲ]6i۩0wV䞦猞tcR{TO?xU57M/ c^Ҟ[T{󄼢J2süΈѫO{ݵo26KYg'S3R DF& 8L"/֨4T=ImhhۤR$#JfM9mޮ<9E1?GLYd ?ũPIfI? խDD1ej &NU,UEy(;ߙ=Nkc;].OI,I1$pvȒ4sr"AEQ@?H6N 5؆qD H-;%YIR$H-ݝٙyvThywmE}j  !=!0y.Rc먳ɘ%+7Nڷ.x˜4UEkUnL(kZRhh]3Q[\}ȕ9kꨊʚD,tTKK-@ULH*P;g)nƍy05kβ4HWDb'V٩* ݒsb2+!9ï#m-MM1GZ5䵴uMh܌b4Vx؟2-y H.zRlrӠ;X/Qtik^hn4156f6\}x:>lvH>I@j4Fކ%rU}2.עUV&NXdl @:A=(FDV 1Dz_bxz5TOXm Tsm 5R}?5q>^Dhj?]33ScƩZ:;|Y_b ڵ2t+><ؔW6mRr]~D=iժIĹ5sLrӠ5fTVs\2ޭU`^/wc9aÄ)udMb_Z1i-tf_&s&kf~NOl:-vwh9sJu^ɮW}.""+1ÒcBCIłXDj&>^uJ_413œXՠFa"ƼT)a)D) 0覤9ZRoCH Iij%D=(SBRDxiھR"$qrp?[P!s^&4ԩr_NZ%hcD1PjT!%RmTe6-T!#H;sƌ3Fƒ $>+, BFDQȍqEun*8Ex ""[DDD DDD@DDD DDD@DDD%*P+?"xra~WDbh,_ړt+jNb]ldXVn_W'Byvxz_Djh[Q?˽甜J8Ajǟ8W2<3S觿O23:x. s3֕V,s|2>w|M:zGt?y߳kߵq׆Hwzә -߱g_hjp^~g;-i}7a#G}tѳ ؗƼ>M=wu[wkSEu>(6||؁o:f[4v׭vWxx}.$;嗢g^nh>OټB|>~>aKCSW:Ʈ-j7︧`=í +=7&Vσ;{yϞҊ% i:i׷ :n ,soÅ>vnkols[:7(t|[gZߩRYA}mCk&V=)8L}iSRǯnǒP8_1L\%a] eUNkW[;z, xcSlp-uL:v}A:TFbNPhڕX6GCx&X!*Oҁhp~:  x3K.zp4߀[)]:]DDDW """"""b """"""b """"""b """"""b """"""b """"""b """"""b """"""b """""""""b """"""b """"""b """"""b """"""b """"""b """"""b """"""b """""""""b """"""b """"""b ""-+.Qt(pH: zBhWu]]-p,txT=vуtnz"f(9^Q֞sN~3mAFg] O ~]k\[}g&z:6:ϻN]b>0\ױ}-|Bc=dġ*tGG~7c[Jk#%?w;Ԍ&[:%cAX LJ}УKϻ~td n>' ~垯t?֞?rևʫ=Ss٬)әPشu]w ɘr6V2\=I+%L/>w'v_kZ?ӻB&,UupB~MT7ضeN 8g_ƥ^m}1,,!;L3 D$絼eKTk+ C~z^sW!C\6\545#a3HI4ybesRcQ!^~̋]܊H _zGξrkhq4;gI]p$lAQD.{Txb{d҅a۷}%fF\h$ 9ޜI/Dd+枚k9?;UOMQo-WƲE]h$B(s2F(Ȯyɪ3'^ݷw*SjV.'Uۚ ;g:'V7a~uj3Z<0/;cAwPG/~9KhJ4]l$ 1;jәwƪz5:6"Wjc.ףUV&|˦/]G5(vWD7f;ӾoO٧{hVOg)}6\~ۍy G`W5R}}:^Dp$FqE-.-e?t@VmJ]FTלDwVZgVuL@܃@sX|KKfTnragRլnRSl|N0.S68$>evZ1:_vF+dI--FT9sTs)wBLvVvDDDta ""b """"""b ""`EEߚ$~oObKQ} [V;O]Us}/+"WQ;P]?}W@xмSؕiW\E|R|Wy g7֊1|R(xg [8IENDB`python-sense-emu-1.2/sense_emu/play.py000066400000000000000000000074771411441564000201150ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see import os import logging import argparse import datetime as dt from time import time, sleep from . import __version__ from .i18n import _ from .terminal import TerminalApplication, FileType from .common import HEADER_REC, DATA_REC, DataRecord from .imu import IMUServer from .pressure import PressureServer from .humidity import HumidityServer from .lock import EmulatorLock class PlayApplication(TerminalApplication): def __init__(self): super(PlayApplication, self).__init__( version=__version__, description=_("Replays readings recorded from a Raspberry Pi " "Sense HAT, via the Sense HAT emulation library.")) self.parser.add_argument('input', type=FileType('rb')) def source(self, f): logging.info(_('Reading header')) magic, ver, offset = HEADER_REC.unpack(f.read(HEADER_REC.size)) if magic != b'SENSEHAT': raise IOError(_('Invalid magic number at start of input')) if ver != 1: raise IOError(_('Unrecognized file version number (%d)') % ver) logging.info( _('Playing back recording taken at %s'), dt.datetime.fromtimestamp(offset).strftime('%c')) offset = time() - offset while True: buf = f.read(DATA_REC.size) if not buf: break elif len(buf) < DATA_REC.size: raise IOError(_('Incomplete data record at end of file')) else: data = DataRecord(*DATA_REC.unpack(buf)) yield data._replace(timestamp=data.timestamp + offset) def main(self, args): lock = EmulatorLock('sense_play') try: lock.acquire() except: logging.error( 'Another process is currently acting as the Sense HAT ' 'emulator') return 1 try: imu = IMUServer(simulate_world=False) psensor = PressureServer(simulate_noise=False) hsensor = HumidityServer(simulate_noise=False) skipped = 0 for rec, data in enumerate(self.source(args.input)): now = time() if data.timestamp < now: if not skipped: logging.warning(_('Skipping records to catch up')) skipped += 1 continue else: sleep(data.timestamp - now) psensor.set_values(data.pressure, data.ptemp) hsensor.set_values(data.humidity, data.htemp) imu.set_imu_values( (data.ax, data.ay, data.az), (data.gx, data.gy, data.gz), (data.cx, data.cy, data.cz), (data.ox, data.oy, data.oz), ) if skipped: logging.warning(_('Skipped %d records during playback'), skipped) logging.info(_('Finished playback of %d records'), rec) finally: lock.release() app = PlayApplication() python-sense-emu-1.2/sense_emu/prefs_dialog.ui000066400000000000000000000326571411441564000215710ustar00rootroot00000000000000 1 60 25 1 5 python-sense-emu-1.2/sense_emu/pressure.py000066400000000000000000000151411411441564000210030ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import sys import os import io import mmap import errno from struct import Struct from collections import namedtuple from random import Random from time import time from threading import Thread, Event from math import isnan import numpy as np from .common import clamp # See LPS25H data-sheet for details of register values PRESSURE_FACTOR = 4096 TEMP_OFFSET = 37 TEMP_FACTOR = 480 PRESSURE_DATA = Struct( '@' # native mode 'B' # pressure sensor type '6p' # pressure sensor name 'l' # P_REF 'l' # P_OUT 'h' # T_OUT 'B' # P_VALID 'B' # T_VALID ) PressureData = namedtuple('PressureData', ('type', 'name', 'P_REF', 'P_OUT', 'T_OUT', 'P_VALID', 'T_VALID')) def pressure_filename(): """ Return the filename used to represent the state of the emulated sense HAT's pressure sensor. On UNIX we try ``/dev/shm`` then fall back to ``/tmp``; on Windows we use whatever ``%TEMP%`` contains """ fname = 'rpi-sense-emu-pressure' if sys.platform.startswith('win'): # just use a temporary file on Windows return os.path.join(os.environ['TEMP'], fname) else: if os.path.exists('/dev/shm'): return os.path.join('/dev/shm', fname) else: return os.path.join('/tmp', fname) def init_pressure(): """ Opens the file representing the state of the pressure sensors. The file-like object is returned. If the file already exists we simply make sure it is the right size. If the file does not already exist, it is created and zeroed. """ try: # Attempt to open the pressure device's file and ensure it's the right # size fd = io.open(pressure_filename(), 'r+b', buffering=0) fd.seek(PRESSURE_DATA.size) fd.truncate() except IOError as e: # If the pressure device's file doesn't exist, create it with # reasonable initial values if e.errno == errno.ENOENT: fd = io.open(pressure_filename(), 'w+b', buffering=0) fd.write(b'\x00' * PRESSURE_DATA.size) else: raise return fd class PressureServer: def __init__(self, simulate_noise=True): self._random = Random() self._fd = init_pressure() self._map = mmap.mmap(self._fd.fileno(), 0, access=mmap.ACCESS_WRITE) data = self._read() if data.type != 3: self._write(PressureData(3, b'LPS25H', 0, 0, 0, 0, 0)) self._pressure = 1013.0 self._temperature = 20.0 else: self._pressure = data.P_OUT / 4096 self._temperature = data.T_OUT / 480 + 42.5 self._noise_thread = None self._noise_event = Event() self._noise_write() # The queue lengths are selected to accurately represent the response # time of the sensors self._pressures = np.full((25,), self._pressure, dtype=np.float) self._temperatures = np.full((25,), self._temperature, dtype=np.float) self.simulate_noise = simulate_noise def close(self): if self._fd: self.simulate_noise = False self._map.close() self._fd.close() self._fd = None self._map = None def _perturb(self, value, error): """ Return *value* perturbed by +/- *error* which is derived from a gaussian random generator. """ # We use an internal Random() instance here to avoid a threading issue # with the gaussian generator (could use locks, but an instance of # Random is easier and faster) return value + self._random.gauss(0, 0.2) * error def _read(self): return PressureData(*PRESSURE_DATA.unpack_from(self._map)) def _write(self, value): PRESSURE_DATA.pack_into(self._map, 0, *value) @property def pressure(self): return self._pressure @property def temperature(self): return self._temperature def set_values(self, pressure, temperature): self._pressure = pressure self._temperature = temperature if not self._noise_thread: self._noise_write() @property def simulate_noise(self): return self._noise_thread is not None @simulate_noise.setter def simulate_noise(self, value): if value and not self._noise_thread: self._noise_event.clear() self._noise_thread = Thread(target=self._noise_loop) self._noise_thread.daemon = True self._noise_thread.start() elif self._noise_thread and not value: self._noise_event.set() self._noise_thread.join() self._noise_thread = None self._noise_write() def _noise_loop(self): while not self._noise_event.wait(0.04): self._noise_write() def _noise_write(self): if self.simulate_noise: self._pressures[1:] = self._pressures[:-1] self._pressures[0] = self._perturb(self.pressure, ( 0.2 if 800 <= self.pressure <= 1100 and 20 <= self.temperature <= 60 else 1.0)) self._temperatures[1:] = self._temperatures[:-1] self._temperatures[0] = self._perturb(self.temperature, ( 2.0 if 0 <= self.temperature <= 65 else 4.0)) pressure = self._pressures.mean() temperature = self._temperatures.mean() else: pressure = self.pressure temperature = self.temperature self._write(self._read()._replace( P_VALID=not isnan(pressure), T_VALID=not isnan(temperature), P_OUT=0 if isnan(pressure) else int(clamp(pressure, 260, 1260) * PRESSURE_FACTOR), T_OUT=0 if isnan(temperature) else int((clamp(temperature, -30, 105) - TEMP_OFFSET) * TEMP_FACTOR), )) python-sense-emu-1.2/sense_emu/record.py000066400000000000000000000130131411441564000204050ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see import os import logging import argparse from threading import Thread, Event from time import time, sleep from . import __version__ from .i18n import _ from .terminal import TerminalApplication, FileType from .common import HEADER_REC, DATA_REC class RecordApplication(TerminalApplication): def __init__(self): super(RecordApplication, self).__init__( version=__version__, description=_("Records the readings from a Raspberry Pi Sense HAT " "in real time, outputting the results to the specified file.")) self.parser.add_argument( '-c', '--config', dest='config', action='store', default='/etc/RTIMULib.ini', metavar='FILE', help=_('the Sense HAT configuration file to use (default: %(default)s)')) self.parser.add_argument( '-d', '--duration', dest='duration', action='store', default=0.0, type=float, metavar='SECS', help=_('the duration to record for in seconds (default: record ' 'until terminated with Ctrl+C)')) self.parser.add_argument( '-i', '--interval', dest='interval', action='store', type=float, metavar='SECS', help=_('the delay between each reading in seconds (default: the ' 'IMU polling interval, typically 0.003 seconds)')) self.parser.add_argument( '-f', '--flush', dest='flush', action='store_true', default=False, help=_('flush every record to disk immediately; reduces chances of ' 'truncated data on power loss, but greatly increases disk activity')) self.parser.add_argument('output', type=FileType('wb')) def main(self, args): try: import RTIMU except ImportError: raise IOError( _('unable to import RTIMU; ensure the Sense HAT library is ' 'correctly installed')) if not args.config.endswith('.ini'): raise argparse.ArgumentError(_('configuration filename must end with .ini')) logging.info(_('Reading settings from %s'), args.config) settings = RTIMU.Settings(args.config[:-4]) logging.info(_('Initializing sensors')) imu = RTIMU.RTIMU(settings) if not imu.IMUInit(): raise IOError(_('Failed to initialize Sense HAT IMU')) psensor = RTIMU.RTPressure(settings) if not psensor.pressureInit(): raise IOError(_('Failed to initialize Sense HAT pressure sensor')) hsensor = RTIMU.RTHumidity(settings) if not hsensor.humidityInit(): raise IOError(_('Failed to initialize Sense HAT humidity sensor')) if args.interval is None: args.interval = imu.IMUGetPollInterval() / 1000.0 # seconds nan = float('nan') logging.info(_('Starting recording')) rec_count = 0 if args.duration: terminate_at = time() + args.duration else: terminate_at = time() + 1e100 args.output.write(HEADER_REC.pack(b'SENSEHAT', 1, time())) status_stop = Event() def status(): while not status_stop.wait(1.0): logging.info(_('%d records written'), rec_count) status_thread = Thread(target=status) status_thread.daemon = True status_thread.start() try: while True: timestamp = time() if imu.IMURead(): ax, ay, az = imu.getAccel() gx, gy, gz = imu.getGyro() cx, cy, cz = imu.getCompass() ox, oy, oz = imu.getFusionData() pvalid, pressure, ptvalid, ptemp = psensor.pressureRead() hvalid, humidity, htvalid, htemp = hsensor.humidityRead() args.output.write(DATA_REC.pack( timestamp, pressure if pvalid else nan, ptemp if ptvalid else nan, humidity if hvalid else nan, htemp if htvalid else nan, ax, ay, az, gx, gy, gz, cx, cy, cz, ox, oy, oz, )) if args.flush: args.output.flush() rec_count += 1 if timestamp > terminate_at: break delay = max(0.0, timestamp + args.interval - time()) if delay: sleep(delay) finally: status_stop.set() status_thread.join() logging.info(_('Finishing recording after %d records'), rec_count) args.output.close() app = RecordApplication() python-sense-emu-1.2/sense_emu/screen.py000066400000000000000000000123521411441564000204130ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import sys import os import io import errno import mmap import struct from threading import Thread, Event import numpy as np GAMMA_DEFAULT = [ 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 17, 18, 20, 21, 23, 25, 27, 29, 31] GAMMA_LOW = [ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10, 10] def screen_filename(): # returns the file used to represent the state of the emulated sense HAT's # screen. On UNIX we try /dev/shm then fall back to /tmp; on Windows we # use whatever %TEMP% points at fname = 'rpi-sense-emu-screen' if sys.platform.startswith('win'): # just use a temporary file on Windows return os.path.join(os.environ['TEMP'], fname) else: if os.path.exists('/dev/shm'): return os.path.join('/dev/shm', fname) else: return os.path.join('/tmp', fname) def init_screen(): try: # Attempt to open the screen's device file and ensure it's 160 bytes # long fd = io.open(screen_filename(), 'r+b', buffering=0) fd.seek(160) fd.truncate() except IOError as e: # If the screen's device file doesn't exist, create it with reasonable # initial values if e.errno == errno.ENOENT: fd = io.open(screen_filename(), 'w+b', buffering=0) fd.write(b'\x00\x00' * 64) fd.write(b''.join(chr(i).encode('ascii') for i in GAMMA_DEFAULT)) else: raise return fd class ScreenClient: def __init__(self): self._fd = init_screen() self._map = mmap.mmap(self._fd.fileno(), 0, access=mmap.ACCESS_READ) # Construct arrays representing the LED states (_screen) and the user # controlled gamma lookup table (_gamma) self._screen = np.frombuffer(self._map, dtype=np.uint16, count=64).reshape((8, 8)) self._gamma = np.frombuffer(self._map, dtype=np.uint8, count=32, offset=128) # Construct the final gamma correction lookup table. This is equivalent # to gamma correction of 1/4 (*much* brighter) because the HAT's RGB # LEDs are much brighter than a corresponding LCD display. It also uses # a non-zero starting point so that LEDs that are off appear gray self._gamma_rgbled = ( np.sqrt(np.sqrt(np.linspace(0.05, 1, 32))) * 255 ).astype(np.uint8) self._touch_stop = Event() self._touch_thread = Thread(target=self._touch_run) self._touch_thread.daemon = True self._touch_thread.start() def close(self): if self._fd: self._touch_stop.set() self._touch_thread.join() self._touch_thread = None self._map.close() self._map = None self._fd.close() self._fd = None def _touch_run(self): # "touch" the screen's frame-buffer once a second. This ensures that # the screen always updates at least once a second and works around the # issue that screen updates can be lost due to lack of resolution of # the file modification timestamps. Unfortunately, futimes(3) is not # universally supported, and only available in Python 3.3+ so this gets # a bit convoluted... # FIXME only need 3.3+ support now; work out which bits are 2.7 and excise touch = lambda: os.utime(self._fd.fileno()) try: if os.utime in os.supports_fd: touch() else: raise NotImplementedError except (AttributeError, NotImplementedError) as e: touch = lambda: os.utime(self._fd.name, None) while not self._touch_stop.wait(1): touch() @property def array(self): return self._screen @property def rgb_array(self): a = np.empty((8, 8, 3), dtype=np.uint8) # convert the RGB565 pixels to RGB555 (as the real hardware does) a[..., 0] = ((self._screen & 0xF800) >> 11).astype(np.uint8) a[..., 1] = ((self._screen & 0x07E0) >> 6).astype(np.uint8) a[..., 2] = (self._screen & 0x001F).astype(np.uint8) # map all values according to the gamma table a = np.take(self._gamma, a) a = np.take(self._gamma_rgbled, a) return a @property def timestamp(self): return os.fstat(self._fd.fileno()).st_mtime python-sense-emu-1.2/sense_emu/sense_emu.png000066400000000000000000003040631411441564000212560ustar00rootroot00000000000000PNG  IHDR'5CsBIT|d pHYs4}4} tEXtSoftwarewww.inkscape.org< IDATxyw}h4#ift[>d c;@ $؆Mac@ ˱`'0K8M!p0WKHsK$Knit==}U}4Ӛy=?ַ>55ao^8j7pWhv]id%5IJXqIcҧ?neF1f'=>dі:o]-*Y@ZI-@XF$=-g7o]{FR869{K%&09 ;"dt=u@ŨD-[&c̍6ԙ#GeFl ԿKYcFol}{n7L,);`!ͧe)I\JAvR6?)Kϧ1weyLNVno׍1UK/W&$7,S*MƍW P*(O(H(V09,k k:_ŝeFTr }ֱo52ojH8&MK6,I40X+?3` qX|z]{V 9Yɉ7/SIYC$Z6ukZ*h,tA.%c #iVҝNg@"Yɉ_ۜsrѻ$5x y-+ȉ%+!3T~l>;[qcǼ]kMIr^{sYuI6k].ZǏ>$?urV'G޻Ϋf*Xu˪636w)''\iAzTfae}ˁ/q%Xr=tXJ:o([ߡXZYWf,7a5Nc"ɉB%dԷU"ڗ,<%(F09܉]SC3mkeo{(krM >adzWlEȚ!}Q0 %_wMaT+cG=C?pb-w]rPJ+߼Fm:'5u)&DHn2%WP^ˊs#yuoB(Kr }W;y!IWN}xq%z6+޳Iƍ_Qϑ%ݼŗP+DZ;쿹mVSenŖ]$~HР9||ߏ:6.Q*e?ztN0fq+iGu6466j͚5Qes1!3De';Kg6ō5{KݚqA kZ黹S%&ܘ+6 ym}Jr^ic;n;m 'Pz'wsg$ѴJ])Ch8uJ@NM׷t]2JKN\' n[ߡJDx %V^![wW w*oOߗdK\2e[t^\kU6++J|#7RMW+%Ne@IȵVdX+kr2/c4a7BdW囕9Mĉ)>w}V0}{>,TgSd GRWWkSNAQ9Qq4zrcv=Q^PD`j.}I)՘j4Ɣr*t>5A1~ǭ~ww3b-*9KLiݗJC+IJ;N:r0FM}\#S6һeF睜軹zkM}mXRĄ?sr+pLZ99]q7^Bܘl)o߮ohhs#O?ROOOlEקp}m۶>֯_"ĄvYR6mɓڿ{.,Ĉfva cc֭[bD3۳gOIIK,ʕ+ChfO>tXB!F4Gy׬Y _*'(K.Dx K022={1F7o1 EL&q#پ}488XZjUlΝ(ze˖;Ĉfm۶PW[[[/k%aׇq)Rb1]z!F4Cرcn8&]6Ĉ*'agTf ~2qt8XB`b.VgL%]FGG?̬͆%*%imgeْ,t8M>RT JS**z%KJ}ax!݅*$U{(ɔg%ιTgR>;5::Z!F3ђ?D"b4+^$O9s-gVwa6Ihu6չ1%5▼yq٬d<޳W9g|׾}m$Feܸ7KNOr*#\F/eHJ.LԨVr"T^}hSzBפԛDJ^09-j ʐv\%V\.NKZsf s#Qbf9j*53R >3Nj_s@up՚lZWFԝD,srrLe&^[XY l})9{s${cr7T>n.KO15-R29 %C>aI>'G6;Y:RO.Mje.SʖqZhcF}ozN_A$}Vzv;嵔`Փ.SA]N+_9|pLJ[2_ LJK:kY.kCaÛ74]h &'&Q#sDqJ,H/Ji]SJOT1k93:y yATS^-xE{|گ5s}WN;вs"=I8gY7Rc/(yYtHyJF(zzsh4r=9UOI:sPm+cBA^/HVO ԝqwYmLjc&ʈS B5krw͒8ۓSlң8jL\H E1w A/.7|>er >`;/9ayT漭NjI 1_A^P!XHV\\o!F3RCuuu!E2X,VrnUCCCIq ZRXfɒ%%~_Q/_%KKKK̮_APJ|v[{dU|hjj|T;.ڕ97/"hx7:O?T^RLb-~5w6t(G>EKZXk 7I)ե7,H2S%LCB]37'51cdOxHAj^}_2×ԝܦeE\F9ژIQ|*ϫ'11M_jZCINcona'U E>2GP(;+m̦DiĹ[Tncje!I꽥bIgSEƝVI(JrYls%UB\XUS 5#OƋ˩{Y 6Hώx"Üne"$;@Y2j" ֵ"NNkU %9aUYFMku.ũϩ5GFUGOhuȉ+n0dy51jh P'',zW\ZNYĶ Irz~g ]|.ĬJRj+fq$U+޲Sۅ1jµVK}Nt:Y) gFbk%:ɦ?69Nଛ(PBh,sE/-R&VOts],tS+@Rqg9aZFM .Ykty&?:i \7hOI:uS-eI4N9uuکDBIN,m%}>}Y '>ߴ:z":i,uPAf<$mzy+J|wH}wϚhK+?96]n_Wnʘ?/ k:%'L܇V92KX_J(m[曘cw|lyfhȪe${tD?߷[?m>naw!FCo^ڋx=;=`e:m2 `OS;8TjG{ߊ:AAqQE׬O~諟O;}T湦qi#ԑ~w~C1fޟS[wnVh';el5+ʮ:4ԌD`UH#O~?f=?3F|[d6ݑtD&$Fv!me[C_;v{㎣7J5$'T^H8l-GWbP>("L?$=I?Wz]H{>}\_}]tRc&i9 U. ĄY<WSSӅhttT\[T__%RPF%} 44Tڬy^yyG[[+Xg28jkk 1R)MNN,SssslllLlqeKR2TCCN]F544(,JL'OonnV,VB|pI}T⽞f56VԴ`I1FnXIR Z[[]L/2&SxQ 睧 Z;ysb^W8S'LON ȝTQcXRI ~2|ePMR%,ڱcGq]}!F4GjEڪ͛7ۧ'NOO֯_bD3۹sgIk֬ʕ+Chf6mT L&Sr/x ʞc=V]{!F4ǏkϞ=EР=y!F4RtvvK. 1޽[יӪUBhf?xII7 KK~oٲ7&&&Jk){DBW]uUȑ#:p@/1"؟|wG>nR̤䄦OӾy8WCtsw_}wя}G TȮY:9ιHb,mرJk*F5$j5NE#?u[h%5|WIɉt8b2g:T9uG>rw=PcɪDɉ%EQ5 (ID#+{ iөDKjr"˖V{&bҖ9(R$ƨ%`FrZj> 䄭Sgk (]Q*q"Y~_ӌ/~Zlrǚ*k EwKRkHc#f5](5b_4u:TQP+|'몊^CZSIQP 9zAe}a5OnJшoānc_cMjɉ\T#'~So^U+E'kּo*r94EfI%5U׫^LmͻorrIW83p"{m&"2Fok]`ɉc"DYkp?-1f<&FN@)&\D\L׬Tyb崬q44$'1eQoeo?dgG#?_Xo^b$'p%2+L9;d+S/.TF: x1h@, Up,e+{Dx  lyIG^qըd%'F]O!uXM8NTX0x2ھX_Y5rxo9 IDATrN+7 ᢥ'eMNbq©ܹ!FPf{=Qgb !Y$=ɞ=*f6zdC^"~˞d̜SL*D,lGN~VHD˱TQ(Jz"^c||\wCX`|:x 2!FT\OGLHrbq/ЪlZc IC^\{IhR〉IN1zzmTS'QI#^\Fܪ{ e7FbIsgd*v<uċ+g.z^Y9v$֫ϩϪϫ>r1/^L9  ;SG> ' kN%$]OG&b&'ΰF:q:a IuvW[W.L7Fqjuuҍ),!0'c%7#ϫϩ1 y-Ij4iWcAӐɯ:U8פgY gb68ĭ|e#_[l {T@dFVuUʳQ2ʛSq5k%'f3Fî+jQ1JkDYD#'\㨧lO3̦?(&'ݸkP~L"9"ErD ) R$'@HNH"9"FAvd7&'Z@k? ]rzOE)؁PtZfc}Ve-=J6ͩm벺{e:o9hhD19١p_6.צsjЎirjsq@0D!03|+P-HN`㥗jIg}_?2E_Dm9muǵ!^R?#k(  wQsscנCB;vjc;5jNH"9"ErD ) R$'@HNH"9"啳 谹l?l+kr"oR.[#~l}`Z ) R$'@HNHu,.9~tbƌUO|ih>}D9kxzrVt'IN=&_璜 bUXHKho d,$'4KQ@Mŗ^X<>}FGg׮2EQɉ%y,mԖ^(ރOIclj?o!Ik⒤dyۓk[:mGwKc D\`GvN!Uu֦cVֹQ1tH,nj\nώFtq ) RԜS $A#¯9 ,8Ol߹BԆ=mn`Z#'<) ڱl\jTE|cfDr@lס'wh||'%Iѯ(Ʀ>Ƈtn@_xrhڶZ{:mʝ< Ἐڡ\vR5mp&S}WKLʍԑ}amOn<$'Vm{t~l;Òn@GvϺ}jGW cgf➼ͫ]+9Umrb|lLLf^LReaj'W`M|G-+ C#eMQG +R$ժjou$cV];FFtر2DTrLm~xl2Q6?{}ɉ FSyéٟHSFjfO]*jxXfRM FT9zn:WcD_Sϩ,ӧmbb* eqjgucutlH˚*UeGf;ף+UpףnmzWV0ᮅq@c<<ֶ6uuw+Se2 9gkd;)ظ\Β#CHN@/Rw'jƆ_d6iMg^}Ym]?߾TͿ=zU-Vҋl[M=_ҹm>[RPk4ҟ]:::8kL>w oSws{+L>v76,-<?ă_ҦV=oC"o=3}gߍ: 8tni]ڰqXեO>cGzJ#$'T$'%RC?%ᑓz'ޥX+(?bJ,YޭĆ;K~0>ҽ-JV_mIA=vd_sny2}A!CF#G /q#FmV-`%=C?>i}ryA+?N6j*t5/cc9xDݗWZtz?Nz@˩:)@Q[Zie I+$/cO&iZ`jLoǿNh[\﵁f9͕'?0:Xߊ!GaV1ݼcf:==Z?2ڹTOrE<|p>Kz>ڣf"RԸtgz}bE@Vek#:>>:y(QR^;_vҦu^]u۝s+%&ЃwJ6$6-H 㧲/u˙/k}ӺmJkKcsJL1M둃*K[zgI ?xjfC=^RSUj?6|NOiil:ysfuo۬>g}HQX*j']:=RuōxȪөӚX_Q}<3~Hc8Q]!M] ՞Gmt[mvkf uE1?IU}_IRϯ:0|\+Z$<_SYk#u#EHیek;11>Âm9:8=T螂mk&SώL&ey,S"P:}o-&IN`=4{ Iʏ,]cw Y,e4͔BxXHNq [ھS*[xe&ڜًHN"ohٸtm6 JUXpb+ ݿwBt"~/U=_Q@J1y<R6Sv͟Tk'W,8WV~?RԾ'g+2RE<NohK9E1b"֑ 5>άq((U\`)\v\~lJ܇L>i_0US1q,,:1};c?![\e3o [t줬AJ:>8y3"s"ֿ=m0Ȕ?s~=bVdJ;W-꼸pK ޔv ET~{ [}E E"9 {p~ȕVE1P%\kŗ(됝()6~폖@0Mjد]E\k0hP/a5aW1ɤsٜJ={-#أx#6kAc%_y JԿjc'51%RJՠ\;{ͩeAr>9M3<9?//0P@R5+OKNs=ݴysjzfx#\4+<*sQ6|悯4ȽRu7ΩW.z-sj;MX@HN`NW6]*L˟CƨͿ4@9iZu%:0t\cQOK2 ͑A ^A>%ϙ2 ͉ ϹR1Mt`xCc۶T dT.}ZuZ e2~^Ϝ<|0%6QUF>w2/)Vߨ2FU?tLʯ@9So+6^u(} Gow\_3ݵMg529Qn|l;;upPNM{ߩ'/UWYK6~.Zz[cY"+̤>oo/\Ck*Yҝnߔz_wy"+Z}{}I~DP3|{\ 9=9Y2O\$ƚJLH ^#svz_`dqMe}3n&*UelY?Ǭm6,]VNmno'oCtֻ PKZަ?\OjbT:+o &x՛k*1!:9yDY۽ҩO:>=ݨ;`m3rlsЃߊ: $I/Y96LNds*MeruK6sΊ5hhɉ_vS%&Xҡꑃg 581Q/7qtgMN5hK GU9/ɉ_YW^asM&'b Wl<]/Uu)SR3'xQ^D IDATt,WS9\]v Th Hꛢl/v{C<S:7"S%iD?B;f~ }7ɨvйuG-u__d6;W\㨷iI6=z4Vjwk演g1inL9'_x:(*6oArD5'i y|_ o65l3IU(&j"S[;KZ;/eAr N_e6ژGI5F.`stbg\a$'`hO65s}1vb:c::X6+sD+@Wy)[>opq=U^9e)х R9.n)HÈW(Zuꖴlsy<Xp꽸޻7 !_׃`w`7lU/VdIް1n$@IBrBr%r]%.]%:Ll06ƽ"[խޥef?d֮vghG?f7ٝ<ADDDDDDDbˉ8P%e8&1}|=d'v>Q3Mp6 'bx """""" +^C4H(N GAz(@‹fQT5i.eG&8b7[Qs{^[m3HM}꽸5s^""""ݰσoؘ$ec  Ӡ $3Lbf "%)P4&}qb23 ܗ=Ѱ8内z@DDDDDDd)W0̌z9ϗ()WHeˢ^̩SL@FDDDDDDa|r?:VD} Άr """"2tlSؘbsI(=FN0:q( $5Ɋ#MzA8ADDDD$XW6.iBO7"cOuȷE6 /j2[n(,dL",NM 1=='XI "D{>P4n&"""""""]8ADDDDDDDcDDDD4 ^k86& z9ް1C>O!" """"t^:w lL%I֖uHy$2̶blRO@Jݡb E """""I햓da9'hraqhxrdY>%,P\,D{g;E """""Iꈇ(|RYљK" 818ADDDD(Ȝ6֋$cyfeis~ H,NѤak Zkx $)P^^6[{^yG2""D;"""""""rhkC{(XYQMG{#v'8DDDDDDēߏ8'Ijy}t81Y8ADDDD ?lLC^F+ L7%" G """""""]8ADDDDDf+mI$_Kdu:& )˴\ |>3e=28ADDDDNɂ/.)ĺ璔QLZyvwvqcc_%K`2M˼`0cf{bjk(D *tdHR6ư(Eac6D7I Q̤yod048C7~twu!࿬W%E1aq $:M4?婡O 3 {a}=oK{ &Є ̫f#R "ߏuJ?U![N4bh;i;u7I$_c`g˪y9e g"Յۿ>v1qUC}I&&L+`Z^ zg#0-iQA48Agm;yлa?uuz`ѝavSNbFɳ anKbF׉Bo'Q ć}^:~3JK cg'#"3 -+ RX-WrPXH 2^q*r *|vX,Hz$3>?d-w9naC7/_棘5m:$A׫aKӅ!."V+` n0^=;#llc_'x7ߎE0Keml6A4<3G=op;qxV"l烢(QoS%X#uj=n`!2?;ǮS?- #Mwyȴ5_GrZAgϧ8^me‚7/Oً1EQb>gz<Mtyh+¦yWazV^L5`3wT/}‚ۓ߁']KEynaL=hs旃uC]Itգ^ ٥ӯ\FJ cؔұnucso?_p=c}|g;Qg3OVHK0;h^[-l 1N';^Vڒcu⹑}>\k[Od_+JspלUceffuBo h||7H6slG< ѿ͚ڰ<ebF~o8~g T>c6P䑦÷Q,F?Sū&1a*Tw&?jH+ <s _JT4x_Vs7//\<1 <۹LUϡPcY[GNoZЮ#s o FJP]o̗rSCCVH:wq8؃>ct ?Ie F H!fsY-hU\UhhzVQ/Jr "&ٜl]E%-+g/3T^SF~\zEݨP7 kZ3f^@񿎑˖`UѬ1-elŚ5c76 8zQ: k3a/W1kOpٹXswOuX """""""]8ADDDDDDDbDDDD4P6fb@1|[&MNC;LDW'N'=rn+Q"dXl kxI(}cP^^6[{^yG2""$@E'(9/b8)4ϋz R^ZہVˀB5}3p^Ӿc1S13x?Ёzk8-F<,1hq'dmwd8:ci-gAFд}qމ֡`G8gxT}cʃ8+iZvs/bТMq\L]ADDd,NиTӆp#MX=n-=Un<֡Sy~L8Cd|GDD4L``M_2Av֡7fڐ}d3n 4܋~uCD @ +DDDD"DUsq$0N<f)$R,2ƅX3zJJT8?Ѓ-'OXq󼕨+(~C~l;{]46[Diɮ=NoLab,* ˇ:JE{O9UUl.,gxGasmD ˱r!2mi ,r'; 737Y'`"[NFPxŒ^YI.ThƖӇ{NGWbz<٢w*$²n}MM\ Ϲ0 EgnA)7w*qO'Z{Cge9I,>q!ddƓ}Ku7/  #@26VHbf*ma㾹SMI*y> ac>ri= s790U-KRwf C,>5d u>|ɇ휸mtrm_@T& 1?!e 8.'TUs@6|p (WoC|序0V;^| Wd?>sh WFzuO@k Yrܿj=^;7d{S+LMsb㼫q p XX\Wo26cf㯯NEwҼRCiCU^$BHB,΁X |GDdpd.PG-ۈ$J!;y21#-7%cWC͸̄X]RZ8Q]8ɸU-) Y(Ŵt=]R3- MHb6ɵd)LIqn:'ZdSŀwFMtjBD&)cZFz99d 3nnl# l࢔DD45-O@Ŏ jЇ@9S"""""""DET~t@H-((D8AXKA0ɓ(E׳K[2` 1LJ.$>^q7T P|jY4R{tKj~.Û1__>K k?^$'4yd.Dq W_bDDDDDDD)ʼn4?(TP>R"""""""J9,NĨxUANR S%HuL(u8 O5U>wqBqQ`q"NAxAW^'Dmܣﰎ'&/~!""""""J!,NWT%('Ir T$]D< PN(հ8ADDDDDDDbqtʤwDDDDD4!#["G K*lfS$cq.OvcQX J)'%8l+CiI̔ """""JRywc#e d{=b^K_rQDDDDDp9>fP5@qA^l0e8c.cDvIDDDDD U?5ަ kqB|VID:b """""J} ̐nA>>Bف3W!) ` """""J5&<ؗV{* """""Y>:-IDDDDDW9Qi\kI6(>X """"8.۾y_mv"8樖g)VDN K ɤd`HHS%'"8ADDDDDqsXC{,**WQT gMY c=Xl4usnA)-H/|XƼ@WDLK?Aq,6 *ۢQ,M,NQ\,E'YT,EZE ыuY.D498ADDDDDq4YA`j?r#/Ld2y;m>X """"f'KQP4'y;҅//BQr8ADDDDD1˗]#Rpv 1''r(e8ADDDDD1VDgD8, aq(e8AY9MoFwYU +AM*S/>>~_A}WUA%o5Q5=&_ĭ4#̉j:N!aNFdGSRÙV\hNb65qu7|#"tWKF>p)伳?dG]P?]$f\Ꮋ83|>bV#;lOSL DF~%8krG;6sxΛR|ؘgڮI(yd5wI*9T{52tW ;=Y%ϋGwDۉ 5gSl9}8|*YC 8݆6_YTvwDaQ\c_ Il/>gT|%}musd'ɠ(3[ EPA]s`[^(r?ivDzy$ǟj=L-Z9J!*\︇c΀%3GbFZa|A@! MoLuj,(.Y2%̣ !דkn&;0+f "Amwx&l?e& 6ͻ KKgn9]u{A. p;-( ܽ EEX=4xD&LH+^x^=;5݊=xdDgƵ!?=Kg 2gd@9Ӗ G[Uru܂RZYy8=.d^< I(phZ-;4͂z X?VJq󼕨+ ݣfm{5L!4K,لvыOðo.Pj _(y7zQ0+VlrPg1Q]{^H_Ӷ(2EX6c6<.k:=nLE^!18Msb2cOqo19jAse~zVW{Q65eKZ{?ϋ] ~G:JU 8ӎA3vV.D͎#ϡ9Ɍ. x IDAT(ߋOY n+eQ^?'o~(KCXT/N\H6se Qyct!{Mmd21#U?^7Dags4-{(؃cխriQOU֘@ʍzy*~;r>opFĮp]כc:P5pڞ\<r oF8ITiϡPcy ]c_̥3^Ω0[M߄.sw;Q+jZjS!Ky}w Tvܮ#ЎFŁ/X5-\|)1kZ ~X!>\M8rA:hx78q!r<3tl.Ӵy*.*X44D=+`~Շ-%[ޘ02y7֒ɄN;ؒ6/mQd6ͻ ?8ވ1ZomW^wGM}w܋Wj~v""_y-wwDʼn8=EDDDDDSـ!>a'M&EhSZO# m0kȴ|Lً`3En}^.Ųxņţ{3mc Tboit^ lE3s6`NA *{EˈV /=n'6ߣi&#'(f :o}8U[pvgpHƹD Y̒ 7YOw]ۆG|I?O//-cgP_Bl;gnb. t >x ^qʼnxx?8YCL"""""c҈Lh<`AvH#-%PxcxnHt["pÜqJ>ߡuՅeH˜9'o ]KΞVUx ٶ^i%""""aMm5BD|5[툴C(i'uf\1acwC :D@3Gkg-:?Lw+4E2%k/NgHN3,NQ\ OiI7~ʂ""&{Îs1TAKQ8w6Zto^ چFF=-qm|zv/[] WѢ"'e#%.ԉ )uވsidAռCg:7>!edOSփeLBu>R۽VD^54Gun8;}ʥ}eUG8GB0JT2ϞCq5>5yN5}P#NJ a丵).43zUcwzYy {=L1wDiu3(]=Yа߂<4425 BG +KNq lBSp†;Ij|4['>'#e㲾i<3/<>EȴachLvkH@ky?{c!Z~U˾{׊8#ơc=HpJм||}(_JWŃ:&~.g'hpVij %Uq5ZJ̹]E|G sfDQ"^I[э"O/G,7Mi݂rO=Ʉ'pܥ0K&.- yix"mvmHbbFx|x?ǧ߀\omI6k{6픲tʤnr<84\Tu{Vހ;j뫖EJK?:tU-(8xFG&ߧ/CbrZG ~u-0c>apܥI,*󋱦r>nͿ-|s7$1ī+Gf-'.:C}v:|{ӽI,r v<܏E] BrbAyYVEƀdǐhÀFS>jͅ 3t,~Mw+ݎ\w5h][Nm WnRNdxޯߍ,q>z)$Wo [xꍆ*L\'^~ { /lw낫 WuŘ5m:z횙 WUUXT.b"pQYmj<}p۸- YySJɌ{V܈|~ XjAɘSIS':Ct4ѽo`WIܶ)(U21C*֡<- cj8GcZxt[P>ԡd`߅-t4ѽo]|Td hƱSn>ҳ2;g 5M!c.ybT,N,P"/g\LfxC47+7KҜ|;*r C'|"(伲<̹|zv^܇QLwIQl+ܾ%6:;I-'(y=3flƴBXVSb#TUD_OI@iX?R& '(kۀ|uzF,_٬cVWr ѣ\A(= tL|R7u0YY(9s4o) """"""Jiw& :f^n^ޘתӫS&  """"6 V11Πn2UH$I:f2+, """""ɍsc'da'>ADDDDDDDbqt """""""DDDDDDD+'HW,NX """""""]8ADDDDDDDbqte;"""""JmB/T)! Y/M.,NQL*Cug^"\X """""""DDDDDDD+'HW,N8ZMyET4L; #&'hjRTk< uw63Iʦb~E """"rTǡt Jeȍ]az.LΈ(blEDDDDSc&.;V3!JQ._g]r-'h k3o}Fwy0%HLgU`N;E9_ {PwhcY IC4. Г%pXE?"=؎>e`nA),Q֯m ](N7 h\@G[;nt GaV}* u^Ν2ѯD|Y ϥmߏpv*"[fp,V /a^3Й=r:9so` (m;[!}LȲiE5od}@C۾wPt@-~lA@N @D̚62R{Ӵ}r\34.o bp y(GwjnkH*P1tv`F Y{8|<+BZ(=ו%sYޯBԒ4W$;acO"yP\RcFw!8/|O6_X"$Rp߉DCm; f A(;xC^˖4Fc]Av6kkՆFyMVa4!Yׁ}mdibwvU?] 8|vPm>r?yw kLE(L_ p<8rPmny3=Q(fG*co9,r1T썸0q>\41N.?X&ĤKpY vTghZqaΑ ̥&fFMzl) &m?kZT5WjZqa~ zp%myo13;Ѩ8Zi!+K*T4w5mz ~X!:W:N܇{s'I G0~tcJsY-hU\9*X4 ~^Տ~? DDfZ9BQvRH3 `^5B1BҊ """"""2 ,RY>a70aaK QfPj7I """"""21+ bTa(%|3|`{BMDDDDDD([DS4!ՏvMv+zNYʩ~+bĂCּ}}+QçeyVD\jbH4ZiymzraVťxT c^pPU^έ^%Yu hھ_UЫz5a, NhTZh}X @rމV<>իiOp .h?m[vG(᜹'w}/`W - """`qUe˱Ӣ8cZSqǜCdqv S}}YcG/T?d\Zx`@aڏեx1WeUcpc߇U?c)q8g]'NF9nQ:[Ϳqw,3HXY76';];k!KLJ^v[?'-(.#w<$ex<^>+l\iv>~t痱h$W=l\=?n$ex*T][D\#"pڨcZ|[w_ ʐRMw0/Z>lBܷj=^qCJ MhȘ.gI*9>WT$1[\,?/q믾o00n̴lmeס{Kthx`䭣(qf`M|l>y +ȝፓݝi,[2RU-;YW,lq~ܥ!d3%%^иopQӳ򰠸GZƝuE -6?;+L7]8c9I*yW-Y `\U-cqt)&3r ~˜fgCK ln? 6./=5q[07 ՅQhOdҒMr}MDDSDDDDDDD+yc IDAT'HW,N!&E}~|ggLdΥW_Ee'M""""""l=DBiwʱP:چ DDDDDDVk! cDDDDDDPʫR4hc """""" ~ ho)sX """"" m^%D,[f/ka7[qҵPSAzN) """""" nr?Lݍ5x{@Qf.>&\3s>Az|7^qrA/݉mGsP罠Ɍ """""" 뚙c:p5U->El?&0KJe,NQX![ rϾ[†ÉIR;$"""""ө:gBDDDDDD0^7~+Baq _woP~ij;>/noLNSs"g""""""xʢBqԹP{?P~, >4bl -'%88]sWA-pԠ-#5:@8A^D5Aj b/&h &D@PI>ebr\*nSi q̾<-2/XxA] 8P9 ӁX8!DzV*I\ʘ """"""}b/j;5C#gЫ8V}[OuX'L3_8^??w3ֶBn 1Aч |R8'<I+~>״.+2?Pywyx}~&񡶣?e)X=%^Vֈ'?zzk؊يlx֡X0n:6~U09_2n09ALNuc\KF=V,f^ƻ&~|pޟ;kwZV\_M{3,*?:VEFu{3B#:v"ѴJpǜلUg2:&Ԓ&_8xgͯm-bIhD}m-Z[,5 Ƅ0 ?~_1&r_n{O_AaDn"ܑPdh4ZHG5\6/wF$hZFX+"̽Xp+ҴiW !, YS0Fʖ3ecE ZfVقNGw 7'..q^T*5db(v^`-`WބqɐDq bPdFg=Y\ɐaG㱕7anb&*Ո>k::gZa2z\SA`I‰2<#twWkȲ4}4@#fj$ ==/Jδ 2DE|cb<FYΏi}L+D U]-x~קȯ-wYW[f_g-Ed`dgMA/up!3nkལ;:Mw$""""`5NABh$5g.J'k%b0b`aQ~9x4sm\PSqeh[NdDŏm49% 6<+D٦uj Zr5nul{Fl=};vqT<JHx?JSl=8ֵ'W2w3?䊻qxy&aw]>֯I׆WʖM÷]Y 3sd' X м!3zC\g |WlBD`0>V : =(GwPEq'EU7cYЩڎsx^9yDAgu-x? /r~.H*}|MsJ e\(kp眕H @-+ ]bǙ.'kmv];}PR=F """"f'f 5"!uYWॻ^ :#g`e\7}޾Lj ͳ.Kw= T5aoY!*[mܞ¡ZR!D0z!hT?[5y"YŻÜLuboY!JWބgo=ߍ]nܞ,oI4LIF'D D`GKR#-2L[>wmeFO»<+`Kki@|HXu3r˷gr3˴*=+qYB.AZ˺W,@.L[ಞ!`mHBa] S.AnoOv#PCK}xN!KР^l<5tA٦dZWsD/,r쟛+1[RzXw oڊBk{y=…c?fmO'.@5*?wm3x ~ځRk)cϳ4"ُ65G/y<k_FXG|G]*{TooK0G] k]\mgN~w z<γjDŬt|[O_^y_w5wj68?Ywځ)r<|^_yBqD;0;老+0XlSeeԚ;8m= ?0}6:٫45Gj,=o^`EVOP} {>E5C$%#1,  (i|g,º4wնiEA#ˮǷ/Oۊ x}Qt/XWEQa@<$E =`)Ƣu2:2ȖlrR< (>ݓc~Gg#?8ш>Qnk}X5f#UEvl¢ ]sW~Kt\3:^vH*|!1omCA9hUj\IXh$}oƙKSkp>›l77Xb_nZ/q|3!+ ^=eȸ{-(b]\{iȎMB]g+%OԔW|}: 6{Jjƣ@J*s֠Z,M>i_d_Y`]u8][>AIc5tjmWm+g o&Mj^Fhʹ?^IZu4tyKDDDD4d$Fb_Y6n%i9Щ(iF~mե0MH*d$ٗ_>v=nn3'H<Nc/C"=imOOA|H*8/,˘J!cmXe6dES1A}Zf&_o}gk@jSL}"056fݽGQDnDDDD4gN 4~v͚oP7+&P<͵(xl@۠Nr(⓾}7_sX iotC犱Ϗ h7awqG 7 "nm'y2156SQ0 E_K gɉHƒhO _'xU9) 1wrD $AUdPgw|vc à[Zz-+DKJ4p|;s٘msmv4G-'hPk@gN\KJ03! a.mr`3{W7tcg_.f Z+2gPe1Z{xtvVEFe*˸vBjt|lxu)Tq+Y $eaσxhÜL/6kښPXWtKCRcY jI3c2: l?xK;,/Hc7t ~ClaA8h9.W 8Ru01&Ԃ8f$qodDDDD3Qshj(9nb}3at އFc4p2%:.m]U`% g#V5apEq^9٭3rO"ccQ}L =f|4p֐)1 Hqj4_00(5Z=v91[Rb`cFϿW*wdU 43fƂ+%:hiCwbǙƢ+6k}pG_ O3TZ=`[U\,68 M_*˃&^9WǃD|h*[ɬ}~wÃejI H LY.[]$'*؋ӡT0Y-׷%(ʳ'dki@YstCjڛ1}R*B"P1x%OHʚ}hf'dW +"7`OYᠭR#cqyl>^&&'.:("68סfl,s:UAZ=:I1^l5uc˩#ƗZR᪩q9'C7tt :k6rR%/D7zL-+ĉa_6s'C; ?}h1h`hC1) Krэqdc*[QT_BV?=2&F> э-YQ\y#WB"6+A$LDUmX0h0qUgۅɓ<蔗t,M#ڮJj,)EM ēnqXfMNז#>C?~   +Z5 ]ma\LRdcab,N3' ".<&vǼl? kr񆋩N\> -aSp}:]CW&r `{?VβkqkCe[:Ax#!4ҋG] VuZGR~iq)^l}w 'ĦMN# +nk?^{'6oǯ7Ət ꫳ'sX?˧ s|4'|~~;J dt aqT{YiS$ebxRQZ;%*5DAV?;S S KXV@ MZ\'>;oroچSc2:^L|G5 klv|du#5"Sb.E5dS!0c1db`l8?SŵkP{oo|uYsf^7 S3qÌNIDLJoDU7Ȳ.1Z<.uw%&[k]y$-Ad]byb͈ qZ';6/pU -py8͘LLlxg#* S HQho,\uhgMhDD@0b+YA1Aa9) ?:,ZRaUhʊ`Zc8e&!@j7`O8\ܤOg&8η~bLgѝs(Sj Tvn/i)^rl)"J{oD|HbvN_ lzmsV 7ϼlDDDD5Yʋdn/9)XOu'n9g%~s{[UiU`7`kqܕ]鋐Ō o4.tmp"xx5HO} ( ch;EW`Nb&f'dXm/5gyLOI5Y ``Ѓ Cwa~r]wydEq}MX9̻'kYA:Q,}:=v 盋M3:, ٺͭ< )։Jl419ADDDDvYJ;8ZsOGFm̄g Io7*S`u>pVqC5`mXY4uw 9<ٱIPw[ESw8f'fbw{_yLkƮvEiSYMow|^3q,*5b"㿷t#Ն/EW{6 ?~ I\7}VdĩJħ@D(+79lo]\t{l`[Q4s):ѥcYD+KΨ݁5Ug㓶X^E1^ص,xpU(kCD@0ǧ"@ESw;g[@ #Of#crLI@_=^ek#N֔!%" S|"؋#\y3klcW$&^9uxt7_soZOCu&^i `ƤTąD{>sz}*q竿nҴ{Rl`oY!^)TqEaAi=?+2gb^dćDاϯ-]߿V\> Rf] Fbvb>/9w:xNC~- .$Pڎ3Xy A.-=No(8 'z[CN3 οg(iw{Zq!e ]mC'kʰϏNA9M{AR#>4f;`U'ו8+ۇw X`8vr}^9}sM]S8UQZƂD#ĶDDDD4V *[^cЎTAYk(pk=tv(_3X(ksY?1 ƫCe a iuNg!(Cwl&'{Ḱsp)_AD.7X ;?ˡl(80mhܷ` n6DDDDDD4$xc uX~x`:|o,k@lS 6419ADDDDDD.Ign~Al'ن08c "_wo)uy;&'ȥ9IHEe[#n~6e/݈ ǧ|Vpn*_MDDDDDDҔӶ~x|CbM#ٕoގ@Kw/B """"""r)@Զ Z^jfCݠ( qiXE409ADDDDDD*7 z@u1CVRd2j:,B MQDDDDDDDҋ{?Nj{?u.a\6]@!0fCWqA-Av& rD>LN_&Aa} p%@v&ǛDDDDDD䖔DdDYK fC2 &Dz&ȝ,25Z5@H12bbLNѰ\3aFՂ .tjfB5;ס0ƿ;n|IaP FIg]|*Qu4A /MiKskt\q48->?~/{  v7,˘0ic """"""riRH`_YѠ*q!^[$ lf1H09ADDDDDD.%ۭci/{#jЌYcjɥ:ȧ """""""b""""""_ ~Ng% Z_^G'Uh4[NKoفy#FKO(EC """"""rЃNC ?nDDDDDDDSLNO19ADDDDDDD>$"""""4;&wOY=d==K'G;):dN|\zdX&i'?[p FT"7=2cӇ' Ⱂ ++UEJNDk:gf0|:ȧ """""""brb4)^ DDDDDDDԇɉQ8!=( $_Rp&&'FIC^ *!""""""8%Qﵨ a43Y̾a86ݪȰXOE4fsu|&ŋxcn8Ο?~)Eُ?~n(QhgUǥa̜)sY_S@|$coBekEi?U~}_[^\wYQPPw{xY~^ψ(H?y)k D R@n_1ꚺΏ\yaioRDm2ۇfMȊζ^r܋QylMJ͋QyۇwAoŸ1Q:ME+P/VCyOϖwiRDSтa jdN'1BԚ_O9P3˞p(f LmKQ[Xe!5P~sUQ%IgChrY7 \3aP2LnTussd j 0Xڻa6xGWOFmGzw?j|GADkۤ-TaAK[=F!4qv`i,Yf ˳'qe=YQcMV.r!MMtj"B֮^X:z<]dӣ-)a wqI J sK'd* 0Kϛ3PSCG#DM[V8dhY,IAFKG7]}ִ"`DLUx$EqO#>:de#_) gMA2Lu^3C =tjT]X7dDMnϱ.{C!07{֒DAkik"{=:, QBڌ22(֮&]MM]P,&(֋AA *(.'"/Z4PG.hjǛĊz\Nh;ANM&fr'N$1;7~ wk3'4vq:qbhV w~9b#?vOj'쯿͂61Y u{&Ŋ׋cEXLgѩ [NG7!jg܍Jl)sUug.zU'zFsj'Fa_Y`:)bRx, C J$YPp:ͣXp:ɚ2{מ̐4n/x>d$Y,!s/l\4s/1u""JNLkRE j!@A,IUCرT &~ڄAXul`bB;Y|쭲aM}uB}+PZhP]zJN)[F c0D7BAH:2]b'!f#(Px97/7;YJ j&ܴDD#49r_J.,dZA'EC=&ס bEtfJ;`M^Ճ7`VA۔ -9k`u{$P.H?e&.6(ȵi"$%%%hooևg,k)H eKY{#֟Vƍs[*b޴DLzXb|8 #$2;a}qrBH/c("}(DJ "IAAԸ?9ML֮zO_tN:ϛ,9|_^ j=iȍ>ZsվeHS&A=?Nhqqr!=z11!Hj#!CF%"""KgcRJS~`&`D6vB0Z=yIXOUCi9t.Ai-PiB}8iK Ofb.琡`}EuAät=`bmrGLC#ﳵX 5kI!DB0 9]Zdc~(PԼX3*֏%A=ֱܝ2qHLj'$ 0"u`Zڊz(mgrIICM4T|#쾅&~:DDDDtIRzo}VQu&徔.A [Ƥ_S fX%IBy>j|GHh(JN/erNAP=J㛈|L1;&Du^u=&'ܦuɱc☘hZkyAF*멩[ 4B|uiRk[z6l+AҴiƢ+^sw}b/? us:vza,eݤh<&%CzLF-+>Aڈ`<&ÌF52[,8Ru-ݝ.Z<:,Me݉*8U_gw|u%Q ኩ3P):I3I~|\xkW]DL b4؉&  Q:+l7`3Tm5a碉7sux~E~]Mչ}MDD~6&""""""K`!Hl5ADDDDDDD%GDDDDDDDKInSЪ:$""""""P FuB """""""b""""""")&'ȧ&T'$ :& Ob]:90Gح| """"""")&'ȧ """""""br| """"""")&'ȧ """""""br| """"""")&'ȧ """""""br| """"""")&'ȧ """""""br| """"""")&'ȧ """""""br| """"""")&'ȧ """""""br| """""&,E/ȼ IDAT[w19ADDDDD~Ak^wuv(jZЫ S:!8J""""""OԄFDFG8Ke-)j/c|] û{F}V@ЪG}DDDDDI49*((8yaaqp㏢(hkkajj"KDDDDD7Tsa=h_ֆ6F5qHq"|]؏@sU.`CphhOut-'g(ʨơVXOw7,˨BDDDDtÃy!O5xl5ՌdH|fB''P]U0ӧc2] 9ZZZC_a zg"D$ Yh9|tА5ZhC܎ꮡV%`U- Mƅ ۅp_1&Zz\iR$m2d1;-oo3ѵt;?|ϻz0ѵxϻ(Otz}M4R 22.Ie:!U,ۋꑑ1d=7>tĸamS 8,>PǾ8dO񱟨)CSwgN:S-(;bt{Od&9-/nBu{#"cGqm]8RyƋy׶N(w=FDD P$?)3ׇ5.sY獼mxN(ݱH|mOY~u7,xJ0UeS篡dtYolkcF-fnzӉhrޯd/F6zF]&Or-VY>SG\4KQyۅWnu(DDD#6uQ}_6#*"dz&^:3:Զ'Y]Z=oWɨԛҫFITRa4ݷ*2~ " K~4il7i=jZݏWTp6Єx &ɣ8 .o۞ϰ>o+c "r:e-[TVm  jߊ M4I7zvy:jx~tb/>+88D ddؒ&b #"Rz=^벵LݥXJ^5B< LP^dAnώ\ma);$G#HGQ(?k* dzFkNG׫Y ufUmM !4a D%L2xvΊM0 $ETZ+5duiV쵫d|[9$c{o7z/ ELP,"R=RkSn_m4|wE0j_NAeC]!B N.9Rk2_OP;;!CE s&XNsP WJk45!Is WN4h.BOX}UϚMsc!^sRG(Ho:E%;1=8\VA!3!X{44JiqlZLQثsց_D!C/R& >鮄}[ypz_Pdm\7c!JkVPRy_?*\p9apr`U%uU=nw=˿Jf|9.~뼾cmeŧ$/my5]vh~w;iz60)OVp0'B VJUs,yϐ5qvJj*yS;wR_wE(&CXW?^v+7/ȱֻ]_Yo+n_2a&kأ{'⵻A P]es==nB8V>sY\pǍ:=cM?q?ny33s̀i }pnUpՔsG珟3WFÎwWJ_(}}Gۖ UEU j`c/fufPu&^pphvyi)*zUv>͇[M!F7`B`܌}i=Nո`?)#rzt2HI`Rzv͞0, n5, h|Q-6.;|^'(P{}h &- = &`2<2}`>CO۰ܭFF}I|0~k?{L sΟw=?t秪 1_c; B!P!B!B!"J !B!BDB!B!( 'B!BQN!B!"$B!B!DDI8!B!BGWMl@6B!B7C&jl;6XB!B!Dɴ!B!BDԐ91556w>B!B!!NIRRR;o^7}q:8ڦB!BqDTTI))}OQ~B!B!%5'B!BQCvD$)f̷, FCB!B!; '*(QHB!B!dZB!B!"J !B!BDB!B!( 'B!BQN!B!"$B!B!DDI8!B!BpB!B!%B!B!"J !B!BDB!B!( 'B!BQN!B!"$B!B!DDI8!B!BpB!B!%B!B!"J !B!İB):BtC !B!İ6!:[2slP1I8!B!&D's[ J}P1I8!B!:"Pä"8E!B!DuLk(^JGKXa!Dxi}h>N*<wP屡'B -N!B!v (bT#L4%3^QI[H[gJ⠣=꽎#P$B!b$hj@33: DT`!Z52:|8l?A56Jd“B!bXK0.EǂLnL))`3kJI\Y B!b &hח"Io_wuᠧv=>=mxs"PqsD(F&}@gJI$,A1˒>`"dPη7]M PyIJcړ=>^Z_Ÿ+px>^R_5X^n/pgs3}׃A7<d'`NO]E*s!M?3-i5*8p^T1R$Ű4dGNn쭭}+G> /|cx^MU=>Oiu97awwۻxlz6goeh9}O fVnW6p@.wq{>44^޺&bPpO Ľ)-,P0 KKs"!Ð|RZ\Liqq1lS 31".ׇn-ل^ֲ+Bc2p:x=>ﯨ V^ mAyT44*#MFN6Tbtqkh4+ 7 =n[Pí/o%c3PUV-y>z֨(n7n.w^fG+k&yNҶl~JO,QV|>N{pˇyTTr'>a^_#\t5a6CYֶ{<]fGkp;TvU+p0Z;.L!d 5FGzqg؂_8Tw?]Am N67pI,ѡfV ig Fcп+UGyK-].GjNr|bz8f&^wf`W*Ufc61Lmk8Nxd*, O`9OZ6"`¤rPxOX"ٟny*q}'m*7.~ZkNTvN'>ߩ!PL>T WwǼMdK jq.6d^9] >`A-AS.oT HW}nVsU94&Ƴ΃}ޯ] u~J7`M.ˋL%0_^wY Ds!DNl|.`.Y0̠] _p'K d1}ޯEsct,֏ vV43O% A;WT]bP><UA~g~>1_3w&s>9oҨ06ӸfLc z1o3k<ǃ:v%N6>rQk n7@CF{*]Q9yukq}7wdommJ?]fgPԷj Q"Vo恜٘uT"Mȉ$)))S; !Bq  1h `"BD ' #ۨB!1)&b_'6[S P(OgB !NiqiB!g ܜ9 S_P `]0EWD; (D6!B!Ġ6!:۲fF_jJeLlN xv]X"DpB!B1hLk@V/P &PLB !3C ,Mǥ{޷~ZPhAQBx yOP ñДޯm(!|v@ G5~Ӵ~~/L;Ag>jB}P޳ʩlpm=?k=RB fg42bS<"Dx%h'S<@2K&/%:RB!g3YJ4z &:ԍY~p &:;}`l^"d)Q!B!Ġ֗`A: Ab02hdzHs`4VU3R}vex_ȖpB!B1h5h׀"S&Wd/ I!$/=>&*:t b4G{P9hLe1LTtH8!B! &P̶`xc'E e, gfLvUr;hQShVOH!B!":h[@1ے¨1.bcҞqK6=!#v^d֘ƣ_}8;!B! K (7VwˌmD;ES-lE7j6ih8KN!Y575oڍ~9iWGkŒ-־B!b9|-=ٳȲą#(`]GP/#(zhpMk!\e4DBDJ?+jSeB!Q{@g{0Ѯ_FPd&]FPtJ~ &ڍPZ,ߏ_,_!b)*tm_G's|F&FgmcY4vG`IGUdh1P$D*2;^7jW38IAL!BAJKJvB*.RgFU|Ʀ}T57HAy/m[M=|' Kp%LKyC #9'gRG OeRz6FF~.uۛY~YQS}32ƜYo`fUEaAqqx\(~Då7uRrx8PUƁeAK =)<_בgK wT>} ($Y @*ėbۉQUq7l̹ԨymB!ؔ<~vUl-=ycϱJ,IQPhu9MJ&fկ`!Mʏk?c1t5fG+k&#./H=:n:7z% ܖ5rOn8nL&\׏f_|&*> $eyH!~m -:jKtT0qV5/W G9t?b$B!y9yLH&Cv̖҃L㦱GWȒ|k5v뼲S~vQZ$.? vO^zs. ì7tH奭k6"~ w +9} oXǴrV },C9mq`o$OoxP~bLU}ExH07ĮpvkR4XZX9I9sT/^29HUV4YǖKI8!BY )cK4vF[N>= uxcs:H#W\NuK#3v9: LHW}|'kmDckp>'7%WQh1S0LÚC;{1!`bt fM?#hB5`k]cEeMs)T}tq^٧#I !B\_Ίg¾E{PW_#Ҳ("V*H9=c4Z T6e4ԯPm:/|ɲ:_O B ($@ &7Yvcua0?z2Ζ?WI^AglB!DhX=cV8ڿˎV(^VѲ[:QPɲ}y"–Op6v?†c{SFpC8=njZxLk()zP%3ESSK0ѽhHoՑH n%hE5ж51N֮l)9O/NOcl0.Ѐb5cP͔sIjjjz;UU$$..SBmz8a0kX|zا 354Jr-B17䟏5S~MV ʊ:2.:ݳ[ÇSRWſwo 5Kf3!5]Ǐ6YX7|VTڢ}ν NW-!_D^UP {[@1ߚ|Ȑnb K>jٳg䐛* PDZ%@/EF/,3FN RsB!" ʊ_VrlH^ɈKq'UrO .F'Hߥq0R ;gk0c 1SY~$l#DT+8!p:ٳ{>'QI ܹ{]val} Eaq %{>{$uԜB!4;dG>f1v6Ў=`tR:̿%pz+ض`dlN'ed%=9s:j gP8}E ǡCػwo墡&Z[[immt1Ca4ZXVbbbHHHd2Ѧ>|'7`)FQ,ᓖ0V~ZWo3EN !B^v%fcPP^īOM|zx߾:^{kdcQ\wDkL}͎Vjmw.%Ywd7㧜h xoo~}_ N((&T]]Ͷmhjp8zvv;>7VTӱZgw^5k#F?z/&S$Ajh:Cq)2>> N!BD?}y91Yx䒛q]ݷ)MVE{#?Yv;{%; &fr~)V,S5\utJ}k "vkD,4{o>.F$l5'Z[[;jYőMr=7nӧOGUƾ=WP]gD^`#˺S[G hPbN!BDHI]l )*]yFws[;ʏp?y xz}1ٜ’ _;Mmw ^Wp?FMLY) >N!BDЛ;zPP+yhrt!SQuƋw|/G*?w_^tf7/~L~Xv?ʵiP3OwaۦRv7UWd0Q__'|Bssl޼ȭ\QSSÖ-[nf͚.Gy (X8ڠ3^O~ lI] !?~Cћgܨ ܐ|Vޫ{[qSh٨ӿF9(+BĮGj|ffSZzF-v]7FS=j<'^TUW҉]˼l8W},à;Oo h~{}=TEq+4K ZѸq_7~z@VmCh;yUNUĦpZ`O?w#G8x >_h QUU"))=z)++cĈX,JC>1WCkhT-v9{ѤRW'.^±FJ \ɥ;FӠ5'<<~NRs" + !Bcӱ4[Das9xvǜlK/l~qg{~ ~+tteFV~gؚȌo+y|mxUڜ^ܺ#rHIeJ٠ &v;k׮ &|>ɓSO?ngԩˊz<֭[Dž^HLLϟp|(ʃMMѳժc%uԱҽ(UjJkeK[Wm1uPD2||l6v`SWWΝ;1cFG@p8X~=K.`pb($Ø}2c<5mip" NjU<ʊi}/ҤB1-=}?~7$twhSd}rj>Ծ6|rfen&Ym'4o\[V}1t5(Zl;w[T47v+ztr̶m)\$5(:=Oۚ\ &5``e5n2r"Jyo /B~]>hqlÅhrgHybwq9.7ֲqzkW$=?BQ2Px|^s\t :UEPU/l&go²9{Ϧ`V'K0qk('aEnrVt3Nf߿ɓ'wWZZJZZG>L1 #(3; qR p"8<(!8uˊOaʈzᡷ\bQݾR2XL}d|/E*B':d\OJs03s,q(jlMl.>3Wv;b9w SGb8X˪<{T6uȸ$?R`–}ӇsRfdPqV,Kob|J&Om|fnn_ ?xzFc}Ӈ9w ܲ]Ǐ==c4w] S`U!yu(qI~NJ?Ͽ׮d?ujlMܔSVcW܍`$%:Q)$GZ[%U4;ZQk4cGb6{otR:[rVT$'px8=jZH>՗oU?fU ̳hZZZ())8r$bX0#i. cǎ1bĈE+++#1oiu-) <c\ٺX_pE`}(0fVtjuQaeK8Lu`h@q/ԹNfe͙5[%gq󿤬o[f]/q(&hqdL, vN_AwtJt LwɊ]_ug,v58~W`FvCD~iTB*M}/;C}zh.ɛ͘2bH #.Kfw<9ydƧPPyPP(o9#X2a&~Qv;| ?g6dR~u$XM~w_o孝vV^1-#+&z]b5WniG+gs+N瀶8N5t?d2KZZZG ,Guu5GnrV4Ç3o^WQ8q\Aī3a)|g9ǙFi ed1Cp[8QL(wW"Bp|*^EӲ:"Vfgא÷/V<ӱOvb*_z#PP^ērp:BF;7/bG^ Mcw2#?&>+*~x˧-`Vnrܐ>xF,29Rs7)|눷D+g~??vlJۈQT}'ї9y`}[5)#r=猚@Ay_`FƵ͹g8Ř,۹lҜ.l#t1KdtLzMV_Zkg" ʊgprǜ%~Y;ۉE:V<9]Jt)c"&rFE4S+ 7jB4}͌30{q(UUIKK#))Bj,/r16,K {qr~p*z6sdy%k#g'yhYhP,4iEvTiB!ė;RJu7/Fy?vX>qTny>1xḙU^7kljGH\>inĞbzw,v~Jm,o6v{{7cGcĈĎ Iwܿ1jmMܷ?,*Y m'oY3WaM\3NEii 1A5uF qLjdOC***FI444=BA3}=uT6oތ}ӸnjjjHMMV(//'77{31:?`R7ЮgB&Ix슻`\ۨmDcm6KOW^;Ę~s,mW7.'v>lw1vq%z͡Doqr}g茊| ߒ|vuGvwy3u~3v2{vLYwdw"oXsk3؋2 }/8XUƝ/^5o`")*q_}VpKmĴ,Z.Uecx\5x|&cR" [1;um***n+++ Ԩ!EQ!)))~* 999AvsH3̩$̽o(J2rB!YKoKo򱽕%<~}okm<)2& _;J]|_m\r%}{ySQ̢NLrwJ|ToiY>TQZ_TK.9ig<^\tFR3j)Muu*J?_5p$5&ĴkrS7|!|ƯV_q"iN;~i*:wV<ëwykRqW4ǯp#_ރQ1*]ff&A7$55CV]]ԯ M\3t>n>s:N!_Zo+?x3F-5[|\g&%d{1ӭ:PyeLL]ɥyۦx}~g2F8bl0v1o_p[6\/44r3[M IDAT_֛XQ7/m]͟Q&Yo/xfJ;SU~{}~͡T7_+{*\|srQTLJnyWKk1ß:7A&3Ro GcccH+cX,'M&Sjj hDNvE4HNnx=&٨s8H8!ƕ:PqOd'wL,ϯU 9=n۸$o6G+[[V> Y{o[:t^bAdv–Q kvs󘐚E% ˤ/M c&w1;gvlEcWgyDOtP4*Q(VUG k !DϴeZCƇ ŨGMAbo\^hNXffLz7ݛzӱ|zxWGX']71Ū]/7;N?Qll9|#N$DFZ|}_dk͇NУ]=) SԨ2 = ^Q׃J vUhQu4t "P0F1th6-tWC~.i94fG++voYhTF'hͩ+:m+V|| Ym-6:l4}Oi}zI~6n_Ă$FPg ]ZtPuˆ_r1 &V-*l ''vL%D:zxsg' h2=ct;WtTA KnqWMJtam=GUSK8q0SpD7r:;3Lc o8 E$z|X}^|^}VҬiRhCuX>in7 ^77 + V)^7AZT:=Uz#@B =Zlrշ_x᫷a\4߾_ 7/FUw1?zy턬}Ю:[]_|!lky[}h7/ =&pBY# leWUѱgx;cZG0U9cl ڂVڢBfƥX#&oyz51>5?]0z3)}+ ;QPVW}`3}EL ߾:F&Uv[W:F&m˦M .z_SS冪DAGs#i'II^{{b4 ^@{"P3P7pRg+l8aHp vPT* F FZȧB4\`bxWIE?)_/bݑ,;+VPckDCds=#b9~ ԑؔ㍵A_NG+BT\6i?N) )*WLSQgkOQ@݆WӰĚNLelHgƠ۟7Z̄Lz)jlg<L=h[t\J_-1< +g+x^|>_1ρq^#=.F]y(=uu3VtN] %Ѐ~?&%mg fGAAQ>B j|5m*BVv6I=nKKiTEݽ(OW߲cam΅K7~o+wϿ#sy1b5oEkLG'ֿ˂IĚ'ܷZ[3FpL1壳޾m{ 3/g"*Khף bY՟N^ FO^Ocs=7\y.;ʋ0͞QLś;ׇ=gEM w ѡlBj#5g3>P__4p]y:2ss';Lz!$!^cY˺`-+Z.TkH!LdL2I&<C EE8UUyC@AǺcXwlq˹y e`â JjmNWjKiʦmEsօlbCV:˯lQt8VVd{k֪$\bذp+kT ymBضڡpeW|[V[DA_OD%5G`?{GNB@@PW1Ea%b>Jt?T* ?Z F*juGf#zBHhJ4H39 )")VlU I: d$\1u0A\8b22(srJf7OF~t,f% N%fZVeu,d]3YkmZM9[We7ٺ*l\8 Pf۝GYxUeV9WlDKj˖=S3"guMbDLh$:)G#gOso4Bhrny޽Auj'0[-g'fW=1x{6ՎȌK.6s9(375j&NTik 2b/%D2C%![C-I굤 ӄR@̤!:.J:d2+8H7JUIN?$0ϸ5MiʮǸ-i # Oͷ;[ݷ{6;mǔ{FV&k98mt [h4,`jRyAԞh4c杌zr Z$lfc`DEm% Dc"D3C5tЎLGG@ঁ18;nN 4*CKj+]&;iQqN_#J.-:{6PVW|NT?dEs'*J>|vv瑱e#z 0?w4`A4^(  &KTæzٿ)֡zH>ƤfcaePAj(=5*q6L';?2222: Q K<#2$#箺ݮѲ"3*kynd۩d#{gm'=?XjP[^rnEʜq19ϒūO_vhA*ٯ/MQQQyoO6˝8GC]-A=Z$ZqFYJ$.oa=!lbm!|$P I4B(!MZ qýQ+l_-oL 36 PXU£vVE@R #PQ+U$GP^_Cn뫾cݜfINƇEZ_%$!/OkR3x}G3ذ;Yۣs?[EAAAP^n쇆VhY]]m8L&$&&%I"[j""ιᄅ=ŌcLFko]t7ԣ@hoDJ&fjk)Ȝy(@o2@\X$^G4yO Rj1 A&<.γ=Qx~o3Kg2RXUfӾA47,Ve'N;g_青ń;9l=y/e I&OIII6q 99cǎWaa]EEE.۔fBDaaskgIJJHNn.k0vX޺ rŖcaP./ A}-1^ t"D3l Ga +nD@}%`=S`jߑ;%^X(ʜqQ ]xF6}.5`ZߋLyiTُٝK~6f\!R\Vىި3-*8q\Jҳ|ƨ KqG\dĪL322Ϸ=vG())ٳǟK[$ɭ4TVVrio*Ft)~}YۍLL?m ʶ >Aᗼ\C=~1b&[x+{ "a%ل :#Zvf9^H߬MAO(Z_?ʼn~閘f{hz\j`6y%'AS!Jly} _n[m{ܯSl/zrP^c'oeB\9Mٻw/%%qiLYYvrfdd]˰0=5=1Tum, @#W&^ tp^ktK,fC$nfq !'! m -1D'HÂ鞘NNB]]ֈ {b'ħ ۮ d%#J/ DvX[탔Z9+9ٱQٽ=> Ѣa-|w|g㦯êe_"33f3{aǎSWWhd2hvΝ;1a9=6^H}uu}B/ӻPQW@ ruT+T+/!c-^&Z+;;akBmT!%2^eR &#Kme_ߩ+q?׼wn4_b[|(e8L 3[? s֙|k-/zCM$挻]zl(Ȭ5vZ^Y-8FM=S[x ׄpq<~=Ϳsa=l?bV6X^AիkB*8Xz{Sjۃ%Vn?Ճ,\dd懚\K9St҅{ OFEEkWu'K5QTqFP#hg1舾8"XױM7 p,rB!Y,Cw(X(P]FFFFy ʊШԌiT1+>{F.I1|:1GP31f4*5SFhqT( %28ۇ HI7[ņF魳;B֨$ V%̿~V Yq|:s6g̒HZM/9KĪû0M=rAjGGAT٥K>]ЦKLȹ?4(ؖv)uOh vqܣce'ذ 3햐pQQa}!>ީV*tXNNGl@ ͞[׮]h|o٘յ'0pDBHѱNH72|&NdD8g~&_d& %OOX?u$Gr[>~A//V| XBg~y yAP=ܮ&l⾯bfK%"><3VbB9YQʌ2?3?ˮ=:\MztI: 'QZ#>,N*hRPzO!N|J]|UJ$Щ[OKV~ׄWPl>(uFppqXXX@tlU*ݻC ZOHkFMX1j 'D$ /"Ө'V NU1!"8צǷ<.:Esi{?\}IQZZҼP;+.Jf%Sl33r&-z߭g$/%X 8战p9ഩrMT7cرc[R yyyl޼٭b:wU111 _b|_}\!L0ɛo2d2x^&# mDMe߹O2, Xt~Ƨ0tG%OUY5*5a΋˜ Xk讽v5<-l:~NUuXd5eBx]ʁ.=c-;Yz!hu,&c4f m6 G.wdu:=檼̝psi-novgrB͒hWPV洩5(Z}.J($$={vvQJy7Sj oAKwzWGNt l"Uk"###GymB1[Wr Q̿~7|8%PO6+<\8z&jwook~`xVOⒹw$ o^gVD6̭StùE7ڝSQ_۬M{`R۽n l,ȷ;.l1 ܍ݚQZ[EBâ;/%`qD, SYv`{琗;3'΁H3&)"'~&9C\X$oMfujy'728s)M*΢7.()gСH xzA~~X ӵkW 0?_XM UU6ֈw" 򰽷Wʼn89݄fMF=)####:E{Þ 6ds-*53;?{qR;nJm-7wF֕{wuMu&͹)}Bc\Rj*{X=Aw XюX=3sYHvWó|V;5J!ȫ\a̢S9r g1MD͐v"}Xr?x}BݹM`xVOrf$+S3ItUYs#vSS)pA۱df3 h";;Y!Ϭ,< Qh4̴4K@(4q,BɃ*Nt1od+r ABb_ޕ11$3pMnjNVo[LUy睡ZWϳ|uKie42‚+Փ[?yn^'V7 vLxu,?&NL1С8qyV(T %f.;/fgQ5!\s03K#ӂbn ls^)< =,O]~{7~\E?+z 約xM;;ZKE~t8qv,-- FÞ={ݻjw<))X˒&L(GeB(l2rƃ'&NDfb Ċ&;]LG#%27 -m:~1DrVgB)(ZVڪ, 5#3y/,R,>+\sJA(IpFT5!pKmx<6v^GS\coגKSFefTvoNTJk0Mtz%gEJ&^Z5mYIc$$)ݵKx;ސjfaR>ذV`DV/g=>S]o;^TkbȐ! l߿?{E ؠ zELL8FOy VQ0dP74;8b:KQ/2222>B[K\֥Ņ|PsκLv)#c`%v\9u<ҢԹòYmZӼc\Bt9mJkgB&w8qBg2Ѧe̺d `Izi<=[]\mLVmAko8h#gOsۘ3;qy} fθ, &ǗB`ȑ_BH )))"..=zd0LLLdȑvn%aJ  $Z6n)(<&Hj wI2d V3l. ,ȱuGWg4PtaR(u.Xd[;e+p͹{|Xe;7'!ͭyyMC3-JS&~?qdn濛~xͤ!6d`)ޠwY7?Q^WV/oss'B^J&!j O.@)>ۺ9˜_X }JT2boT*袋 6kUZ_Z-E;EǷiGMFq]4cf 访-=ż_?wKbE=JkZW3K?ҷ?-+xL{i|~lvdz-IcKBHH2x`rrrh4-9j5]taС9Awތ1?&@jjnl^BnP\"JXAZ T(Y1V$jACnܯZu5w%*mDQf C}6VH2<#####2 ya<:fӻ访SR[GJT,œufQ|Ŀ{hIR[KD-6n1v,<1Od&g8\z P)LFG5Z~ʋWEfl?4JOQבLlAS5'[m̢R`\+r\"v0/'`;8-+|jNU"7) =1B_ 9S]k=5ujZ^\/LͶO䑅9[\%.,m?-:i˜GENN [sV t(**̙3TU9/9CDD)))8BBB>|8 &3C%IN}DH 5w]R.jZ>޼w0ae塝7yr$E-+bO7[o3:#=:V,*6ּ!X )kTe ܍qx}w|*t*!BǛ3m&# `?o=̵F!8aw5iffV?}jhu8Z۵c98meaEd~tt4_~9߿B 55TZ-TTTPUUnwBAdd$111$&&氝 t֍<՗%Ј +(b&Z%o~E_eR+kok8!d̰-H3ahfT}s3oj;<4{ZIb$z{Zڛ=2 Wq}%I\G8Q+t-UF_XXmS b?UW F7iQqdť J[3-.6&;>DT %gsCBakQg5[R%-(XR2r؉xJk}aJAA\FVv4*5FFEB^g44{a}} PQ肛aHF1$2P^WUFdbۉkyh61򵇝ܶ]q3ܗW>uJ%?hwl塝[ 0=d{,!]{_{{܌k! Y|$58;3*+k|=a׮]:(R__O}}=ɄdRP*JXXaaamfзo_inD~9Var΅|SgE}=M(MC .3# ʕ*6,VFD92[ٿ)0Cg*luo7gj^KęM.22222SXUFaC-pim,Ս Zq2K"O %'9P5`Z.ћZ\o`69%pL8XrMa`\<=HVyE8XzϷ♟t3ǚ[sd[s9 !J5(!::ܧEDD#Fsqc[ "##SRRRٳ'qqqnVa⨡Olq[uEF/:}7=}ɬ2'u$)N8f_Eex_t<ɇ~F4ڳ~^{(DAy1{i;ZČ#uWl|nbBKuU*u6qb{QT鸤G&„ `)Jٽ{wwNmm-ŔRZZJ}}BBB'11DD]x G„"}:'yU,wBc6ݍf{^~XO aH%'DPk_G9ӖphPD $_E{6؉!-[g\e#c<1E$E.JO,Šu{6=701С0aşpd2Q[[KMM шhD$PըT*"""[q˶hMrF@M|+f:H¢\`ܹsO ?ԟMdhNJ$ %} i/A0?x%6a,EOLlXjw,52pw"y3.# X9pFb(5T*ѤMnn.{O>撝MFF+Lajź6Yv7R׺J ?Z[o`M @QqQ'=^F`w @l{‚o3jV Q}R},튛gD%81~w_\ˋWS=TAy] ˊmRׄR{VP0>wslZⲜKw.qN6w%VScaJ pR#h@ka,@@zܹ^\p'Vi-"_іSwB- LGEkQ;6O5na[H^+L;+%q96^~s aj9rBFFFX\v%>6Gv؜LF~?kUzumJO-2>?Jۓ]ƌ~XV-du%ީY<;6ܳ=^]۝=ΎOe.הذ}{ݫqeA{%?&{4&xJ0C= wG|1W_}e|^G%8BCGK1*9pBFFF+,?U|pDZÀۇ֨/1彧SU7x T~9z&wFo2Rm;5*n]v+_'/`/pY{eQ;WҜ$Gƺ6dxp5 A5>[Ԅ(I9"ݵeuլ9YD<8, H@OaP)||f$ugpMB zr*w####s> :]znHFw4 7bˁٟDAy} 8 Xޕ0އ~bjjx;|(ATq+\o }Rg-Bxs(-vxy1lu)-E?߶ r_2Vl Yh V\(-L~1p|Yr3ђ15&OPI91M=J䠨lKT(I:a_ҋ&#{,a)HH+:ANBòzpfaaQ %9"jmqtCm.KNbvP+4HÈ^Fa`ǩ#$EIQϱ""4!ڛXk*X}dN@I3![t[ļ+$YQ_=_}wwtITYMA#3.ɣzUe{:f:`qxug9^^Ү*ZfPw}GѦeo3'+ `IكǶfSs- _lkk+Y&$k,D;nFEOGx ܌N8TPlA &###5VޅI4R(LȈM$=&p1H&WtHe7 ^ ?xYDRPnllzq?jNV_|SR}_~<}L"*l>qpM03=/\s{ꎺ IDATc:$ڴ{E-'MFl"1+r";"Cy9ڎ(֭HcC#uf]2%>[Him:R"c靚eKej ~xs}W״q̅,PxW +OxwЮ!u 0K;dg@,X n` pz$)PJ"5 -RgC .y[gvs<6vldl;S I-1+zdųuF=,|u u\ux|/L 7q]\ӗ6#ã3::|Ö7>Ldp(p3r#4!,)/^9AcȊKOjvY'“;N9>wBI:೹9Y񿙳mXZT9NNHFlK햘֮八$,(#gOw>= `DJUzܳH?ߩ._ߛ u0Ȍ$Lh|8 (|-TU{xrϗ/x|U,Ͳox[V5&H \6ZDIHt.=,~|DhB0".`u*-`LEےYyh'zV31lxӴO}ʌ~g?rls+іoeOJbB8:*(;f7{|d˾`ލ<0Ftiw3' 8Śrw.-5fr϶"64? [m&Rd%QX}xo3LЄ#(HfԻSCO?n (vsq h;0\ՍV& +VBF(PDiۅ  k(d@/]'8vf+DQN1wmOhq|&RMU<^DŽƄIK<޷X##CK`dKC;snA{˙?xe7UVw$?&Jhs;;nlvfAh AŠW 10wLwNn׵_n]k%L8W| u3'﫷:0 K[ UUӽ6\Qx w>`(\ݡ׉&>8*Nj ?l EaŠďEXBN4Ҷ,,=$5g_ǧN$lԺy$[~U|+*M <|Lbܹ Jf>J/PWB DnF<####,ORXUFZT#QVWm ^ ^\o3OG7yo|iW#&Ծ#؉;Ϯ#f ۢVKG}KGT Z60"ڠ[?y?GLDƦB'lΠE9h6oxe7ቘ\$]78 X -uDP8vjEcaiS-U3"6:d1-pN#(:0R:6~zṇ~S=><$3$J7Nt \%"7%M+銰o{sZak*>\̕7 D-֮0ј ڽ X2222mA8ے_Pײk ZBBշU f_Xäwk 伡m^[ )es.1P8QQ qŊJ_y(E\mZjTj= ܭUab#L+LQ٥;e s==NmH<:6ӟvFP8&6Z-"&:MIݝ)ў$LX)Wv}5gG"=uBR=џ+*\c#vrPr/bR?K$莃ON˩ԋ[0U>1soZb^Kl ̚d|6LKXS;zd0cK}hɆp{IŔ>yu=?ʷCQ@۞^̅$"'$vLEI@w[ OR[3??:Ϲ6ǧSaf70ȋsJ Ş"1d2227[NJ[RPпSWVb G'0,M/kV<5"C@= MIWm8/k{flckvƁ‹kqzF)(P;Xw,ߖ2>wݹܤt{Gz>,|u ^[Et*7z|Xb\qM%/, <;th(P~$I(cfƟ;Dcq0\\ 9“%Pg!} +(:0PhI1eX~ܹsϚ=YsB8G(WBPPP`hЂڽRڟu'R}9K^z͟`Iݨ7eDI(Ie=E-cÖ[B 2LtMV@I&J# i0"br07[+[x)$G50/Xyx'w$FwCFL"SsXYL%Xk?eϙbC#I 뎵^]I4t;Vg3-mN|x#l*wGLDE +$+.& XIvdF=- Kӽ͐\yܙg ӻ5q.rx|m5+lIGFM:!v̌*jG[S|KFDPB7-aɴNK5(\YH[Z‘0a*Pk#uQ ,L jJ"@$IUw_ Lo~vߜ)q*Q%\ \ R@2#&N49$P"IievS P2pDs5H,H e֜iX#Q*@D}[lABw:,@bF4oFD3AةƤwX,B@D\8&Nl<~ZŶ:~}Dw4kc0X{lT%ܤIH[m}7f] Qzc[-rz~fxVO" ^>Nxi %%2W.7"O.oi:hfioW2)sdDVO^Z5?oq:!1B`mz3<˽#&1ko¹Fûy8ZvY]5{,[Z g*KwU qϷ-+Wjl}6 =z ?x?s0bo^ J[#"*$⚊6_pE)hz5ešL;`Z|y] 3<_.V!JN v}VPZ[SWLp'92GOmY}xgJJݘkWdBSos=@@Jp ;1e$*?=;u{=Fz5E(^xUVB A${ٚMNvvgf9y2y9 g F z>H7z>xwKTL 瑑;@qk|<0J`H7P4Yv!&n&Հ݈TP l +BQ"㖌$\߽Qb]2IEЉ-rfNrs[Ǯ0r9q^l<@qܜʯc/%lqu;z?k6Y~EgEOc}= 6]8آE[AB!fxJ~OV;^͇kb,s1wbzJ6^3RColvCv|J _(nfO\oK!b2%R. aˁa&z20xfp,{~nه@$jvGܼ*=bh^ SqJ8\mlؿvfe ΝaON| Yq$FIZȯ6no?GcIM(I1{C.2&+Xf#,&/J_;S 0aHA1-*d 1gOh%#|bxr?SsbayjT %;s#}+5[33ug3Ⱦ`iA :=55$IMlط˧/c$wFl(m.;Jl:y%c.fmzGz o/׃ يjGJF2IC1xhVih\gdQiSL3X喑-]mA3 i0n <@ 0TpH$j,࿐)Sşӧ4{"e8$I$I#x.TJn| јhzP Ռ~fagf' ,D P0x2o+RbbGя8J,Vdpb-v|#æu6V󺚣3#lYnKf8~CKh= STnqgF$Iƒf7T#Ɗ"بv֣3(e64^i;I?qDٮ(|V=Ԛ,t(.iF3'8a($eO*wV“B"8KQ(3b0z"=:!L$3Ysie ;7HXdaI$k9Yrzm՚4'm=Lh9shܺFՙdMb33&@Y@Z| _9tsGr*DNhDCee~HC& "&2]ש=| à&̡+I$IC#s֎K"{Czk9B/`h.ǟv깦d~$:̚訧gZa@"0d`$_ V)6s7Y5QZȐP`2SlXPu(/mbrnHhyh%g'񐘜y$ًIyi)--mbB:$Ir$*tSԘܧAslǎpvbgA`tjGEeO_{*/u7lKj4G3׉0g KIOXh`٦`]9WॗZ9J:7()({#[t]⢢ܲɴ ԉWM$Ij}X.aOAsQqw0g 7@q>&bg[./OB:t@w6TSYE Dj=v~l@,&3jDxF4!PU}#CHճ3ʪbpǙƨbթT%QKƃ%#T-h ISs1/!rԌl:Ϗ$Ii\v7pӂа+f[47 l,G% sDFc̎N6@_|LtkQL|͡(G3w\h[~2a^B׆s|Np(**R-=UX7 fOQs*M1J.+G=7 VcG~#IGnAOAoh]*)E` ߏYn&ݳwdM+_~E(ƙnC3j=Z:觳d]ɁܵہσV1Frb#^'+*rn>YT+(kp:_m*G6|B9}$i"1X_^ӌVш]FGlBIBOED0_?kbqtS |OR\[G?C7٣LAnB UxܤAZLV;49_f祺 M!s^- /!!2:Pu)ܸe=7g.5`ݮogf2sݜeܶr~e=w/pu?z{K;@!kVMF09dVjqz9 C\Q׽,wk-FP;jQcFՠKG}&h#wo_I'F}ME(Bώ"/!˧-3 .=#bk$7!K ?35MnfPb3ǟn9fyfE%>"{.xx~.&>b3}ﺩJ%[泡 L l~^'Ȭ6Y8n} sSQia+;<͸@<޽x [t "nEa-n=&`=etSc!,lI7ÀS;,4U(]%*1t@>d}CHvXstIC1^y-O>?Y ǶQg?Ɠ0+&rRzbdMaav!\sya4qT67 VPIHi Y? sO|&jV^ݛhsu"]#kײό\ d?|)^>\g^Kq$d\=BL#xI`=QCTIH4xx>!`HJ6,Au2T3^@aކmp߄!'ۓlp6G zs\ZK=EfOvۢhff,VC[\i҄=V'>SaqBGJkpN/G'&NF9˰-,; UNsWWk6A3Nsm⑛)sj"r6P\{JJ[ wqͬŬ2%ӃW(ުvl>y?j`Ň旸[IeE,6tg^ ?pzϳ`ܼ`uĊY\7{)+y^s駆'oWW-I`d`bNXhBP9fPԙFb! poyAs 9b㻽Am(J`.N[<MY*:NaM{ߵq*T1S->`z S I4945$|.i䵼B3t-pi14b˧-=dSlطaRp4c>]SXY0uu]u?㖧d}Cn*6&$IRX8eqq^u*"Py6oӍ5̜(RlNq7b y֟hΈh:U<uB;L13 \ğ)$ILdĀ.* Kװ(k*4;8n{%;ˏ|DY,=NHlGzlbc.6/Yi9DZmT5^A}~-Ksn蜨=y}bnYx)-u<_nRbit~ʠffKodAV!;ӻ#='!"K#-&J9װ,o&'Ok>όl u>yJFGm~>vxgY7 }at VDM['2ru#~ZWpgnɌګD\arfO-t]x q"rf}և E\DJCqmDא$I rvu2)y (29e޺\o`w;!ѕە{ NJ_k5Jd2~O)PT˿gX"΁~.OD{-O˩Rc~$dM>`N[]}W}#˵py|ZvÑre2=+!F)[mˎK^¹|ɟo5y3bi2 YB}1 ,ϟI~[*qI̟ؗ-.aE,nyAZ6=5/,Yl l9SHI;sNv|2nwRbl̟͊YA&g<&\?g %'<&-fROewyMHaCc$#6}Jr:grrz~}!EZlB*d&>ˈM N=KQMDΜ$i"6Yh01AP?xρ*N}gXMfu^O? <[m~qCж6W27#y =rupœi1W(/{W#h+xtsz$8q*)~훞U3/u?%l$I [$>3=7 8eSjbyiFp?4BUv uk9=fS(Gsi-"e>gݚ~{$Iۙi/=[U_֑nGk)mCjGZlu%-L-[ ,ʞʯn2;x.~E~>DNJ&E]U{qЖx⃷]7@-E^k ]K+b$<;nNs_BlQvaо˦(u}F:ZUv"yjښx~~wQ]_zLe]N6fZ9y惖D:U-nHM9/I$TmKdf=- _K%ZgP#0 Rوue_T09+f,#:bP?3(Y[T)^6ST_Zs^VcY-͇oKXT Q, Nl=uhkUV|2/Z ȈMT63R[xN*z6t )2XRn;gͲ*ô~%IƋWZ#4Y)8Iֽ6bRd5DOOݱޛ,}á?a3{0a܏=w)leFDGPlSfsqc l AFي/ V&4~2w)5!K׬D%#fҮXǮ$MjV;jlvWz3k:Bwt(~ƽvC?R~2"(do%b7ey|l/sz_+.q9]Kx{pRX;GMl ӒܐN4M/3N"Am{ ?yQ_/#Q3]u~`28!I҄Ѧ3Fy^>ϰKcY5QjQ]tS{lI?.oNP8'|$yqދ-w BQpM@i4 x7(&*M.ml;SlO XIB-Jbs"{͙ΩN$MLɚ 3G^md{ ,jx> xC^B*SXzk2Cq[/gItNXmxՐy6>{)EAR:IoVf{rXҕ@;߄TPuKKZ5r2O({*ag)N"}W=7z&b(Lɴ%IxH-v2n}l @j*1 5<{htݏra=]=]VO`0,;o"js:rѺ*}>>zHED J=21ZUWp"9$IgIP8asb'Ay4yWT3& &3R0pWCUr7~tߚpPS%d6 O8't bbl0yI|>"5gy$tBjI5ѤNe e|~$I:ot ׃lOfg#ǁOceW N|e%G  +gfw 1#5O^ub46C`[Qh*ͪF6lu]}0*^nDsUKו>s[t!i=@͇dG9+kD:&0P @h|BC NE)T\B$I4fd&:BMQY{jd%qtȠ@wK3tOfⲫY;i9&3%*Dbhx΄S UEԃx)HJ+8}&pvzh'/!ԨM=β\4eY#DxaN&EW2q(jڛtLqcur,xvԮeKdf?@r RZ祡=$IUЩX)7)AdWn>WՄ@NP0pWGSʟ.z$q|J}@P%w>@lPhR]4l9j-CHZ#8iSbqlQ5Rd`B$i:^[y)j'ɻ #?1߹Fs-%U6й ~)eW93$T~DNOfX73h QH I/% R+s^f)]$I2]ZRS',vX#8h5V;,6J6MT&t WPg֕h˜eAϴFܕ0&PB$I<ԔgFmCy00h sW>NWV8V.5mMnf? Pz<\7g)7[Weҙw,2(dJkmw.G>+.K\{oYem}tUxbо7l+g?azJVm -E%Wi}~E>5󼚏wf.|zӊY[Gwo_y+[[9X^J$Ic?DgcvaueβE!ѥ _Vvrs=ݛ5G2yQI$?ZZNGē~M'S\OAR:pϹ\t*?>f_Gu5* _XAtr?WXW~LqC5y $vmecQMxN?*߽,ɝ+_~%G0 hxŗ0'Er)hvƮT3-9]WxDpgre1f//bQTAuf(g* /cuLK&גUQuwZ^?gY?yyU ^<58{}D3/D 8PwvO\dY=6Gm{3MKAbm~O I$il>?0곬)_~FUdG/Kks;ovo6J?Ē152[I$Iˎh'!"9Rw~oni0h?7C/OL'f22}9[_ T%ϑlRKH8}N9y;j2skɉO Ҏ_nz5 tnyi2kf,dU :ᑐS~Sx54: MbrR2qz|*`VZ΀GYmA+DBd4[ti`ߩ*@`,\:Nz!~1-%g^5UnX)I$ Lw4:y,/(h "wm/oXbJ $I}W}-7{"s k Hw ǺVrϲ8?QnkњrʛBWdފ":ZTEҩ󘓞((bKvPU }; IDAT%SAiS-Oo"`΃_&+.˧- 9*V6R˦ǬwV.2YSP±r67hTVQ{Enks܌|2bp9Vr8&gROŗ5e|}=~(fGš99yd% uF53 'il'RXm &bcՔ9;]Dm{ g+MN4~BK$IcoS)"HY𫲧X|0˹-.!@P+5:KL.$I$ Ռl^t w\bVd;81X%|So= {6H$IYg^5GeO) {JvWv>_$I$ ˱ ^?(,8l]3i-$I&XVB7* %I$I:q$܁ @SͪB 6n<+:ؕf$I&e+Gzۥ7?w}J#J[=\w&Ee{jB,#$IyWzކ΅O{Ip/EQ]|%I$Igj\%kWwQΨʞ)3\F_cY@ ֽ<4aN.D=$IysFq>/m&3_zvs+*12o=Á"I$Io}Q%|kIb',n]@_HĜ\b&I$I*ƨbܾt ތ{E5a3[L'م"9PU$I$Mf_}#FC:]֯[p[ڜU0N4ԄT[x7M$I$I$I&5Ն_GmiŲgʶg=8ˋ;!Bb0ebND I$I$I$I0uk.GwwTKK塢<8Kn7%G{^ :1d)6as %I$I$I$dx]ZOk<tfprV (L| ` ybFJ$I$I$Igxhuhuhց*d0xqlbH"8mmSR|Bp75б%L'K$I$I$Ig6:t E(E)VjbG9s[ N!PѨ8=՞ H$I$I$IZ]4W PNmww?{•'_ }-7C=Oml7TB(cjI$I$I$I>Aa'ׁubx^5Q %J]Go'z]QY"T3 I. $I$I$IƟa`>мC1߅"^d7p|=fq5 + Xd6I$I$I$I8;-uULݠ8g-х0R ğvI$I$I$IR4'C|mrl7*&Mp"c`g$I$I$Iy 4Ћ1(F/ϕ7Ֆ.V IENDB`python-sense-emu-1.2/sense_emu/sense_emu.svg000066400000000000000000002036731411441564000212760ustar00rootroot00000000000000 image/svg+xml Raspberry PiSense HATVersion 1.0 (c) Raspberry Pi 2015 ACCEL/GYRO/MAG HUMIDITY PRESSURE python-sense-emu-1.2/sense_emu/sense_emu_gui.png000066400000000000000000000042221411441564000221140ustar00rootroot00000000000000PNG  IHDR$$sBIT|d pHYsn tEXtSoftwarewww.inkscape.org<IDATX՘}lU?y{{{n۵k/mlc q /#_$bu(a8@B 1\vm3lk{~{V, d-or$ws>rI4no\k~$TTdA)ڏ>;tN{ط6T(r4+L1/ mk 荶Ԕ7bA _d'2 ,[1Mz/˷R,pqM0lN sU–1S<)Ք}^pf[LUxo96Ě%-=چLt {WգBtdžQ[cNg ?'K3~˵)Y}N2d"f,b$`r*]"[VՋlY7YeCI64?svwnYD\) WfS^a,7D.D>ő0PMzO%eS璜L'HNT wԓ଎6\_gйc&<)r_P.}5k7RY؋J[wB|,/^hguQ f(9LӱEbQtXbQb4D*7JGXE=`NJA{"!IE9w{@% "(Y&SGMJ9v[ װH}BdAԸA Z h=S {[J>e8>8>9Et%kd["E$g2 8Ȉ%Yעij(ӆŞ?!a4NG.J\IɊvChj#ghjÕ"MLY:l+S0u:(&W6* Q8nRUuTחsps UI)dL”/OӚYTE-(yk?' f$C lסhI'EF2 tk*9uipu͗V3ԝb3I!<&4}4 9zpUV_kPu}ECWlu(YGƇϦN2Kq"?]:>P9„ˍ6{D'--knkPٻ{(9r4>+˪aٰ{~#`M;Ϙi|WP,6`/2@ٙEל״DT* zx.PI4Ue(ZNDׯXIL|akJ [^gQvj+LOH(jUh4yGhM q~#*tzY{YE9yl9<.c~V] Qz>)Rv0\vu~\I(x֋&h xKceܨ0! Mg qϧ,ssγ&%u%#j07pr;>ED[ŨҺioWԽoփ¤Ne7J|!IYb_"Kmj=^Г}/ɴբEV"Hq${ǵUQ%*2,>zO,o@Ҽ0&ul image/svg+xml python-sense-emu-1.2/sense_emu/sense_hat.py000066400000000000000000000661011411441564000211060ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import struct import io import os import sys import math import time import numpy as np import shutil import glob import array import struct import subprocess as sp import warnings from PIL import Image # pillow from copy import deepcopy from . import RTIMU from .lock import EmulatorLock from .stick import SenseStick from .screen import init_screen, GAMMA_DEFAULT, GAMMA_LOW class SenseHat: """ The main interface the Raspberry Pi Sense HAT. This class provides properties to query the various sensors on the Sense HAT (:attr:`temp`, :attr:`pressure`, :attr:`humidity`, :attr:`gyro`, etc.) and methods to control the LED "screen" on the HAT (:meth:`set_pixel`, :meth:`set_pixels`). The *imu_settings_file* parameter specifies the base name of the configuration file used to calibrate the sensors on the HAT. An ".ini" suffix will be implicitly added to this filename. If a file with the resulting name is present in :file:`~/.config/sense_hat`, it will be used in the configuration. Otherwise, the file will be located within :file:`/etc`, and will be copied to :file:`~/.config/sense_hat` before use. The *text_assets* parameter provides the base name of the PNG image and text file which will be used to define the font used by the :meth:`show_message` method. """ SENSE_HAT_FB_NAME = 'RPi-Sense FB' SENSE_HAT_FB_FBIOGET_GAMMA = 61696 SENSE_HAT_FB_FBIOSET_GAMMA = 61697 SENSE_HAT_FB_FBIORESET_GAMMA = 61698 SENSE_HAT_FB_GAMMA_DEFAULT = 0 SENSE_HAT_FB_GAMMA_LOW = 1 SENSE_HAT_FB_GAMMA_USER = 2 SETTINGS_HOME_PATH = '.config/sense_hat' def __init__( self, imu_settings_file='RTIMULib', text_assets='sense_hat_text' ): lock = EmulatorLock('sense_emu') if not lock.wait(1): warnings.warn(Warning('No emulator detected; spawning sense_emu_gui')) try: setpgrp = os.setpgrp except AttributeError: setpgrp = None # setpgrp is called to spawn a new process group, ensuring that # signals from the interpreter (e.g. the user pressing Ctrl+C) # don't get sent to the emulator too sp.Popen( ['sense_emu_gui'], preexec_fn=setpgrp, stdin=sp.DEVNULL, stdout=sp.DEVNULL, stderr=sp.DEVNULL, close_fds=True) self._fb_device = self._get_fb_device() if self._fb_device is None: raise OSError('Cannot detect %s device' % self.SENSE_HAT_FB_NAME) # 0 is With B+ HDMI port facing downwards pix_map0 = np.array([ [0, 1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14, 15], [16, 17, 18, 19, 20, 21, 22, 23], [24, 25, 26, 27, 28, 29, 30, 31], [32, 33, 34, 35, 36, 37, 38, 39], [40, 41, 42, 43, 44, 45, 46, 47], [48, 49, 50, 51, 52, 53, 54, 55], [56, 57, 58, 59, 60, 61, 62, 63] ], int) pix_map90 = np.rot90(pix_map0) pix_map180 = np.rot90(pix_map90) pix_map270 = np.rot90(pix_map180) self._pix_map = { 0: pix_map0, 90: pix_map90, 180: pix_map180, 270: pix_map270 } self._rotation = 0 # Load text assets dir_path = os.path.dirname(__file__) self._load_text_assets( os.path.join(dir_path, '%s.png' % text_assets), os.path.join(dir_path, '%s.txt' % text_assets) ) # Load IMU settings and calibration data self._imu_settings = self._get_settings_file(imu_settings_file) self._imu = RTIMU.RTIMU(self._imu_settings) self._imu_init = False # Will be initialised as and when needed self._pressure = RTIMU.RTPressure(self._imu_settings) self._pressure_init = False # Will be initialised as and when needed self._humidity = RTIMU.RTHumidity(self._imu_settings) self._humidity_init = False # Will be initialised as and when needed self._last_orientation = {'pitch': 0, 'roll': 0, 'yaw': 0} raw = {'x': 0, 'y': 0, 'z': 0} self._last_compass_raw = deepcopy(raw) self._last_gyro_raw = deepcopy(raw) self._last_accel_raw = deepcopy(raw) self._compass_enabled = False self._gyro_enabled = False self._accel_enabled = False self._stick = SenseStick() #### # Text assets #### # Text asset files are rotated right through 90 degrees to allow blocks of # 40 contiguous pixels to represent one 5 x 8 character. These are stored # in a 8 x 640 pixel png image with characters arranged adjacently # Consequently we must rotate the pixel map left through 90 degrees to # compensate when drawing text def _load_text_assets(self, text_image_file, text_file): """ Internal. Builds a character indexed dictionary of pixels used by the show_message function below """ text_pixels = self.load_image(text_image_file, False) with open(text_file, 'r') as f: loaded_text = f.read() self._text_dict = {} for index, s in enumerate(loaded_text): start = index * 40 end = start + 40 char = text_pixels[start:end] self._text_dict[s] = char def _trim_whitespace(self, char): # For loading text assets only """ Internal. Trims white space pixels from the front and back of loaded text characters """ psum = lambda x: sum(sum(x, [])) if psum(char) > 0: is_empty = True while is_empty: # From front row = char[0:8] is_empty = psum(row) == 0 if is_empty: del char[0:8] is_empty = True while is_empty: # From back row = char[-8:] is_empty = psum(row) == 0 if is_empty: del char[-8:] return char def _get_settings_file(self, imu_settings_file): """ Internal. Logic to check for a system wide RTIMU ini file. This is copied to the home folder if one is not already found there. """ ini_file = '%s.ini' % imu_settings_file home_dir = os.path.expanduser('~') home_path = os.path.join(home_dir, self.SETTINGS_HOME_PATH) if not os.path.exists(home_path): os.makedirs(home_path) home_file = os.path.join(home_path, ini_file) home_exists = os.path.isfile(home_file) system_file = os.path.join('/etc', ini_file) system_exists = os.path.isfile(system_file) if system_exists and not home_exists: shutil.copyfile(system_file, home_file) return RTIMU.Settings(os.path.join(home_path, imu_settings_file)) # RTIMU will add .ini internally def _get_fb_device(self): """ Internal. Finds the correct frame buffer device for the sense HAT and returns its /dev name. """ fd = init_screen() result = fd.name fd.close() return result #### # Joystick #### @property def stick(self): """ A :class:`SenseStick` object representing the Sense HAT's joystick. """ return self._stick #### # LED Matrix #### @property def rotation(self): return self._rotation @rotation.setter def rotation(self, r): self.set_rotation(r, True) def set_rotation(self, r=0, redraw=True): """ Sets the LED matrix rotation for viewing, adjust if the Pi is upside down or sideways. 0 is with the Pi HDMI port facing downwards """ if r in self._pix_map.keys(): if redraw: pixel_list = self.get_pixels() self._rotation = r if redraw: self.set_pixels(pixel_list) else: raise ValueError('Rotation must be 0, 90, 180 or 270 degrees') def _pack_bin(self, pix): """ Internal. Encodes python list [R,G,B] into 16 bit RGB565 """ r = (pix[0] >> 3) & 0x1F g = (pix[1] >> 2) & 0x3F b = (pix[2] >> 3) & 0x1F bits16 = (r << 11) + (g << 5) + b return struct.pack('H', bits16) def _unpack_bin(self, packed): """ Internal. Decodes 16 bit RGB565 into python list [R,G,B] """ output = struct.unpack('H', packed) bits16 = output[0] r = (bits16 & 0xF800) >> 11 g = (bits16 & 0x7E0) >> 5 b = (bits16 & 0x1F) return [int(r << 3), int(g << 2), int(b << 3)] def flip_h(self, redraw=True): """ Flip LED matrix horizontal """ pixel_list = self.get_pixels() flipped = [] for i in range(8): offset = i * 8 flipped.extend(reversed(pixel_list[offset:offset + 8])) if redraw: self.set_pixels(flipped) return flipped def flip_v(self, redraw=True): """ Flip LED matrix vertical """ pixel_list = self.get_pixels() flipped = [] for i in reversed(range(8)): offset = i * 8 flipped.extend(pixel_list[offset:offset + 8]) if redraw: self.set_pixels(flipped) return flipped def set_pixels(self, pixel_list): """ Accepts a list containing 64 smaller lists of ``[R,G,B]`` pixels and updates the LED matrix. R,G,B elements must intergers between 0 and 255 """ if len(pixel_list) != 64: raise ValueError('Pixel lists must have 64 elements') for index, pix in enumerate(pixel_list): if len(pix) != 3: raise ValueError('Pixel at index %d is invalid. Pixels must contain 3 elements: Red, Green and Blue' % index) for element in pix: if element > 255 or element < 0: raise ValueError('Pixel at index %d is invalid. Pixel elements must be between 0 and 255' % index) with open(self._fb_device, 'rb+') as f: map = self._pix_map[self._rotation] for index, pix in enumerate(pixel_list): # Two bytes per pixel in fb memory, 16 bit RGB565 f.seek(map[index // 8][index % 8] * 2) # row, column f.write(self._pack_bin(pix)) def get_pixels(self): """ Returns a list containing 64 smaller lists of ``[R,G,B]`` pixels representing what is currently displayed on the LED matrix """ pixel_list = [] with open(self._fb_device, 'rb') as f: map = self._pix_map[self._rotation] for row in range(8): for col in range(8): # Two bytes per pixel in fb memory, 16 bit RGB565 f.seek(map[row][col] * 2) # row, column pixel_list.append(self._unpack_bin(f.read(2))) return pixel_list def set_pixel(self, x, y, *args): """ Updates the single ``[R,G,B]`` pixel specified by x and y on the LED matrix Top left = 0,0 Bottom right = 7,7 e.g. ap.set_pixel(x, y, r, g, b) or pixel = (r, g, b) ap.set_pixel(x, y, pixel) """ pixel_error = 'Pixel arguments must be given as (r, g, b) or r, g, b' if len(args) == 1: pixel = args[0] if len(pixel) != 3: raise ValueError(pixel_error) elif len(args) == 3: pixel = args else: raise ValueError(pixel_error) if x > 7 or x < 0: raise ValueError('X position must be between 0 and 7') if y > 7 or y < 0: raise ValueError('Y position must be between 0 and 7') for element in pixel: if element > 255 or element < 0: raise ValueError('Pixel elements must be between 0 and 255') with open(self._fb_device, 'rb+') as f: map = self._pix_map[self._rotation] # Two bytes per pixel in fb memory, 16 bit RGB565 f.seek(map[y][x] * 2) # row, column f.write(self._pack_bin(pixel)) def get_pixel(self, x, y): """ Returns a list of [R,G,B] representing the pixel specified by x and y on the LED matrix. Top left = 0,0 Bottom right = 7,7 """ if x > 7 or x < 0: raise ValueError('X position must be between 0 and 7') if y > 7 or y < 0: raise ValueError('Y position must be between 0 and 7') pix = None with open(self._fb_device, 'rb') as f: map = self._pix_map[self._rotation] # Two bytes per pixel in fb memory, 16 bit RGB565 f.seek(map[y][x] * 2) # row, column pix = self._unpack_bin(f.read(2)) return pix def load_image(self, file_path, redraw=True): """ Accepts a path to an 8 x 8 image file and updates the LED matrix with the image """ if not os.path.exists(file_path): raise IOError('%s not found' % file_path) img = Image.open(file_path).convert('RGB') pixel_list = list(map(list, img.getdata())) if redraw: self.set_pixels(pixel_list) return pixel_list def clear(self, *args): """ Clears the LED matrix with a single colour, default is black / off e.g. ap.clear() or ap.clear(r, g, b) or colour = (r, g, b) ap.clear(colour) """ black = (0, 0, 0) # default if len(args) == 0: colour = black elif len(args) == 1: colour = args[0] elif len(args) == 3: colour = args else: raise ValueError('Pixel arguments must be given as (r, g, b) or r, g, b') self.set_pixels([colour] * 64) def _get_char_pixels(self, s): """ Internal. Safeguards the character indexed dictionary for the show_message function below """ if len(s) == 1 and s in self._text_dict.keys(): return list(self._text_dict[s]) else: return list(self._text_dict['?']) def show_message( self, text_string, scroll_speed=.1, text_colour=[255, 255, 255], back_colour=[0, 0, 0] ): """ Scrolls a string of text across the LED matrix using the specified speed and colours """ # We must rotate the pixel map left through 90 degrees when drawing # text, see _load_text_assets previous_rotation = self._rotation self._rotation -= 90 if self._rotation < 0: self._rotation = 270 dummy_colour = [None, None, None] string_padding = [dummy_colour] * 64 letter_padding = [dummy_colour] * 8 # Build pixels from dictionary scroll_pixels = [] scroll_pixels.extend(string_padding) for s in text_string: scroll_pixels.extend(self._trim_whitespace(self._get_char_pixels(s))) scroll_pixels.extend(letter_padding) scroll_pixels.extend(string_padding) # Recolour pixels as necessary coloured_pixels = [ text_colour if pixel == [255, 255, 255] else back_colour for pixel in scroll_pixels ] # Shift right by 8 pixels per frame to scroll scroll_length = len(coloured_pixels) // 8 for i in range(scroll_length - 8): start = i * 8 end = start + 64 self.set_pixels(coloured_pixels[start:end]) time.sleep(scroll_speed) self._rotation = previous_rotation def show_letter( self, s, text_colour=[255, 255, 255], back_colour=[0, 0, 0] ): """ Displays a single text character on the LED matrix using the specified colours """ if len(s) > 1: raise ValueError('Only one character may be passed into this method') # We must rotate the pixel map left through 90 degrees when drawing # text, see _load_text_assets previous_rotation = self._rotation self._rotation -= 90 if self._rotation < 0: self._rotation = 270 dummy_colour = [None, None, None] pixel_list = [dummy_colour] * 8 pixel_list.extend(self._get_char_pixels(s)) pixel_list.extend([dummy_colour] * 16) coloured_pixels = [ text_colour if pixel == [255, 255, 255] else back_colour for pixel in pixel_list ] self.set_pixels(coloured_pixels) self._rotation = previous_rotation @property def gamma(self): with open(self._fb_device, 'rb') as f: f.seek(128) return struct.unpack('32B', f.read(32)) @gamma.setter def gamma(self, buffer): if len(buffer) != 32: raise ValueError('Gamma array must be of length 32') if not all(b <= 31 for b in buffer): raise ValueError('Gamma values must be bewteen 0 and 31') if not isinstance(buffer, array.array): buffer = array.array('B', buffer) with open(self._fb_device, 'rb+') as f: f.seek(128) f.write(struct.pack('32B', *buffer)) def gamma_reset(self): """ Resets the LED matrix gamma correction to default """ self.gamma = GAMMA_DEFAULT @property def low_light(self): return self.gamma == tuple(GAMMA_LOW) @low_light.setter def low_light(self, value): if value: self.gamma = GAMMA_LOW else: self.gamma = GAMMA_DEFAULT #### # Environmental sensors #### def _init_humidity(self): """ Internal. Initialises the humidity sensor via RTIMU """ if not self._humidity_init: self._humidity_init = self._humidity.humidityInit() if not self._humidity_init: raise OSError('Humidity Init Failed') def _init_pressure(self): """ Internal. Initialises the pressure sensor via RTIMU """ if not self._pressure_init: self._pressure_init = self._pressure.pressureInit() if not self._pressure_init: raise OSError('Pressure Init Failed') def get_humidity(self): """ Returns the percentage of relative humidity """ self._init_humidity() # Ensure humidity sensor is initialised humidity = 0 data = self._humidity.humidityRead() if (data[0]): # Humidity valid humidity = data[1] return humidity @property def humidity(self): return self.get_humidity() def get_temperature_from_humidity(self): """ Returns the temperature in Celsius from the humidity sensor """ self._init_humidity() # Ensure humidity sensor is initialised temp = 0 data = self._humidity.humidityRead() if (data[2]): # Temp valid temp = data[3] return temp def get_temperature_from_pressure(self): """ Returns the temperature in Celsius from the pressure sensor """ self._init_pressure() # Ensure pressure sensor is initialised temp = 0 data = self._pressure.pressureRead() if (data[2]): # Temp valid temp = data[3] return temp def get_temperature(self): """ Returns the temperature in Celsius """ return self.get_temperature_from_humidity() @property def temp(self): return self.get_temperature_from_humidity() @property def temperature(self): return self.get_temperature_from_humidity() def get_pressure(self): """ Returns the pressure in Millibars """ self._init_pressure() # Ensure pressure sensor is initialised pressure = 0 data = self._pressure.pressureRead() if (data[0]): # Pressure valid pressure = data[1] return pressure @property def pressure(self): return self.get_pressure() #### # IMU Sensor #### def _init_imu(self): """ Internal. Initialises the IMU sensor via RTIMU """ if not self._imu_init: self._imu_init = self._imu.IMUInit() if self._imu_init: self._imu_poll_interval = self._imu.IMUGetPollInterval() * 0.001 # Enable everything on IMU self.set_imu_config(True, True, True) else: raise OSError('IMU Init Failed') def set_imu_config(self, compass_enabled, gyro_enabled, accel_enabled): """ Enables and disables the gyroscope, accelerometer and/or magnetometer input to the orientation functions """ # If the consuming code always calls this just before reading the IMU # the IMU consistently fails to read. So prevent unnecessary calls to # IMU config functions using state variables self._init_imu() # Ensure imu is initialised if (not isinstance(compass_enabled, bool) or not isinstance(gyro_enabled, bool) or not isinstance(accel_enabled, bool)): raise TypeError('All set_imu_config parameters must be of boolean type') if self._compass_enabled != compass_enabled: self._compass_enabled = compass_enabled self._imu.setCompassEnable(self._compass_enabled) if self._gyro_enabled != gyro_enabled: self._gyro_enabled = gyro_enabled self._imu.setGyroEnable(self._gyro_enabled) if self._accel_enabled != accel_enabled: self._accel_enabled = accel_enabled self._imu.setAccelEnable(self._accel_enabled) def _read_imu(self): """ Internal. Tries to read the IMU sensor three times before giving up """ self._init_imu() # Ensure imu is initialised attempts = 0 success = False while not success and attempts < 3: success = self._imu.IMURead() attempts += 1 time.sleep(self._imu_poll_interval) return success def _get_raw_data(self, is_valid_key, data_key): """ Internal. Returns the specified raw data from the IMU when valid """ result = None if self._read_imu(): data = self._imu.getIMUData() if data[is_valid_key]: raw = data[data_key] result = { 'x': raw[0], 'y': raw[1], 'z': raw[2] } return result def get_orientation_radians(self): """ Returns a dictionary object to represent the current orientation in radians using the aircraft principal axes of pitch, roll and yaw """ raw = self._get_raw_data('fusionPoseValid', 'fusionPose') if raw is not None: raw['roll'] = raw.pop('x') raw['pitch'] = raw.pop('y') raw['yaw'] = raw.pop('z') self._last_orientation = raw return deepcopy(self._last_orientation) @property def orientation_radians(self): return self.get_orientation_radians() def get_orientation_degrees(self): """ Returns a dictionary object to represent the current orientation in degrees, 0 to 360, using the aircraft principal axes of pitch, roll and yaw """ orientation = self.get_orientation_radians() for key, val in orientation.items(): deg = math.degrees(val) # Result is -180 to +180 orientation[key] = deg + 360 if deg < 0 else deg return orientation def get_orientation(self): return self.get_orientation_degrees() @property def orientation(self): return self.get_orientation_degrees() def get_compass(self): """ Gets the direction of North from the magnetometer in degrees """ self.set_imu_config(True, False, False) orientation = self.get_orientation_degrees() if type(orientation) is dict and 'yaw' in orientation.keys(): return orientation['yaw'] else: return None @property def compass(self): return self.get_compass() def get_compass_raw(self): """ Magnetometer x y z raw data in uT (micro teslas) """ raw = self._get_raw_data('compassValid', 'compass') if raw is not None: self._last_compass_raw = raw return deepcopy(self._last_compass_raw) @property def compass_raw(self): return self.get_compass_raw() def get_gyroscope(self): """ Gets the orientation in degrees from the gyroscope only """ self.set_imu_config(False, True, False) return self.get_orientation_degrees() @property def gyro(self): return self.get_gyroscope() @property def gyroscope(self): return self.get_gyroscope() def get_gyroscope_raw(self): """ Gyroscope x y z raw data in radians per second """ raw = self._get_raw_data('gyroValid', 'gyro') if raw is not None: self._last_gyro_raw = raw return deepcopy(self._last_gyro_raw) @property def gyro_raw(self): return self.get_gyroscope_raw() @property def gyroscope_raw(self): return self.get_gyroscope_raw() def get_accelerometer(self): """ Gets the orientation in degrees from the accelerometer only """ self.set_imu_config(False, False, True) return self.get_orientation_degrees() @property def accel(self): return self.get_accelerometer() @property def accelerometer(self): return self.get_accelerometer() def get_accelerometer_raw(self): """ Accelerometer x y z raw data in Gs """ raw = self._get_raw_data('accelValid', 'accel') if raw is not None: self._last_accel_raw = raw return deepcopy(self._last_accel_raw) @property def accel_raw(self): return self.get_accelerometer_raw() @property def accelerometer_raw(self): return self.get_accelerometer_raw() python-sense-emu-1.2/sense_emu/sense_hat_text.png000066400000000000000000000016141411441564000223040ustar00rootroot00000000000000PNG  IHDR.@ pHYs  tIME ) C+tEXtCommentCreated with GIMPWIDAThZr 5顙-OH;SԚ`O/Iko[k[zFD1χsg hc֚| KCGcs*ƍJ``oXh 8\4wc 3 2"t_ 1`ψe[,1_ Iә v8k9X e8_p5B'ie͠J<cf`qt|?rtIٰ5\FVk2¨KWee\Ew]AjdT$Cmeg,:,@;ImL9NĎ֢}/hxhhUhɡA#evnێ=_B39r(:4F[YMAy =oY F#amnōXbÚj`/X$ cĎ千vq ,]hT}zWOHLJ\mZaq룴1&4? |f+#m-k+`ct1 [ą ;=JqX;WNn-|veM ٯM#<0123456789.=)(ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz?,;:|@%[&_']\~ python-sense-emu-1.2/sense_emu/stick.py000066400000000000000000000401711411441564000202510ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see import io import os import sys import glob import errno import struct import select import inspect import socket from functools import wraps from collections import namedtuple from threading import Thread, Event from queue import Queue, Empty from time import sleep DIRECTION_UP = 'up' DIRECTION_DOWN = 'down' DIRECTION_LEFT = 'left' DIRECTION_RIGHT = 'right' DIRECTION_MIDDLE = 'middle' ACTION_PRESSED = 'pressed' ACTION_RELEASED = 'released' ACTION_HELD = 'held' class InputEvent(namedtuple('InputEvent', ('timestamp', 'direction', 'action'))): """ A :func:`~collections.namedtuple` derivative representing a joystick event. The following attributes are present: .. attribute:: timestamp The time at which the event occurred, represented as the number of seconds since the UNIX epoch (same output as :func:`~time.time`). .. attribute:: direction The direction in which the joystick was pushed (or released from), as one of the constants :data:`DIRECTION_UP`, :data:`DIRECTION_DOWN`, :data:`DIRECTION_LEFT`, :data:`DIRECTION_RIGHT`, :data:`DIRECTION_MIDDLE` .. attribute:: action The action that occurred, as one of the constants :data:`ACTION_PRESSED`, :data:`ACTION_RELEASED`, or :data:`ACTION_HELD`. """ def stick_address(): """ Return the socket address used represent the state of the emulated sense HAT's joystick. On UNIX we try ``/dev/shm`` then fall back to ``/tmp`` (UNIX sockets); on Windows we use localhost. """ fname = 'rpi-sense-emu-stick' if sys.platform.startswith('win'): # use UDP sockets on Windows return (socket.AF_INET, socket.SOCK_DGRAM, ('127.0.0.1', 53753)) else: # use UNIX sockets everywhere else if os.path.exists('/dev/shm'): return (socket.AF_UNIX, socket.SOCK_DGRAM, os.path.join('/dev/shm', fname)) else: return (socket.AF_UNIX, socket.SOCK_DGRAM, os.path.join('/tmp', fname)) def init_stick_client(): """ Opens a socket representing the state of the joystick as a series of evdev events. A file-like object is returned (readable like the character device representing the real joystick). A background thread is spawned to take care of connecting to the stick server (and to automatically handle re-connections in the case of termination). The thread is marked as a daemon thread so it won't prevent script shutdown. """ family, sock_type, addr = stick_address() client = socket.socket(family, sock_type) if family == socket.AF_INET: client.bind(('127.0.0.1', 0)) elif family == socket.AF_UNIX: fname = 'rpi-sense-emu-client-%d' % os.getpid() addr_path = os.path.dirname(addr) try: os.unlink(os.path.join(addr_path, fname)) except OSError as e: if e.errno != errno.ENOENT: raise client.bind(os.path.join(addr_path, fname)) if not client.getsockname(): raise RuntimeError('Failed to create client socket for stick emulation') # Start up a background thread which persistently attempts to connect to # the server and pings it with "hello" to notify it that we want stick # events. This must be persistent in case the emulating client is stopped # and restarted def ping_server(): while True: try: client.connect(addr) client.send(b'hello') except socket.error as e: if e.errno not in (errno.ENOENT, errno.ENOTCONN, errno.ECONNREFUSED): raise sleep(1) thread = Thread(target=ping_server) thread.daemon = True thread.start() # Construct file object on top of the socket which we'll return as the # result return client.makefile('rb', 0) class SenseStick: """ Represents the joystick on the Sense HAT. """ SENSE_HAT_EVDEV_NAME = 'Raspberry Pi Sense HAT Joystick' EVENT_FORMAT = 'llHHI' EVENT_SIZE = struct.calcsize(EVENT_FORMAT) EV_KEY = 0x01 STATE_RELEASE = 0 STATE_PRESS = 1 STATE_HOLD = 2 KEY_UP = 103 KEY_LEFT = 105 KEY_RIGHT = 106 KEY_DOWN = 108 KEY_ENTER = 28 def __init__(self): self._stick_file = self._stick_device() self._callbacks = {} self._callback_thread = None self._callback_event = Event() def close(self): if self._stick_file: self._callbacks.clear() self._start_stop_thread() self._stick_file.close() self._stick_file = None def __enter__(self): return self def __exit__(self, exc_type, exc_value, exc_tb): self.close() def _stick_device(self): """ Discovers the filename of the evdev device that represents the Sense HAT's joystick. """ return init_stick_client() def _read(self): """ Reads a single event from the joystick, blocking until one is available. Returns ``None`` if a non-key event was read, or an :class:`InputEvent` tuple describing the event otherwise. """ event = self._stick_file.read(self.EVENT_SIZE) (tv_sec, tv_usec, type, code, value) = struct.unpack(self.EVENT_FORMAT, event) if type == self.EV_KEY: return InputEvent( timestamp=tv_sec + (tv_usec / 1000000), direction={ self.KEY_UP: DIRECTION_UP, self.KEY_DOWN: DIRECTION_DOWN, self.KEY_LEFT: DIRECTION_LEFT, self.KEY_RIGHT: DIRECTION_RIGHT, self.KEY_ENTER: DIRECTION_MIDDLE, }[code], action={ self.STATE_PRESS: ACTION_PRESSED, self.STATE_RELEASE: ACTION_RELEASED, self.STATE_HOLD: ACTION_HELD, }[value]) else: return None def _wait(self, timeout=None): """ Waits *timeout* seconds until an event is available from the joystick. Returns ``True`` if an event became available, and ``False`` if the timeout expired. """ r, w, x = select.select([self._stick_file], [], [], timeout) return bool(r) def _wrap_callback(self, fn): # Shamelessley nicked (with some variation) from GPIO Zero :) @wraps(fn) def wrapper(event): return fn() if fn is None: return None elif not callable(fn): raise ValueError('value must be None or a callable') elif inspect.isbuiltin(fn): # We can't introspect the prototype of builtins. In this case we # assume that the builtin has no (mandatory) parameters; this is # the most reasonable assumption on the basis that pre-existing # builtins have no knowledge of InputEvent, and the sole parameter # we would pass is an InputEvent return wrapper else: # Try binding ourselves to the argspec of the provided callable. # If this works, assume the function is capable of accepting no # parameters and that we have to wrap it to ignore the event # parameter try: inspect.getcallargs(fn) return wrapper except TypeError: try: # If the above fails, try binding with a single tuple # parameter. If this works, return the callback as is inspect.getcallargs(fn, ()) return fn except TypeError: raise ValueError( 'value must be a callable which accepts up to one ' 'mandatory parameter') def _start_stop_thread(self): if self._callbacks and not self._callback_thread: self._callback_event.clear() self._callback_thread = Thread(target=self._callback_run) self._callback_thread.daemon = True self._callback_thread.start() elif not self._callbacks and self._callback_thread: self._callback_event.set() self._callback_thread.join() self._callback_thread = None def _callback_run(self): while not self._callback_event.wait(0): event = self._read() if event: callback = self._callbacks.get(event.direction) if callback: callback(event) callback = self._callbacks.get('*') if callback: callback(event) def wait_for_event(self, emptybuffer=False): """ Waits until a joystick event becomes available. Returns the event, as an :class:`InputEvent` tuple. If *emptybuffer* is ``True`` (it defaults to ``False``), any pending events will be thrown away first. This is most useful if you are only interested in "pressed" events. """ if emptybuffer: while self._wait(0): self._read() while self._wait(): event = self._read() if event: return event def get_events(self): """ Returns a list of all joystick events that have occurred since the last call to :meth:`get_events`. The list contains events in the order that they occurred. If no events have occurred in the intervening time, the result is an empty list. """ result = [] while self._wait(0): event = self._read() if event: result.append(event) return result @property def direction_up(self): """ The function to be called when the joystick is pushed up. The function can either take a parameter which will be the :class:`InputEvent` tuple that has occurred, or the function can take no parameters at all. Assign ``None`` to prevent this event from being fired. """ return self._callbacks.get(DIRECTION_UP) @direction_up.setter def direction_up(self, value): self._callbacks[DIRECTION_UP] = self._wrap_callback(value) self._start_stop_thread() @property def direction_down(self): """ The function to be called when the joystick is pushed down. The function can either take a parameter which will be the :class:`InputEvent` tuple that has occurred, or the function can take no parameters at all. Assign ``None`` to prevent this event from being fired. """ return self._callbacks.get(DIRECTION_DOWN) @direction_down.setter def direction_down(self, value): self._callbacks[DIRECTION_DOWN] = self._wrap_callback(value) self._start_stop_thread() @property def direction_left(self): """ The function to be called when the joystick is pushed left. The function can either take a parameter which will be the :class:`InputEvent` tuple that has occurred, or the function can take no parameters at all. Assign ``None`` to prevent this event from being fired. """ return self._callbacks.get(DIRECTION_LEFT) @direction_left.setter def direction_left(self, value): self._callbacks[DIRECTION_LEFT] = self._wrap_callback(value) self._start_stop_thread() @property def direction_right(self): """ The function to be called when the joystick is pushed right. The function can either take a parameter which will be the :class:`InputEvent` tuple that has occurred, or the function can take no parameters at all. Assign ``None`` to prevent this event from being fired. """ return self._callbacks.get(DIRECTION_RIGHT) @direction_right.setter def direction_right(self, value): self._callbacks[DIRECTION_RIGHT] = self._wrap_callback(value) self._start_stop_thread() @property def direction_middle(self): """ The function to be called when the joystick middle click is pressed. The function can either take a parameter which will be the :class:`InputEvent` tuple that has occurred, or the function can take no parameters at all. Assign ``None`` to prevent this event from being fired. """ return self._callbacks.get(DIRECTION_MIDDLE) @direction_middle.setter def direction_middle(self, value): self._callbacks[DIRECTION_MIDDLE] = self._wrap_callback(value) self._start_stop_thread() @property def direction_any(self): """ The function to be called when the joystick is used. The function can either take a parameter which will be the :class:`InputEvent` tuple that has occurred, or the function can take no parameters at all. This event will always be called *after* events associated with a specific action. Assign ``None`` to prevent this event from being fired. """ return self._callbacks.get('*') @direction_any.setter def direction_any(self, value): self._callbacks['*'] = self._wrap_callback(value) self._start_stop_thread() class StickServer: def __init__(self): family, sock_type, addr = stick_address() server = socket.socket(family, sock_type) if family == socket.AF_UNIX: try: # Kill any pre-existing socket os.unlink(addr) except OSError: pass server.bind(addr) self._stop = Event() self._queue = Queue() self._thread = Thread(target=self._serve, args=(server,)) self._thread.daemon = True self._thread.start() def _serve(self, server): try: clients = set() while not self._stop.wait(0): # Pick up any new clients waiting to receive events while select.select([server], [], [], 0)[0]: data, addr = server.recvfrom(64) if data == b'hello': clients.add(addr) try: # Grab any data waiting to be sent to clients; we put the # only pause for the thread here to ensure timely response # to events being placed in the queue buf = self._queue.get(timeout=0.1) except Empty: pass else: # Send the event to all connected clients (pruning any that # fail) for client in list(clients): try: server.sendto(buf, client) except socket.error as e: if e.errno in (errno.ENOENT, errno.ECONNREFUSED): clients.remove(client) finally: family = server.family addr = server.getsockname() server.close() if family == socket.AF_UNIX: # Only works because socket name is guaranteed to be absolute os.unlink(addr) def close(self): if self._thread: self._stop.set() self._thread.join() def send(self, buf): self._queue.put(buf) python-sense-emu-1.2/sense_emu/terminal.py000066400000000000000000000237061411441564000207540ustar00rootroot00000000000000# vim: set et sw=4 sts=4 fileencoding=utf-8: # # Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016 Raspberry Pi Foundation # # This package is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free # Software Foundation; either version 2.1 of the License, or (at your option) # any later version. # # This package is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see """ Defines base classes for command line utilities. This module define a TerminalApplication class which provides common facilities to command line applications: a help screen, universal file globbing, response file handling, and common logging configuration and options. """ import sys import io import os import argparse import textwrap import logging import locale import traceback import configparser from .i18n import init_i18n, _ try: # Optionally import argcomplete (for auto-completion) if it's installed import argcomplete except ImportError: argcomplete = None # Set up a console logging handler which just prints messages without any other # adornments. This will be used for logging messages sent before we "properly" # configure logging according to the user's preferences init_i18n() _CONSOLE = logging.StreamHandler(sys.stderr) _CONSOLE.setFormatter(logging.Formatter('%(message)s')) _CONSOLE.setLevel(logging.DEBUG) logging.getLogger().addHandler(_CONSOLE) class FileType: # Variant of argparse.FileType that handles binary stdin/stdout streams # correctly under Python 3 def __init__(self, mode='r', bufsize=-1, encoding=None, errors=None): self._mode = mode self._bufsize = bufsize self._encoding = encoding self._errors = errors def __call__(self, string): if string == '-': if 'r' in self._mode: if 'b' in self._mode: try: return sys.stdin.buffer except AttributeError: pass return sys.stdin elif 'w' in self._mode: if 'b' in self._mode: try: return sys.stdout.buffer except AttributeError: pass return sys.stdout else: raise ValueError(_('argument "-" with mode %r') % self._mode) try: return io.open(string, self._mode, self._bufsize, self._encoding, self._errors) except IOError as e: raise argparse.ArgumentTypeError( _("can't open '%(name)s': %(error)s") % {'name': string, 'error': e}) def __repr__(self): args = self._mode, self._bufsize kwargs = [('encoding', self._encoding), ('errors', self._errors)] args_str = ', '.join([repr(arg) for arg in args if arg != -1] + ['%s=%r' % (kw, arg) for kw, arg in kwargs if arg is not None]) return '%s(%s)' % (type(self).__name__, args_str) class TerminalApplication: """ Base class for command line applications. This class provides command line parsing, file globbing, response file handling and common logging configuration for command line utilities. Descendent classes should override the main() method to implement their main body, and __init__() if they wish to extend the command line options. """ # Get the default output encoding from the default locale encoding = locale.getdefaultlocale()[1] # This class is the abstract base class for each of the command line # utility classes defined. It provides some basic facilities like an option # parser, console pretty-printing, logging and exception handling def __init__( self, version, description=None, config_files=None, config_section=None, config_bools=None): super(TerminalApplication, self).__init__() if description is None: description = self.__doc__ self.parser = argparse.ArgumentParser( description=description, fromfile_prefix_chars='@') self.parser.add_argument( '--version', action='version', version=version) if config_files: self.config = configparser.ConfigParser(interpolation=None) self.config_files = config_files self.config_section = config_section self.config_bools = config_bools self.parser.add_argument( '-c', '--config', metavar='FILE', help=_('specify the configuration file to load')) else: self.config = None self.parser.set_defaults(log_level=logging.WARNING) self.parser.add_argument( '-q', '--quiet', dest='log_level', action='store_const', const=logging.ERROR, help=_('produce less console output')) self.parser.add_argument( '-v', '--verbose', dest='log_level', action='store_const', const=logging.INFO, help=_('produce more console output')) opt = self.parser.add_argument( '-l', '--log-file', metavar='FILE', help=_('log messages to the specified file')) if argcomplete: # XXX Complete with *.log, *.txt #opt.completer = ??? pass self.parser.add_argument( '-P', '--pdb', dest='debug', action='store_true', default=False, help=_('run under PDB (debug mode)')) def __call__(self, args=None): if args is None: args = sys.argv[1:] if argcomplete: argcomplete.autocomplete(self.parser, exclude=['-P']) elif 'COMP_LINE' in os.environ: return 0 sys.excepthook = self.handle args = self.read_configuration(args) args = self.parser.parse_args(args) self.configure_logging(args) if args.debug: try: import pudb except ImportError: pudb = None import pdb return (pudb or pdb).runcall(self.main, args) else: return self.main(args) or 0 def read_configuration(self, args): if not self.config: return args # Parse the --config argument only parser = argparse.ArgumentParser(add_help=False) parser.add_argument('-c', '--config', dest='config', action='store') conf_args, args = parser.parse_known_args(args) if conf_args.config: self.config_files.append(conf_args.config) logging.info( _('Reading configuration from %s'), ', '.join(self.config_files)) conf_read = self.config.read(self.config_files) if conf_args.config and conf_args.config not in conf_read: self.parser.error('unable to read %s' % conf_args.config) if conf_read: if self.config_bools is None: self.config_bools = ['pdb'] else: self.config_bools = ['pdb'] + self.config_bools if not self.config_section: self.config_section = self.config.sections()[0] if not self.config_section in self.config.sections(): self.parser.error( _('unable to locate [%s] section in configuration') % self.config_section) self.parser.set_defaults(**{ key: self.config.getboolean(self.config_section, key) if key in self.config_bools else self.config.get(self.config_section, key) for key in self.config.options(self.config_section) }) return args def configure_logging(self, args): _CONSOLE.setLevel(args.log_level) if args.log_file: log_file = logging.FileHandler(args.log_file) log_file.setFormatter( logging.Formatter('%(asctime)s, %(levelname)s, %(message)s')) log_file.setLevel(logging.DEBUG) logging.getLogger().addHandler(log_file) if args.debug: logging.getLogger().setLevel(logging.DEBUG) else: logging.getLogger().setLevel(logging.INFO) def handle(self, exc_type, exc_value, exc_trace): "Global application exception handler" if issubclass(exc_type, (SystemExit,)): # Exit with 0 ("success") for system exit (as it was intentional) return 0 elif issubclass(exc_type, (KeyboardInterrupt,)): # Exit with 2 if the user deliberately terminates with Ctrl+C return 2 elif issubclass(exc_type, (argparse.ArgumentError,)): # For option parser errors output the error along with a message # indicating how the help page can be displayed logging.critical(str(exc_value)) logging.critical(_('Try the --help option for more information.')) return 2 elif issubclass(exc_type, (IOError,)): # For simple errors like IOError just output the message which # should be sufficient for the end user (no need to confuse them # with a full stack trace) logging.critical(str(exc_value)) return 1 else: # Otherwise, log the stack trace and the exception into the log # file for debugging purposes for line in traceback.format_exception(exc_type, exc_value, exc_trace): for msg in line.rstrip().split('\n'): logging.critical(msg.replace('%', '%%')) return 1 def main(self, args): "Called as the main body of the utility" raise NotImplementedError python-sense-emu-1.2/setup.cfg000066400000000000000000000037371411441564000164270ustar00rootroot00000000000000[metadata] name = sense-emu version = attr: sense_emu.__version__ description = The Raspberry Pi Sense HAT Emulator library long_description = file: README.rst author = Raspberry Pi Foundation author_email = info@raspberrypi.org url = https://sense-emu.readthedocs.io/ project_urls = Documentation = https://sense-emu.readthedocs.io/ Source Code = https://github.com/astro-pi/python-sense-emu Issue Tracker = https://github.com/astro-pi/python-sense-emu/issues keywords = raspberrypi sense hat license = GPL-2.0-or-later classifiers = Development Status :: 5 - Production/Stable Environment :: Console Environment :: X11 Applications :: GTK Intended Audience :: Developers License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) Operating System :: POSIX :: Linux Operating System :: MacOS :: MacOS X Operating System :: Microsoft :: Windows Programming Language :: Python :: 2.7 Programming Language :: Python :: 3.2 Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Topic :: Scientific/Engineering [options] packages = find: install_requires = numpy Pillow [options.package_data] sense_emu = *.ui *.png *.txt sense_emu_gui.svg gschemas.compiled examples/*/*.py locale/*/LC_MESSAGES/*.mo [options.extras_require] test = pytest pytest-cov mock doc = sphinx sphinx-rtd-theme [options.entry_points] console_scripts = sense_rec = sense_emu.record:app sense_play = sense_emu.play:app sense_csv = sense_emu.dump:app gui_scripts = sense_emu_gui = sense_emu.gui:main [tool:pytest] addopts = --cov --tb=short testpaths = tests [coverage:run] source = sense_emu branch = true [coverage:report] ignore_errors = true show_missing = true exclude_lines = assert False raise NotImplementedError pass python-sense-emu-1.2/setup.py000066400000000000000000000002571411441564000163120ustar00rootroot00000000000000# Raspberry Pi Sense HAT Emulator library for the Raspberry Pi # Copyright (c) 2016-2021 Raspberry Pi Foundation from setuptools import setup setup()