From e228457cf1df9c89888911aa2bff06f879217188 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 18 Dec 2021 13:15:38 +0000 Subject: [PATCH 001/485] Initial commit --- .gitignore | 129 ++++++++++ LICENSE | 674 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 805 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ad5fcc --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# sleep_tracker_pinetime_wasp-os +A sleep tracker for the pinetime smartwatch by pine64, on python, to run on wasp-os From 2004f0a88499f25f87681ea9f2fa65ccd315eb04 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 18 Dec 2021 14:34:27 +0100 Subject: [PATCH 002/485] initial readme --- README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ad5fcc..40045ce 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ # sleep_tracker_pinetime_wasp-os -A sleep tracker for the pinetime smartwatch by pine64, on python, to run on wasp-os +A project of sleep tracker for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). + +## Note to reader: +* I'm starting the project around January 2022 +* I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) +* If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) + +## Currently planned features: +**sleep tracking** +* tracks sleep using wrist motion data and occasional heart rate monitoring + * each night is recorded in a file that can be easily sent back to the phone +* rudimentary display of sleep graph on the device itself, with a quality score if I can find a good metric +* try to roughly infer the sleep stage *on the device itself* + * if you actually use the watch during the night, make sure to count it as wakefulness + +**alarm clock** +* setting up an alarm should suggest the most appropriate sleep duration like what [sleepyti.me](http://sleepyti.me) does +* try to optimize the wake up time based on inferred sleep stage + +**settings panel** +* to specify how early the watch can wake you +* to specify a battery threshold under which it sould not keep tracking sleep, to make sure you don't drain the battery and end up missing the alarm clock + +**misc** +* turn off the Bluetooth connection when no phone is connected +* turn off the screen during the night From 000a5a63b4db834e12cc6088e0b7e5a679ea96a2 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 18 Dec 2021 14:35:23 +0100 Subject: [PATCH 003/485] initial readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40045ce..bd0719c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# sleep_tracker_pinetime_wasp-os -A project of sleep tracker for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). +# Waspos Sleep Tracker +**Goal:** sleep tracker for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). ## Note to reader: * I'm starting the project around January 2022 From e9e9e2e6ee2c7144175a90572f1186abe81c7619 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 18 Dec 2021 15:22:24 +0100 Subject: [PATCH 004/485] minor --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bd0719c..0ea0155 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,5 @@ **misc** * turn off the Bluetooth connection when no phone is connected * turn off the screen during the night +* make sure to not use more than X% of the battery in all cases +* make sure to turn off if sleep lasts more than 12h (in which case the user forgot to disable it) From 078028de66dc9ca505e95e89d60744265f29f2e6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 19 Dec 2021 18:56:31 +0100 Subject: [PATCH 005/485] targeted memory reactivation fantasy --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0ea0155..bb7c67f 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,4 @@ * turn off the screen during the night * make sure to not use more than X% of the battery in all cases * make sure to turn off if sleep lasts more than 12h (in which case the user forgot to disable it) +* ability to send in real time to bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. From 6792f3434363cf085a68c13a7bbdce744f374532 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 20 Dec 2021 16:15:12 +0100 Subject: [PATCH 006/485] typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb7c67f..dc31dca 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ **settings panel** * to specify how early the watch can wake you -* to specify a battery threshold under which it sould not keep tracking sleep, to make sure you don't drain the battery and end up missing the alarm clock +* to specify a battery threshold under which it should not keep tracking sleep, to make sure you don't drain the battery and end up missing the alarm clock **misc** * turn off the Bluetooth connection when no phone is connected * turn off the screen during the night * make sure to not use more than X% of the battery in all cases * make sure to turn off if sleep lasts more than 12h (in which case the user forgot to disable it) -* ability to send in real time to bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. +* ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. From 90bb4af5e6540b3f643ad843722305f97f46d4da Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 20 Dec 2021 16:16:20 +0100 Subject: [PATCH 007/485] hardcode limits if heart rate is unreliable --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dc31dca..ba15381 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,4 @@ * make sure to not use more than X% of the battery in all cases * make sure to turn off if sleep lasts more than 12h (in which case the user forgot to disable it) * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. +* hardcode limits to avoid issues if heart rate is suddenly found to be through the roof or something From ebc07f156651e99d469389c26f159cb8017dc96f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 20 Dec 2021 16:16:51 +0100 Subject: [PATCH 008/485] minor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba15381..3a7490f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Goal:** sleep tracker for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). ## Note to reader: -* I'm starting the project around January 2022 +* I'm intend to start coding around January 2022 * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) From 43745886cde503e9fb75ef510361a28780788dcd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 27 Dec 2021 13:36:44 +0100 Subject: [PATCH 009/485] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a7490f..d4d64a1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Goal:** sleep tracker for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). ## Note to reader: -* I'm intend to start coding around January 2022 +* I plan to start coding around January 2022 * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) From f1f99b1b6d8c4f942b0edf67ebce8b29e414d441 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 27 Dec 2021 13:37:07 +0100 Subject: [PATCH 010/485] first step: log sleep data to calibrate algo --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d4d64a1..f40114c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,10 @@ * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) -## Currently planned features: +## Roadmap / Currently planned features: +**First step** +* focus on logging your sleep to accumulate data and share it on github. The more data you have the easier it will be to calibrate the algorithm. + **sleep tracking** * tracks sleep using wrist motion data and occasional heart rate monitoring * each night is recorded in a file that can be easily sent back to the phone From 42a51a7c2cafe7d3c89dac68c0f02dc1971b4f32 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 27 Dec 2021 13:37:17 +0100 Subject: [PATCH 011/485] added features I'm not sure yet --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f40114c..7cc3477 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,6 @@ * make sure to turn off if sleep lasts more than 12h (in which case the user forgot to disable it) * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. * hardcode limits to avoid issues if heart rate is suddenly found to be through the roof or something + +**Features that I'm note sure yet** +* should the watch ask you after waking up to rate your sleep on a simple scale? From 46071c9efe9a9ef12950251e5415e0aca3345f1e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 9 Jan 2022 09:22:50 +0100 Subject: [PATCH 012/485] added section: related links --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 7cc3477..13646b9 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,8 @@ **Features that I'm note sure yet** * should the watch ask you after waking up to rate your sleep on a simple scale? + +## Related links: +* very interesting research paper on the topic : https://academic.oup.com/sleep/article/42/12/zsz180/5549536 +* maybe coding a 1D convolution is a good way to extract peaks +* list of ways to find local maxima in python : https://blog.finxter.com/how-to-find-local-minima-in-1d-and-2d-numpy-arrays/ + https://pythonawesome.com/overview-of-the-peaks-dectection-algorithms-available-in-python/ From b87b8e1bcc3a1385ea0e047b2fcc8580d55106b7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 16 Jan 2022 16:59:08 +0100 Subject: [PATCH 013/485] docs: wake up after duration or at specific time --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 13646b9..4840b9d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ * if you actually use the watch during the night, make sure to count it as wakefulness **alarm clock** -* setting up an alarm should suggest the most appropriate sleep duration like what [sleepyti.me](http://sleepyti.me) does +* setting up an alarm should suggest the most appropriate sleep duration like what [sleepyti.me](http://sleepyti.me) does, so either sleep duration or by wake up time * try to optimize the wake up time based on inferred sleep stage **settings panel** From de4bb830cf29810f08d1e40a9e30a7e749cb27b4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 16 Jan 2022 16:59:27 +0100 Subject: [PATCH 014/485] docs: idea of OLS regression of known sinusoidal function --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4840b9d..b567a24 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ * make sure to turn off if sleep lasts more than 12h (in which case the user forgot to disable it) * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. * hardcode limits to avoid issues if heart rate is suddenly found to be through the roof or something +* maybe the least cpu intensive way to compute optimal wake up time would be to compute least square difference with sinusoidal with varying periods and phases. **Features that I'm note sure yet** * should the watch ask you after waking up to rate your sleep on a simple scale? From 9fcdf60183220fed51cea538961ba2bd4ed5ecee Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 17 Jan 2022 23:00:14 +0100 Subject: [PATCH 015/485] docs: added link to detaileed implementation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b567a24..e6d242f 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ * should the watch ask you after waking up to rate your sleep on a simple scale? ## Related links: +* article with detailed implementation : https://www.nature.com/articles/s41598-018-31266-z * very interesting research paper on the topic : https://academic.oup.com/sleep/article/42/12/zsz180/5549536 * maybe coding a 1D convolution is a good way to extract peaks * list of ways to find local maxima in python : https://blog.finxter.com/how-to-find-local-minima-in-1d-and-2d-numpy-arrays/ + https://pythonawesome.com/overview-of-the-peaks-dectection-algorithms-available-in-python/ From 590ec28fd2edf28973152b5086752230ad3aaa82 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 15:59:00 +0100 Subject: [PATCH 016/485] docs: added Status section --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e6d242f..7b3fb1d 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,12 @@ **Goal:** sleep tracker for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). ## Note to reader: -* I plan to start coding around January 2022 * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) +## Status: +* **trying to create a sleep logger app, this will help run code on the simulator** + ## Roadmap / Currently planned features: **First step** * focus on logging your sleep to accumulate data and share it on github. The more data you have the easier it will be to calibrate the algorithm. From 6d567589ef760d172d16b97aafdb34f537b64d68 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 15:59:28 +0100 Subject: [PATCH 017/485] added first draft of SleepTracker (only logger for now) --- SleepTracker.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 SleepTracker.py diff --git a/SleepTracker.py b/SleepTracker.py new file mode 100644 index 0000000..a089dfc --- /dev/null +++ b/SleepTracker.py @@ -0,0 +1,86 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +# Copyright (C) 2021 github.com/thiswillbeyourgithub/ + +"""Sleep tracker +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# https://github.com/thiswillbeyourgithub/sleep_tracker_pinetime_wasp-os + +End goal: +This app is designed to track accelerometer and heart rate data periodically +during the night. It can also compute the best time to wake you up, up to +30 minutes before the alarm you set up manually. +""" + +import wasp +import watch +import widgets + + +class SleepTrackerApp(): + NAME = 'SleepT' + + def __init__(self): + self.filep = "sleep_tracking.txt" + try: + f = open(self.filep, "r") + f.close() + except FileNotFoundError: + f = open(self.filep, "w") + f.write("") + f.close() + self.buff = "" # accel data not yet written to disk + self._tracking = None # None = not tracking, else = start timestamp + + def foreground(self): + self._draw() + wasp.system.request_event(wasp.EventMask.TOUCH) + + def background(self): + f = open(self.filep, "a") + f.write(self.buff) + self.buff = "" + f.close() + return True + + def touch(self, event): + if self.btn_on: + if self.btn_on.touch(event): + self._tracking = watch.rtc.get_time() + wasp.system.request_tick(300000) # every 5 minutes + else: + if self.btn_off.touch(event): + self._tracking = None + self._draw() + + def tick(self, ticks): + if self._tracking is not None: + acc = [str(x) for x in watch.accel.read_xyz()] + self.buff += "\n" + watch.rtc.time() + ",".join(acc) + self._periodicSave() + + def _periodicSave(self): + if len(self.buff.split("\n")) > 30: + f = open(self.filep, "a") + f.write(self.buff) + self.buff = "" + f.close() + + def _draw(self): + draw = wasp.watch.drawable + draw.fill(0) + draw.string("Sleep Tracker", 40, 0) + if self._tracking is None: + self.btn_on = widgets.Button(x=50, y=120, w=100, h=100, label="On") + self.btn_on.draw() + self.btn_off = None + else: + self.btn_off = widgets.Button(x=50, y=120, w=100, h=100, label="Off") + h = str(self._tracking[0]) + m = str(self._tracking[1]) + draw.string('Started at', 50, 70) + draw.string(h + "h" + m + "m", 50, 80) + self.btn_off.draw() + self.btn_on = None + wasp.system.bar.clock = True + wasp.system.bar.battery = True From 6a44c80602a9464764d9fa86149f3a539fd3cc2a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 18:26:20 +0100 Subject: [PATCH 018/485] modified: SleepTracker.py using set_alarm instead of ticks still untested --- SleepTracker.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/SleepTracker.py b/SleepTracker.py index a089dfc..65e8c22 100644 --- a/SleepTracker.py +++ b/SleepTracker.py @@ -18,10 +18,12 @@ import widgets class SleepTrackerApp(): - NAME = 'SleepT' + NAME = 'ST' def __init__(self): self.filep = "sleep_tracking.txt" + self.freq = 5 # poll accelerometer data every X minutes + assert self.freq < 60 try: f = open(self.filep, "r") f.close() @@ -43,24 +45,50 @@ class SleepTrackerApp(): f.close() return True + def _add_alarm(self): + """ + adds an alarm to the next 5 minutes to log the accelerometer data + once + In the current implementation, if the night starts on the last day of + the month, it will probably create a bug. + """ + now = watch.rtc.get_localtime() + yyyy = now[0] + mm = now[1] + dd = now[2] + hh = now[3] + mn = now[4] + self.freq + if mn >= 60: + mn -= 60 + hh += 1 + if hh >= 24: + hh -= 24 + dd += 1 + self.next_al = (yyyy, mm, dd, hh, mn, 0, 0, 0, 0) + wasp.system.set_alarm(self.next_al, self._trackOnce) + def touch(self, event): if self.btn_on: if self.btn_on.touch(event): self._tracking = watch.rtc.get_time() - wasp.system.request_tick(300000) # every 5 minutes + # add data point every self.freq minutes + self._add_alarm() else: if self.btn_off.touch(event): self._tracking = None + wasp.system.cancel_alarm(self.next_al, self._trackOnce) + self._periodicSave() self._draw() - def tick(self, ticks): + def _trackOnce(self): if self._tracking is not None: acc = [str(x) for x in watch.accel.read_xyz()] self.buff += "\n" + watch.rtc.time() + ",".join(acc) + self._add_alarm() self._periodicSave() def _periodicSave(self): - if len(self.buff.split("\n")) > 30: + if len(self.buff.split("\n")) > self.freq: f = open(self.filep, "a") f.write(self.buff) self.buff = "" From 6709be732700dc79599b32a3028ae6cf087339de Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 18:36:28 +0100 Subject: [PATCH 019/485] fix: forgot to use time.mktime --- SleepTracker.py => SleepT.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename SleepTracker.py => SleepT.py (97%) diff --git a/SleepTracker.py b/SleepT.py similarity index 97% rename from SleepTracker.py rename to SleepT.py index 65e8c22..ea30969 100644 --- a/SleepTracker.py +++ b/SleepT.py @@ -13,6 +13,7 @@ during the night. It can also compute the best time to wake you up, up to """ import wasp +import time import watch import widgets @@ -64,7 +65,7 @@ class SleepTrackerApp(): if hh >= 24: hh -= 24 dd += 1 - self.next_al = (yyyy, mm, dd, hh, mn, 0, 0, 0, 0) + self.next_al = time.mktime((yyyy, mm, dd, hh, mn, 0, 0, 0, 0)) wasp.system.set_alarm(self.next_al, self._trackOnce) def touch(self, event): From 57b7b90dd5a1a8337232074ecd482576b8fb64f6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 18:36:39 +0100 Subject: [PATCH 020/485] shortenned SleepTracker to SleepT --- SleepT.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepT.py b/SleepT.py index ea30969..e508107 100644 --- a/SleepT.py +++ b/SleepT.py @@ -18,8 +18,8 @@ import watch import widgets -class SleepTrackerApp(): - NAME = 'ST' +class SleepTApp(): + NAME = 'SleepT' def __init__(self): self.filep = "sleep_tracking.txt" From 1dc5c78daf983c918f8fd628fb23c617d1df3ba7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 18:36:52 +0100 Subject: [PATCH 021/485] added TODO list to the to of the file --- SleepT.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SleepT.py b/SleepT.py index e508107..7a15d9b 100644 --- a/SleepT.py +++ b/SleepT.py @@ -17,6 +17,10 @@ import time import watch import widgets +# TODO : +# * use one file per recording +# * handle last time of the month issue +# * class SleepTApp(): NAME = 'SleepT' From 1ce00db2df19d03197c9104bb6ae635339d4cf0d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 22:42:35 +0100 Subject: [PATCH 022/485] fix: time.mktime() does the job --- SleepT.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/SleepT.py b/SleepT.py index 7a15d9b..09a6cc1 100644 --- a/SleepT.py +++ b/SleepT.py @@ -54,8 +54,8 @@ class SleepTApp(): """ adds an alarm to the next 5 minutes to log the accelerometer data once - In the current implementation, if the night starts on the last day of - the month, it will probably create a bug. + In the current implementation, time.mktime takes care of the modulo + if for example the next alarm is at 4h65minutes """ now = watch.rtc.get_localtime() yyyy = now[0] @@ -63,12 +63,6 @@ class SleepTApp(): dd = now[2] hh = now[3] mn = now[4] + self.freq - if mn >= 60: - mn -= 60 - hh += 1 - if hh >= 24: - hh -= 24 - dd += 1 self.next_al = time.mktime((yyyy, mm, dd, hh, mn, 0, 0, 0, 0)) wasp.system.set_alarm(self.next_al, self._trackOnce) From 2fdb751b8f5f933e5a7e8fde29e0b97273db1633 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 22:42:48 +0100 Subject: [PATCH 023/485] fix: minor string handling --- SleepT.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index 09a6cc1..5771c75 100644 --- a/SleepT.py +++ b/SleepT.py @@ -82,8 +82,9 @@ class SleepTApp(): def _trackOnce(self): if self._tracking is not None: acc = [str(x) for x in watch.accel.read_xyz()] - self.buff += "\n" + watch.rtc.time() + ",".join(acc) + self.buff += "\n" + str(int(watch.rtc.time())) + "," + ",".join(acc) self._add_alarm() + print(self.buff) self._periodicSave() def _periodicSave(self): From cd29be0f5085fca2524755627b99ded14a5b720a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 22:43:01 +0100 Subject: [PATCH 024/485] minor: cleaner UI --- SleepT.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepT.py b/SleepT.py index 5771c75..39bc6db 100644 --- a/SleepT.py +++ b/SleepT.py @@ -99,15 +99,15 @@ class SleepTApp(): draw.fill(0) draw.string("Sleep Tracker", 40, 0) if self._tracking is None: - self.btn_on = widgets.Button(x=50, y=120, w=100, h=100, label="On") + self.btn_on = widgets.Button(x=0, y=170, w=240, h=69, label="On") self.btn_on.draw() self.btn_off = None else: - self.btn_off = widgets.Button(x=50, y=120, w=100, h=100, label="Off") + self.btn_off = widgets.Button(x=0, y=170, w=240, h=69, label="Off") h = str(self._tracking[0]) m = str(self._tracking[1]) draw.string('Started at', 50, 70) - draw.string(h + "h" + m + "m", 50, 80) + draw.string(h + "h" + m + "m", 50, 90) self.btn_off.draw() self.btn_on = None wasp.system.bar.clock = True From 38c699c6b050ad72cbb6715586bcb45ff54e8ceb Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 22:54:38 +0100 Subject: [PATCH 025/485] better code + poll every X seconds is possible --- SleepT.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/SleepT.py b/SleepT.py index 39bc6db..1fce9ce 100644 --- a/SleepT.py +++ b/SleepT.py @@ -27,7 +27,7 @@ class SleepTApp(): def __init__(self): self.filep = "sleep_tracking.txt" - self.freq = 5 # poll accelerometer data every X minutes + self.freq = 60 # poll accelerometer data every X seconds assert self.freq < 60 try: f = open(self.filep, "r") @@ -54,16 +54,8 @@ class SleepTApp(): """ adds an alarm to the next 5 minutes to log the accelerometer data once - In the current implementation, time.mktime takes care of the modulo - if for example the next alarm is at 4h65minutes """ - now = watch.rtc.get_localtime() - yyyy = now[0] - mm = now[1] - dd = now[2] - hh = now[3] - mn = now[4] + self.freq - self.next_al = time.mktime((yyyy, mm, dd, hh, mn, 0, 0, 0, 0)) + self.next_al = time.mktime(watch.rtc.get_localtime()) + self.freq wasp.system.set_alarm(self.next_al, self._trackOnce) def touch(self, event): From 5359d439cce3f3d00460d8c59141b64001a57486 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 25 Jan 2022 22:54:51 +0100 Subject: [PATCH 026/485] todo --- SleepT.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SleepT.py b/SleepT.py index 1fce9ce..168baaa 100644 --- a/SleepT.py +++ b/SleepT.py @@ -19,8 +19,6 @@ import widgets # TODO : # * use one file per recording -# * handle last time of the month issue -# * class SleepTApp(): NAME = 'SleepT' From 0e4942250d5a767d1e12899431b8ccd2a1080a20 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 18:59:56 +0100 Subject: [PATCH 027/485] minor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b3fb1d..0121a7f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ * make sure to turn off if sleep lasts more than 12h (in which case the user forgot to disable it) * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. * hardcode limits to avoid issues if heart rate is suddenly found to be through the roof or something -* maybe the least cpu intensive way to compute optimal wake up time would be to compute least square difference with sinusoidal with varying periods and phases. +* maybe the least cpu intensive way to compute optimal wake up time would be to compute least square difference with sinusoidal of varying periods and phases. **Features that I'm note sure yet** * should the watch ask you after waking up to rate your sleep on a simple scale? From f7146fe05e2d0940fe536668dba8e331881b52b9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 19:08:39 +0100 Subject: [PATCH 028/485] fix: remove wrong assert --- SleepT.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index 168baaa..628d6b9 100644 --- a/SleepT.py +++ b/SleepT.py @@ -26,7 +26,6 @@ class SleepTApp(): def __init__(self): self.filep = "sleep_tracking.txt" self.freq = 60 # poll accelerometer data every X seconds - assert self.freq < 60 try: f = open(self.filep, "r") f.close() From 80a060133a1e5a36abe225ffdd5c3750b7853f60 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 19:08:59 +0100 Subject: [PATCH 029/485] minor: add newline after each line instead of before --- SleepT.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index 628d6b9..4440abd 100644 --- a/SleepT.py +++ b/SleepT.py @@ -71,7 +71,7 @@ class SleepTApp(): def _trackOnce(self): if self._tracking is not None: acc = [str(x) for x in watch.accel.read_xyz()] - self.buff += "\n" + str(int(watch.rtc.time())) + "," + ",".join(acc) + self.buff += str(int(watch.rtc.time())) + "," + ",".join(acc) + "\n" self._add_alarm() print(self.buff) self._periodicSave() From 7e30f5fe67c40fbdb46f7962e6c8e2afd061f216 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 19:18:34 +0100 Subject: [PATCH 030/485] new: create one logging file per run --- SleepT.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/SleepT.py b/SleepT.py index 4440abd..552dc8c 100644 --- a/SleepT.py +++ b/SleepT.py @@ -24,16 +24,7 @@ class SleepTApp(): NAME = 'SleepT' def __init__(self): - self.filep = "sleep_tracking.txt" self.freq = 60 # poll accelerometer data every X seconds - try: - f = open(self.filep, "r") - f.close() - except FileNotFoundError: - f = open(self.filep, "w") - f.write("") - f.close() - self.buff = "" # accel data not yet written to disk self._tracking = None # None = not tracking, else = start timestamp def foreground(self): @@ -58,9 +49,13 @@ class SleepTApp(): def touch(self, event): if self.btn_on: if self.btn_on.touch(event): + # create one file for each run + tod = [str(x) for x in watch.rtc.get_localtime()[0:5]] + self.filep = "sleep_data_" + "_".join(tod) + ".txt" self._tracking = watch.rtc.get_time() # add data point every self.freq minutes self._add_alarm() + self.buff = "" # accel data not yet written to disk else: if self.btn_off.touch(event): self._tracking = None From 356e468e2dd5227771b71dda50b22df1b733c8ba Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 19:19:04 +0100 Subject: [PATCH 031/485] new: log data on background only if running --- SleepT.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SleepT.py b/SleepT.py index 552dc8c..737b1f2 100644 --- a/SleepT.py +++ b/SleepT.py @@ -32,11 +32,11 @@ class SleepTApp(): wasp.system.request_event(wasp.EventMask.TOUCH) def background(self): - f = open(self.filep, "a") - f.write(self.buff) - self.buff = "" - f.close() - return True + if self._tracking is not None: + f = open(self.filep, "a") + f.write(self.buff) + self.buff = "" + f.close() def _add_alarm(self): """ From 67764fe8c660bfac359a70e73b199e6331e248bb Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 19:19:43 +0100 Subject: [PATCH 032/485] minor: renamed function --- SleepT.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/SleepT.py b/SleepT.py index 737b1f2..d24cbd4 100644 --- a/SleepT.py +++ b/SleepT.py @@ -38,7 +38,7 @@ class SleepTApp(): self.buff = "" f.close() - def _add_alarm(self): + def _add_accel_alar(self): """ adds an alarm to the next 5 minutes to log the accelerometer data once @@ -54,7 +54,7 @@ class SleepTApp(): self.filep = "sleep_data_" + "_".join(tod) + ".txt" self._tracking = watch.rtc.get_time() # add data point every self.freq minutes - self._add_alarm() + self._add_accel_alar() self.buff = "" # accel data not yet written to disk else: if self.btn_off.touch(event): @@ -67,8 +67,7 @@ class SleepTApp(): if self._tracking is not None: acc = [str(x) for x in watch.accel.read_xyz()] self.buff += str(int(watch.rtc.time())) + "," + ",".join(acc) + "\n" - self._add_alarm() - print(self.buff) + self._add_accel_alar() self._periodicSave() def _periodicSave(self): From de58a40825f4d646213dface28569e482c3e76d0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 19:20:06 +0100 Subject: [PATCH 033/485] added docstrings --- SleepT.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index d24cbd4..ce09d80 100644 --- a/SleepT.py +++ b/SleepT.py @@ -40,7 +40,7 @@ class SleepTApp(): def _add_accel_alar(self): """ - adds an alarm to the next 5 minutes to log the accelerometer data + set an alarm, due in self.freq minutes, to log the accelerometer data once """ self.next_al = time.mktime(watch.rtc.get_localtime()) + self.freq @@ -64,6 +64,8 @@ class SleepTApp(): self._draw() def _trackOnce(self): + """get one data point of accelerometer + this function is called every self.freq seconds""" if self._tracking is not None: acc = [str(x) for x in watch.accel.read_xyz()] self.buff += str(int(watch.rtc.time())) + "," + ",".join(acc) + "\n" @@ -72,12 +74,15 @@ class SleepTApp(): def _periodicSave(self): if len(self.buff.split("\n")) > self.freq: + "save data to file only every few checks" + if len(self.buff.split("\n")) > 20 or force_save: f = open(self.filep, "a") f.write(self.buff) self.buff = "" f.close() def _draw(self): + "GUI" draw = wasp.watch.drawable draw.fill(0) draw.string("Sleep Tracker", 40, 0) From d52a1d62c79bc0f444987d31f96905f3c2909aec Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 19:20:23 +0100 Subject: [PATCH 034/485] force saving to file when clicking stop --- SleepT.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SleepT.py b/SleepT.py index ce09d80..4755a84 100644 --- a/SleepT.py +++ b/SleepT.py @@ -60,7 +60,7 @@ class SleepTApp(): if self.btn_off.touch(event): self._tracking = None wasp.system.cancel_alarm(self.next_al, self._trackOnce) - self._periodicSave() + self._periodicSave(force_save=True) self._draw() def _trackOnce(self): @@ -72,8 +72,7 @@ class SleepTApp(): self._add_accel_alar() self._periodicSave() - def _periodicSave(self): - if len(self.buff.split("\n")) > self.freq: + def _periodicSave(self, force_save=False): "save data to file only every few checks" if len(self.buff.split("\n")) > 20 or force_save: f = open(self.filep, "a") From ddc7e60f749723633772fbaea89cdd52f9e2a693 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 19:27:50 +0100 Subject: [PATCH 035/485] fix: buff not initialized at the right place --- SleepT.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index 4755a84..5e82df2 100644 --- a/SleepT.py +++ b/SleepT.py @@ -49,13 +49,13 @@ class SleepTApp(): def touch(self, event): if self.btn_on: if self.btn_on.touch(event): + self.buff = "" # accel data not yet written to disk # create one file for each run tod = [str(x) for x in watch.rtc.get_localtime()[0:5]] self.filep = "sleep_data_" + "_".join(tod) + ".txt" self._tracking = watch.rtc.get_time() # add data point every self.freq minutes self._add_accel_alar() - self.buff = "" # accel data not yet written to disk else: if self.btn_off.touch(event): self._tracking = None From 5e005af4e4c0bcc5f205fba0828c73c4feae8490 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 19:47:51 +0100 Subject: [PATCH 036/485] minor: don't redraw UI if clicking outside buttons --- SleepT.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index 5e82df2..bc8973a 100644 --- a/SleepT.py +++ b/SleepT.py @@ -56,12 +56,13 @@ class SleepTApp(): self._tracking = watch.rtc.get_time() # add data point every self.freq minutes self._add_accel_alar() + self._draw() else: if self.btn_off.touch(event): self._tracking = None wasp.system.cancel_alarm(self.next_al, self._trackOnce) self._periodicSave(force_save=True) - self._draw() + self._draw() def _trackOnce(self): """get one data point of accelerometer From ba714563369d61170fd97b64b98a7768dd9c24d6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 20:01:27 +0100 Subject: [PATCH 037/485] todo --- SleepT.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index bc8973a..686bfde 100644 --- a/SleepT.py +++ b/SleepT.py @@ -18,7 +18,7 @@ import watch import widgets # TODO : -# * use one file per recording +# * class SleepTApp(): NAME = 'SleepT' From f2b8131d51b6ccd7c7468e00c38556629026c533 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 20:14:09 +0100 Subject: [PATCH 038/485] feat: store sleep data in a directory --- SleepT.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index 686bfde..f32a8c2 100644 --- a/SleepT.py +++ b/SleepT.py @@ -16,6 +16,7 @@ import wasp import time import watch import widgets +import os # TODO : # * @@ -26,6 +27,8 @@ class SleepTApp(): def __init__(self): self.freq = 60 # poll accelerometer data every X seconds self._tracking = None # None = not tracking, else = start timestamp + if not os.path.exists("sleep_accel_data"): + os.makedirs("sleep_accel_data") def foreground(self): self._draw() @@ -52,7 +55,7 @@ class SleepTApp(): self.buff = "" # accel data not yet written to disk # create one file for each run tod = [str(x) for x in watch.rtc.get_localtime()[0:5]] - self.filep = "sleep_data_" + "_".join(tod) + ".txt" + self.filep = "sleep_accel_data/" + "_".join(tod) + ".txt" self._tracking = watch.rtc.get_time() # add data point every self.freq minutes self._add_accel_alar() From 8ad3428307f60c6c5f180c3c0b2187fd96f20306 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 26 Jan 2022 20:18:56 +0100 Subject: [PATCH 039/485] docs: mention current state --- SleepT.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SleepT.py b/SleepT.py index f32a8c2..62d9222 100644 --- a/SleepT.py +++ b/SleepT.py @@ -10,6 +10,9 @@ End goal: This app is designed to track accelerometer and heart rate data periodically during the night. It can also compute the best time to wake you up, up to 30 minutes before the alarm you set up manually. + +Current state: +Trying to log my sleep data for a few days prior to working on the algorithm """ import wasp From 33815d6829b1adc5aa50898a2479954ba8ec7ad3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 00:06:56 +0100 Subject: [PATCH 040/485] fix: using mkdir instead of os.makedirs --- SleepT.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SleepT.py b/SleepT.py index 62d9222..7d1f123 100644 --- a/SleepT.py +++ b/SleepT.py @@ -19,7 +19,7 @@ import wasp import time import watch import widgets -import os +from shell import mkdir # TODO : # * @@ -30,8 +30,7 @@ class SleepTApp(): def __init__(self): self.freq = 60 # poll accelerometer data every X seconds self._tracking = None # None = not tracking, else = start timestamp - if not os.path.exists("sleep_accel_data"): - os.makedirs("sleep_accel_data") + mkdir("sleep_accel_data") def foreground(self): self._draw() From a428f38c30dfe11de7c5622a853dcaf0a852cbc0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 00:29:00 +0100 Subject: [PATCH 041/485] fix: mkdir crash if folder already exists --- SleepT.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index 7d1f123..87bff4a 100644 --- a/SleepT.py +++ b/SleepT.py @@ -30,7 +30,10 @@ class SleepTApp(): def __init__(self): self.freq = 60 # poll accelerometer data every X seconds self._tracking = None # None = not tracking, else = start timestamp - mkdir("sleep_accel_data") + try: + mkdir("sleep_accel_data") + except FileExistsError: + pass def foreground(self): self._draw() From 349cfebc8fa50618f18f696c03616feca2daf177 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 01:06:00 +0100 Subject: [PATCH 042/485] minor: docstrings --- SleepT.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/SleepT.py b/SleepT.py index 87bff4a..770cee1 100644 --- a/SleepT.py +++ b/SleepT.py @@ -47,10 +47,8 @@ class SleepTApp(): f.close() def _add_accel_alar(self): - """ - set an alarm, due in self.freq minutes, to log the accelerometer data - once - """ + """set an alarm, due in self.freq minutes, to log the accelerometer data + once""" self.next_al = time.mktime(watch.rtc.get_localtime()) + self.freq wasp.system.set_alarm(self.next_al, self._trackOnce) @@ -82,7 +80,7 @@ class SleepTApp(): self._periodicSave() def _periodicSave(self, force_save=False): - "save data to file only every few checks" + """save data to file only every few checks""" if len(self.buff.split("\n")) > 20 or force_save: f = open(self.filep, "a") f.write(self.buff) @@ -90,7 +88,7 @@ class SleepTApp(): f.close() def _draw(self): - "GUI" + """GUI""" draw = wasp.watch.drawable draw.fill(0) draw.string("Sleep Tracker", 40, 0) From e2fbb70d495631c22232a7234531a0d7ce00f5c6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 01:14:24 +0100 Subject: [PATCH 043/485] docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0121a7f..f8674c9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Waspos Sleep Tracker -**Goal:** sleep tracker for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). +**Goal:** sleep tracker for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os), that wakes you up at the best time. ## Note to reader: * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) From 29536a0ac02832bf7d564548f2acadf61774c155 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 01:14:37 +0100 Subject: [PATCH 044/485] docs: added instructions --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f8674c9..044b0f8 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,14 @@ ## Note to reader: * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) - -## Status: -* **trying to create a sleep logger app, this will help run code on the simulator** +* Status: + * Currently a sleep logger app, this will help run code on the simulator + * **Instructions**: (with modified wasp-os that exposes accel data) + * get the latest python file : SleepT.py + * compile it : `./micropython/mpy-cross/mpy-cross -mno-unicode -march=armv7m SleepT.py` + * send compiled : `./tools/wasptool --verbose --upload SleepT.mpy --binary` + * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('SleepT.SleepTApp')` + * run it! ## Roadmap / Currently planned features: **First step** From f5cd59defa7700cd53a8a144873cb4916028a24c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 01:14:46 +0100 Subject: [PATCH 045/485] todo --- README.md | 2 +- SleepT.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 044b0f8..51a4314 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## Roadmap / Currently planned features: **First step** -* focus on logging your sleep to accumulate data and share it on github. The more data you have the easier it will be to calibrate the algorithm. +* ~~focus on logging your sleep to accumulate data and share it on github~~DONE. The more data you have the easier it will be to calibrate the algorithm. **sleep tracking** * tracks sleep using wrist motion data and occasional heart rate monitoring diff --git a/SleepT.py b/SleepT.py index 770cee1..4c79d7c 100644 --- a/SleepT.py +++ b/SleepT.py @@ -21,8 +21,6 @@ import watch import widgets from shell import mkdir -# TODO : -# * class SleepTApp(): NAME = 'SleepT' From 0e32eabb83ff72e918cd3b452de47fcbd266a38c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 01:36:22 +0100 Subject: [PATCH 046/485] docs: better --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 51a4314..c91b9c2 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ * **Instructions**: (with modified wasp-os that exposes accel data) * get the latest python file : SleepT.py * compile it : `./micropython/mpy-cross/mpy-cross -mno-unicode -march=armv7m SleepT.py` - * send compiled : `./tools/wasptool --verbose --upload SleepT.mpy --binary` - * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('SleepT.SleepTApp')` + * send compiled : `./tools/wasptool --verbose --upload SleepT.mpy --as apps/SleepT.mpy --binary` + * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('apps.SleepT.SleepTApp')` * run it! ## Roadmap / Currently planned features: From 862b181e55fca0460e5a944dd3076859d2a423ed Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 01:36:35 +0100 Subject: [PATCH 047/485] fix: proper creation of directory --- SleepT.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepT.py b/SleepT.py index 4c79d7c..a9fd349 100644 --- a/SleepT.py +++ b/SleepT.py @@ -19,7 +19,7 @@ import wasp import time import watch import widgets -from shell import mkdir +import shell class SleepTApp(): @@ -29,8 +29,8 @@ class SleepTApp(): self.freq = 60 # poll accelerometer data every X seconds self._tracking = None # None = not tracking, else = start timestamp try: - mkdir("sleep_accel_data") - except FileExistsError: + shell.mkdir("sleep_accel_data") + except: # file exists pass def foreground(self): From 6aceca3726bac476149d2867842a5d9dee4fd234 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 12:03:17 +0100 Subject: [PATCH 048/485] minor: change default freq to 5 minutes --- SleepT.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index a9fd349..c45d1bc 100644 --- a/SleepT.py +++ b/SleepT.py @@ -26,7 +26,7 @@ class SleepTApp(): NAME = 'SleepT' def __init__(self): - self.freq = 60 # poll accelerometer data every X seconds + self.freq = 300 # poll accelerometer data every X seconds self._tracking = None # None = not tracking, else = start timestamp try: shell.mkdir("sleep_accel_data") From b094cf566639b499c2d73e4ab82e0912afdf51a6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 12:25:53 +0100 Subject: [PATCH 049/485] new: _tracking is boolean, store start time elsewhere and print battery and number of datapoints --- SleepT.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/SleepT.py b/SleepT.py index c45d1bc..e543e38 100644 --- a/SleepT.py +++ b/SleepT.py @@ -27,7 +27,7 @@ class SleepTApp(): def __init__(self): self.freq = 300 # poll accelerometer data every X seconds - self._tracking = None # None = not tracking, else = start timestamp + self._tracking = False # False = not tracking, True = currently tracking try: shell.mkdir("sleep_accel_data") except: # file exists @@ -53,17 +53,20 @@ class SleepTApp(): def touch(self, event): if self.btn_on: if self.btn_on.touch(event): + self._tracking = True self.buff = "" # accel data not yet written to disk # create one file for each run tod = [str(x) for x in watch.rtc.get_localtime()[0:5]] self.filep = "sleep_accel_data/" + "_".join(tod) + ".txt" - self._tracking = watch.rtc.get_time() # add data point every self.freq minutes + self._data_point_nb = 0 # tracks number of data_points so far + self._start_t = watch.rtc.get_time() self._add_accel_alar() self._draw() else: if self.btn_off.touch(event): - self._tracking = None + self._tracking = False + self.start_t = None wasp.system.cancel_alarm(self.next_al, self._trackOnce) self._periodicSave(force_save=True) self._draw() @@ -71,9 +74,10 @@ class SleepTApp(): def _trackOnce(self): """get one data point of accelerometer this function is called every self.freq seconds""" - if self._tracking is not None: + if self._tracking: acc = [str(x) for x in watch.accel.read_xyz()] - self.buff += str(int(watch.rtc.time())) + "," + ",".join(acc) + "\n" + self._data_point_nb += 1 + self.buff += str(self._data_point_nb) + "," + str(int(watch.rtc.time())) + "," + ",".join(acc) + "\n" self._add_accel_alar() self._periodicSave() @@ -90,17 +94,16 @@ class SleepTApp(): draw = wasp.watch.drawable draw.fill(0) draw.string("Sleep Tracker", 40, 0) - if self._tracking is None: + if self._tracking: + self.btn_off = widgets.Button(x=0, y=170, w=240, h=69, label="Off") + h = str(self._start_t[0]) + m = str(self._start_t[1]) + draw.string('Started at ' + h + ":" + m, 0, 70) + draw.string("data:" + str(self._data_point_nb), 0, 90) + draw.string("bat:" + str(watch.battery.level()) + "%", 0, 110) + self.btn_off.draw() + self.btn_on = None + else: self.btn_on = widgets.Button(x=0, y=170, w=240, h=69, label="On") self.btn_on.draw() self.btn_off = None - else: - self.btn_off = widgets.Button(x=0, y=170, w=240, h=69, label="Off") - h = str(self._tracking[0]) - m = str(self._tracking[1]) - draw.string('Started at', 50, 70) - draw.string(h + "h" + m + "m", 50, 90) - self.btn_off.draw() - self.btn_on = None - wasp.system.bar.clock = True - wasp.system.bar.battery = True From a77481641c95f011b8f035942ae5e169a19064e8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 12:26:17 +0100 Subject: [PATCH 050/485] style: better code to create a file --- SleepT.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/SleepT.py b/SleepT.py index e543e38..55a3930 100644 --- a/SleepT.py +++ b/SleepT.py @@ -55,12 +55,11 @@ class SleepTApp(): if self.btn_on.touch(event): self._tracking = True self.buff = "" # accel data not yet written to disk - # create one file for each run - tod = [str(x) for x in watch.rtc.get_localtime()[0:5]] - self.filep = "sleep_accel_data/" + "_".join(tod) + ".txt" - # add data point every self.freq minutes self._data_point_nb = 0 # tracks number of data_points so far self._start_t = watch.rtc.get_time() + + # create one file per recording session: + self.filep = "sleep_accel_data/" + "_".join(map(str, watch.rtc.get_localtime()[0:5])) + ".csv" self._add_accel_alar() self._draw() else: From ade3c7498b6bb5975b520133a0ea87a7d0f41a53 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 12:26:39 +0100 Subject: [PATCH 051/485] redraw the screen every press to make sure to display data --- SleepT.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/SleepT.py b/SleepT.py index 55a3930..a2957b9 100644 --- a/SleepT.py +++ b/SleepT.py @@ -61,14 +61,13 @@ class SleepTApp(): # create one file per recording session: self.filep = "sleep_accel_data/" + "_".join(map(str, watch.rtc.get_localtime()[0:5])) + ".csv" self._add_accel_alar() - self._draw() else: if self.btn_off.touch(event): self._tracking = False self.start_t = None wasp.system.cancel_alarm(self.next_al, self._trackOnce) self._periodicSave(force_save=True) - self._draw() + self._draw() def _trackOnce(self): """get one data point of accelerometer @@ -78,20 +77,23 @@ class SleepTApp(): self._data_point_nb += 1 self.buff += str(self._data_point_nb) + "," + str(int(watch.rtc.time())) + "," + ",".join(acc) + "\n" self._add_accel_alar() - self._periodicSave() + self._periodicSave(force_save=True) def _periodicSave(self, force_save=False): """save data to file only every few checks""" - if len(self.buff.split("\n")) > 20 or force_save: + if len(self.buff.split("\n")) > 5 or force_save: f = open(self.filep, "a") f.write(self.buff) self.buff = "" f.close() + wasp.gc.collect() def _draw(self): """GUI""" draw = wasp.watch.drawable draw.fill(0) + wasp.system.bar.clock = True + wasp.system.bar.battery = True draw.string("Sleep Tracker", 40, 0) if self._tracking: self.btn_off = widgets.Button(x=0, y=170, w=240, h=69, label="Off") From 3d98010c0cd6ded5ab8adabd97a6a0a14e1bada4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 12:27:09 +0100 Subject: [PATCH 052/485] dont implement background because it's useless --- SleepT.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/SleepT.py b/SleepT.py index a2957b9..f17f337 100644 --- a/SleepT.py +++ b/SleepT.py @@ -37,13 +37,6 @@ class SleepTApp(): self._draw() wasp.system.request_event(wasp.EventMask.TOUCH) - def background(self): - if self._tracking is not None: - f = open(self.filep, "a") - f.write(self.buff) - self.buff = "" - f.close() - def _add_accel_alar(self): """set an alarm, due in self.freq minutes, to log the accelerometer data once""" From 3caa7e45522c02ce2de243a3d331c53a4fa4f6b4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 12:56:28 +0100 Subject: [PATCH 053/485] new: store battery percent in data --- SleepT.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index f17f337..45e242f 100644 --- a/SleepT.py +++ b/SleepT.py @@ -68,7 +68,7 @@ class SleepTApp(): if self._tracking: acc = [str(x) for x in watch.accel.read_xyz()] self._data_point_nb += 1 - self.buff += str(self._data_point_nb) + "," + str(int(watch.rtc.time())) + "," + ",".join(acc) + "\n" + self.buff += str(self._data_point_nb) + "," + str(int(watch.rtc.time())) + "," + ",".join(acc) + "," + str(watch.battery.level()) + "\n" self._add_accel_alar() self._periodicSave(force_save=True) From c59fcb1dd2abf8ba2bf740ff4201d75e20ff859e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 12:57:00 +0100 Subject: [PATCH 054/485] UI: better, smaller, with battery and clock --- SleepT.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/SleepT.py b/SleepT.py index 45e242f..8ed5bf1 100644 --- a/SleepT.py +++ b/SleepT.py @@ -20,6 +20,7 @@ import time import watch import widgets import shell +import fonts class SleepTApp(): @@ -28,6 +29,7 @@ class SleepTApp(): def __init__(self): self.freq = 300 # poll accelerometer data every X seconds self._tracking = False # False = not tracking, True = currently tracking + self.font = fonts.sans18 try: shell.mkdir("sleep_accel_data") except: # file exists @@ -85,19 +87,21 @@ class SleepTApp(): """GUI""" draw = wasp.watch.drawable draw.fill(0) - wasp.system.bar.clock = True - wasp.system.bar.battery = True - draw.string("Sleep Tracker", 40, 0) + draw.set_font(self.font) if self._tracking: - self.btn_off = widgets.Button(x=0, y=170, w=240, h=69, label="Off") + self.btn_off = widgets.Button(x=0, y=170, w=240, h=69, label="Stop tracking") + self.btn_off.draw() h = str(self._start_t[0]) m = str(self._start_t[1]) draw.string('Started at ' + h + ":" + m, 0, 70) draw.string("data:" + str(self._data_point_nb), 0, 90) - draw.string("bat:" + str(watch.battery.level()) + "%", 0, 110) - self.btn_off.draw() self.btn_on = None else: - self.btn_on = widgets.Button(x=0, y=170, w=240, h=69, label="On") + draw.string('Track your sleep' , 0, 70) + self.btn_on = widgets.Button(x=0, y=170, w=240, h=69, label="Start tracking") self.btn_on.draw() self.btn_off = None + self.cl = widgets.Clock(True) + self.cl.draw() + bat = widgets.BatteryMeter() + bat.draw() From 2e36915a40a5196ff43cc4d610f22f842f84ea9a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 12:57:11 +0100 Subject: [PATCH 055/485] change name to smalle and understandable --- SleepT.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepT.py b/SleepT.py index 8ed5bf1..9b99a18 100644 --- a/SleepT.py +++ b/SleepT.py @@ -24,7 +24,7 @@ import fonts class SleepTApp(): - NAME = 'SleepT' + NAME = 'ZzzTrck' def __init__(self): self.freq = 300 # poll accelerometer data every X seconds From f218dfc86070ee842ac6e30316018058f3b19aee Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 12:58:27 +0100 Subject: [PATCH 056/485] changed name to ZzzTracker --- README.md | 8 ++++---- SleepT.py => ZzzTracker.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename SleepT.py => ZzzTracker.py (99%) diff --git a/README.md b/README.md index c91b9c2..398ca99 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ * Status: * Currently a sleep logger app, this will help run code on the simulator * **Instructions**: (with modified wasp-os that exposes accel data) - * get the latest python file : SleepT.py - * compile it : `./micropython/mpy-cross/mpy-cross -mno-unicode -march=armv7m SleepT.py` - * send compiled : `./tools/wasptool --verbose --upload SleepT.mpy --as apps/SleepT.mpy --binary` - * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('apps.SleepT.SleepTApp')` + * get the latest python file : ZzzTracker.py + * compile it : `./micropython/mpy-cross/mpy-cross -mno-unicode -march=armv7m ZzzTracker.py` + * send compiled : `./tools/wasptool --verbose --upload ZzzTracker.mpy --as apps/ZzzTracker.mpy --binary` + * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('apps.ZzzTracker.ZzzTracker')` * run it! ## Roadmap / Currently planned features: diff --git a/SleepT.py b/ZzzTracker.py similarity index 99% rename from SleepT.py rename to ZzzTracker.py index 9b99a18..0dd4323 100644 --- a/SleepT.py +++ b/ZzzTracker.py @@ -23,7 +23,7 @@ import shell import fonts -class SleepTApp(): +class ZzzTrackerApp(): NAME = 'ZzzTrck' def __init__(self): From 7fe90cd7d7f32e409a8592b897c55820128198e0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 13:07:17 +0100 Subject: [PATCH 057/485] fix: minor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 398ca99..42d4360 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ * get the latest python file : ZzzTracker.py * compile it : `./micropython/mpy-cross/mpy-cross -mno-unicode -march=armv7m ZzzTracker.py` * send compiled : `./tools/wasptool --verbose --upload ZzzTracker.mpy --as apps/ZzzTracker.mpy --binary` - * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('apps.ZzzTracker.ZzzTracker')` + * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('apps.ZzzTracker.ZzzTrackerApp')` * run it! ## Roadmap / Currently planned features: From 7df8f2687a967dcadce2b58674439eff233e8cad Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 13:07:28 +0100 Subject: [PATCH 058/485] new: store data in logs/sleep/ --- ZzzTracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 0dd4323..890b6de 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -31,7 +31,7 @@ class ZzzTrackerApp(): self._tracking = False # False = not tracking, True = currently tracking self.font = fonts.sans18 try: - shell.mkdir("sleep_accel_data") + shell.mkdir("logs/sleep/") except: # file exists pass @@ -54,7 +54,7 @@ class ZzzTrackerApp(): self._start_t = watch.rtc.get_time() # create one file per recording session: - self.filep = "sleep_accel_data/" + "_".join(map(str, watch.rtc.get_localtime()[0:5])) + ".csv" + self.filep = "logs/sleep/" + "_".join(map(str, watch.rtc.get_localtime()[0:5])) + ".csv" self._add_accel_alar() else: if self.btn_off.touch(event): From ae65171c71cb86e8c707bf1b77f383e49b98ae5c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 13:36:15 +0100 Subject: [PATCH 059/485] safer folder creation --- ZzzTracker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 890b6de..bf06c2d 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -31,8 +31,12 @@ class ZzzTrackerApp(): self._tracking = False # False = not tracking, True = currently tracking self.font = fonts.sans18 try: - shell.mkdir("logs/sleep/") - except: # file exists + shell.mkdir("logs/") + except: # folder already exists + pass + try: + shell.mkdir("logs/sleep") + except: # folder exists pass def foreground(self): From 80a0cd42a4a3c43edbac05e3c632cf02886097e8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 13:36:34 +0100 Subject: [PATCH 060/485] minor: move function down --- ZzzTracker.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index bf06c2d..1aa32c5 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -43,11 +43,6 @@ class ZzzTrackerApp(): self._draw() wasp.system.request_event(wasp.EventMask.TOUCH) - def _add_accel_alar(self): - """set an alarm, due in self.freq minutes, to log the accelerometer data - once""" - self.next_al = time.mktime(watch.rtc.get_localtime()) + self.freq - wasp.system.set_alarm(self.next_al, self._trackOnce) def touch(self, event): if self.btn_on: @@ -68,6 +63,12 @@ class ZzzTrackerApp(): self._periodicSave(force_save=True) self._draw() + def _add_accel_alar(self): + """set an alarm, due in self.freq minutes, to log the accelerometer data + once""" + self.next_al = time.mktime(watch.rtc.get_localtime()) + self.freq + wasp.system.set_alarm(self.next_al, self._trackOnce) + def _trackOnce(self): """get one data point of accelerometer this function is called every self.freq seconds""" From 293eb9dba70caf7cc93627f10bf6f482241e16a5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 13:36:42 +0100 Subject: [PATCH 061/485] implement self.sleep() --- ZzzTracker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ZzzTracker.py b/ZzzTracker.py index 1aa32c5..cb34c44 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -43,6 +43,9 @@ class ZzzTrackerApp(): self._draw() wasp.system.request_event(wasp.EventMask.TOUCH) + def sleep(self): + """keep running in the background""" + return False def touch(self, event): if self.btn_on: From 5e18e11c7198dc99bcdb98d4778937e8af548842 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 15:31:31 +0100 Subject: [PATCH 062/485] more robust mkdir --- ZzzTracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index cb34c44..2a0e145 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -19,7 +19,7 @@ import wasp import time import watch import widgets -import shell +from shell import mkdir import fonts @@ -31,11 +31,11 @@ class ZzzTrackerApp(): self._tracking = False # False = not tracking, True = currently tracking self.font = fonts.sans18 try: - shell.mkdir("logs/") + mkdir("logs/") except: # folder already exists pass try: - shell.mkdir("logs/sleep") + mkdir("logs/sleep") except: # folder exists pass From adc813825cbf7184c9b33dca49a128d3f4a850e2 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 15:31:46 +0100 Subject: [PATCH 063/485] better buff addition + compute angle --- ZzzTracker.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 2a0e145..83f2dd5 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -16,6 +16,7 @@ Trying to log my sleep data for a few days prior to working on the algorithm """ import wasp +from math import atan, pi import time import watch import widgets @@ -76,9 +77,19 @@ class ZzzTrackerApp(): """get one data point of accelerometer this function is called every self.freq seconds""" if self._tracking: - acc = [str(x) for x in watch.accel.read_xyz()] + acc = watch.accel.read_xyz() self._data_point_nb += 1 - self.buff += str(self._data_point_nb) + "," + str(int(watch.rtc.time())) + "," + ",".join(acc) + "," + str(watch.battery.level()) + "\n" + angle = atan(acc[2] / (acc[0]**2 + acc[1]**2 + 0.00001)) * 180 / pi + + val = [] + val.append(self._data_point_nb) + val.append(int(watch.rtc.time() - 1600000000)) + val.extend([x*180/pi for x in acc]) + val.append(angle) + val.append(watch.battery.level()) + print(val) + + self.buff += ",".join([str(x) for x in val]) + "\n" self._add_accel_alar() self._periodicSave(force_save=True) From f828c365a6662fea5f3ca8cf8b41ff29952cda97 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 15:49:56 +0100 Subject: [PATCH 064/485] minor --- ZzzTracker.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 83f2dd5..e4d54df 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -16,12 +16,16 @@ Trying to log my sleep data for a few days prior to working on the algorithm """ import wasp -from math import atan, pi +from math import atan, pi, pow import time import watch import widgets from shell import mkdir import fonts +from micropython import const + +_RAD = 180/pi +_OFFSET = const(1600000000) class ZzzTrackerApp(): @@ -75,21 +79,23 @@ class ZzzTrackerApp(): def _trackOnce(self): """get one data point of accelerometer - this function is called every self.freq seconds""" + this function is called every self.freq seconds + I kept only the first 5 digits of some values to save space""" if self._tracking: acc = watch.accel.read_xyz() self._data_point_nb += 1 - angle = atan(acc[2] / (acc[0]**2 + acc[1]**2 + 0.00001)) * 180 / pi + # formula from https://www.nature.com/articles/s41598-018-31266-z + angle = atan(acc[2] / (pow(acc[0], 2) + pow(acc[1], 2) + 0.0000001)) * _RAD val = [] - val.append(self._data_point_nb) - val.append(int(watch.rtc.time() - 1600000000)) - val.extend([x*180/pi for x in acc]) - val.append(angle) - val.append(watch.battery.level()) - print(val) + val.append(str(self._data_point_nb)) + val.append(str(int(watch.rtc.time() - _OFFSET))) # more compact + val.extend([str(x * _RAD)[0:5] for x in acc]) + val.append(str(angle)[0:5]) + val.append(str(watch.battery.level())) + #print(val) - self.buff += ",".join([str(x) for x in val]) + "\n" + self.buff += ",".join(val) + "\n" self._add_accel_alar() self._periodicSave(force_save=True) From 950187ab9f2c7fff7482feaf44378f20045efee9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 18:11:51 +0100 Subject: [PATCH 065/485] write file size --- ZzzTracker.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ZzzTracker.py b/ZzzTracker.py index e4d54df..640d01d 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -15,6 +15,7 @@ Current state: Trying to log my sleep data for a few days prior to working on the algorithm """ +from os import stat import wasp from math import atan, pi, pow import time @@ -120,6 +121,10 @@ class ZzzTrackerApp(): m = str(self._start_t[1]) draw.string('Started at ' + h + ":" + m, 0, 70) draw.string("data:" + str(self._data_point_nb), 0, 90) + try: + draw.string("size:" + str(stat(self.filep)[6]), 0, 110) + except: + pass self.btn_on = None else: draw.string('Track your sleep' , 0, 70) From cc7c6d712d03bcedbb5a66caa1ac6fe4b5487072 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 27 Jan 2022 18:15:51 +0100 Subject: [PATCH 066/485] log less data --- ZzzTracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 640d01d..5b03ad3 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -89,10 +89,10 @@ class ZzzTrackerApp(): angle = atan(acc[2] / (pow(acc[0], 2) + pow(acc[1], 2) + 0.0000001)) * _RAD val = [] - val.append(str(self._data_point_nb)) + #val.append(str(self._data_point_nb)) val.append(str(int(watch.rtc.time() - _OFFSET))) # more compact - val.extend([str(x * _RAD)[0:5] for x in acc]) - val.append(str(angle)[0:5]) + #val.extend([str(x * _RAD)[0:5] for x in acc]) + val.append(str(angle)[0:6]) val.append(str(watch.battery.level())) #print(val) From 0ce0a79149151ac243c08373ce525641068dd972 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 28 Jan 2022 14:19:43 +0100 Subject: [PATCH 067/485] new: store file with timestamp as name, to make the output more compact --- ZzzTracker.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 5b03ad3..22e601e 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -26,7 +26,6 @@ import fonts from micropython import const _RAD = 180/pi -_OFFSET = const(1600000000) class ZzzTrackerApp(): @@ -36,6 +35,7 @@ class ZzzTrackerApp(): self.freq = 300 # poll accelerometer data every X seconds self._tracking = False # False = not tracking, True = currently tracking self.font = fonts.sans18 + self.offset = None try: mkdir("logs/") except: # folder already exists @@ -60,12 +60,14 @@ class ZzzTrackerApp(): self.buff = "" # accel data not yet written to disk self._data_point_nb = 0 # tracks number of data_points so far self._start_t = watch.rtc.get_time() + self.offset = int(watch.rtc.time()) # create one file per recording session: - self.filep = "logs/sleep/" + "_".join(map(str, watch.rtc.get_localtime()[0:5])) + ".csv" + self.filep = "logs/sleep/" + str(self.offset)) + ".csv" self._add_accel_alar() else: if self.btn_off.touch(event): + self.offset = None self._tracking = False self.start_t = None wasp.system.cancel_alarm(self.next_al, self._trackOnce) @@ -90,7 +92,7 @@ class ZzzTrackerApp(): val = [] #val.append(str(self._data_point_nb)) - val.append(str(int(watch.rtc.time() - _OFFSET))) # more compact + val.append(str(int(watch.rtc.time() - self.offset))) # more compact #val.extend([str(x * _RAD)[0:5] for x in acc]) val.append(str(angle)[0:6]) val.append(str(watch.battery.level())) From 614abf3d4399133711cb1267c639fa064f6322a9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 28 Jan 2022 15:33:52 +0100 Subject: [PATCH 068/485] refactor: much improved, now stores the averaged value of some time --- ZzzTracker.py | 92 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 22e601e..51fbe9a 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -17,33 +17,38 @@ Trying to log my sleep data for a few days prior to working on the algorithm from os import stat import wasp -from math import atan, pi, pow +from math import atan, pow, degrees import time import watch import widgets -from shell import mkdir +from shell import mkdir, cd import fonts from micropython import const -_RAD = 180/pi +# SETTINGS: +_POLLFREQ = const(15) # poll accelerometer data every X seconds, they will be averaged +_WIN_L = const(300) # number of seconds between writing average accel values +# not settings: +_RATIO = const(_WIN_L / _POLLFREQ) # save data every X data points +_FONT = fonts.sans18 + class ZzzTrackerApp(): NAME = 'ZzzTrck' def __init__(self): - self.freq = 300 # poll accelerometer data every X seconds self._tracking = False # False = not tracking, True = currently tracking - self.font = fonts.sans18 - self.offset = None try: mkdir("logs/") except: # folder already exists pass + cd("logs") try: - mkdir("logs/sleep") - except: # folder exists + mkdir("sleep") + except: # folder already exists pass + cd("..") def foreground(self): self._draw() @@ -54,68 +59,83 @@ class ZzzTrackerApp(): return False def touch(self, event): + """either start trackign or disable it, draw the screen in all cases""" if self.btn_on: if self.btn_on.touch(event): self._tracking = True - self.buff = "" # accel data not yet written to disk - self._data_point_nb = 0 # tracks number of data_points so far - self._start_t = watch.rtc.get_time() - self.offset = int(watch.rtc.time()) + # accel data not yet written to disk: + self.buff_x = 0 + self.buff_y = 0 + self.buff_z = 0 + self._data_point_nb = 0 # total number of data points so far + self._last_checkpoint = 0 # to know when to save to file + self._start_t = watch.rtc.get_time() # to display when recording started on screen + self.offset = const(int(watch.rtc.time())) # makes output more compact # create one file per recording session: - self.filep = "logs/sleep/" + str(self.offset)) + ".csv" + self.filep = "logs/sleep/" + str(self.offset) + ".csv" self._add_accel_alar() else: if self.btn_off.touch(event): - self.offset = None self._tracking = False self.start_t = None wasp.system.cancel_alarm(self.next_al, self._trackOnce) - self._periodicSave(force_save=True) + self._periodicSave() + self.offset = None + self._last_checkpoint = 0 self._draw() def _add_accel_alar(self): - """set an alarm, due in self.freq minutes, to log the accelerometer data + """set an alarm, due in _POLLFREQ minutes, to log the accelerometer data once""" - self.next_al = time.mktime(watch.rtc.get_localtime()) + self.freq + self.next_al = time.mktime(watch.rtc.get_localtime()) + _POLLFREQ wasp.system.set_alarm(self.next_al, self._trackOnce) def _trackOnce(self): - """get one data point of accelerometer - this function is called every self.freq seconds - I kept only the first 5 digits of some values to save space""" + """get one data point of accelerometer every _POLLFREQ seconds and + they are then averaged and stored every _WIN_L seconds""" if self._tracking: acc = watch.accel.read_xyz() + self.buff_x += acc[0] + self.buff_y += acc[1] + self.buff_z += acc[2] self._data_point_nb += 1 + self._add_accel_alar() + self._periodicSave() + + def _periodicSave(self): + """save data after averageing over a window to file""" + n = self._data_point_nb - self._last_checkpoint + if n >= _RATIO: + x_avg = self.buff_x / n + y_avg = self.buff_y / n + z_avg = self.buff_z / n + self.buff_x = 0 + self.buff_y = 0 + self.buff_z = 0 + # formula from https://www.nature.com/articles/s41598-018-31266-z - angle = atan(acc[2] / (pow(acc[0], 2) + pow(acc[1], 2) + 0.0000001)) * _RAD + angl_avg = degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001))) val = [] - #val.append(str(self._data_point_nb)) - val.append(str(int(watch.rtc.time() - self.offset))) # more compact - #val.extend([str(x * _RAD)[0:5] for x in acc]) - val.append(str(angle)[0:6]) + val.append(str(int(watch.rtc.time() - self.offset))) + val.append(str(x_avg)[0:6]) + val.append(str(y_avg)[0:6]) + val.append(str(z_avg)[0:6]) + val.append(str(angl_avg)[0:6]) val.append(str(watch.battery.level())) - #print(val) - self.buff += ",".join(val) + "\n" - self._add_accel_alar() - self._periodicSave(force_save=True) - - def _periodicSave(self, force_save=False): - """save data to file only every few checks""" - if len(self.buff.split("\n")) > 5 or force_save: f = open(self.filep, "a") - f.write(self.buff) - self.buff = "" + f.write(",".join(val) + "\n") f.close() + self._last_checkpoint = self._data_point_nb wasp.gc.collect() def _draw(self): """GUI""" draw = wasp.watch.drawable draw.fill(0) - draw.set_font(self.font) + draw.set_font(_FONT) if self._tracking: self.btn_off = widgets.Button(x=0, y=170, w=240, h=69, label="Stop tracking") self.btn_off.draw() From a4d15f3da5062af40358bc5652c76c2edfcb0ce7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 28 Jan 2022 15:39:01 +0100 Subject: [PATCH 069/485] fix: _RATIO has to be an integer --- ZzzTracker.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 51fbe9a..9340ad2 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -25,13 +25,10 @@ from shell import mkdir, cd import fonts from micropython import const -# SETTINGS: + _POLLFREQ = const(15) # poll accelerometer data every X seconds, they will be averaged _WIN_L = const(300) # number of seconds between writing average accel values - - -# not settings: -_RATIO = const(_WIN_L / _POLLFREQ) # save data every X data points +_RATIO = const(20) # must be _WIN_L / _POLLFREQ, means that data will be written every X data points _FONT = fonts.sans18 class ZzzTrackerApp(): From 7de3b22d2bfcc71ab83badf0dd001101fa6a328a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 28 Jan 2022 15:55:26 +0100 Subject: [PATCH 070/485] change import statements --- ZzzTracker.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 9340ad2..26e05cd 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -16,20 +16,20 @@ Trying to log my sleep data for a few days prior to working on the algorithm """ from os import stat -import wasp +from wasp import watch, system, EventMask, gc from math import atan, pow, degrees -import time -import watch -import widgets +from time import mktime +from watch import rtc, battery, accel +from widgets import Clock, BatteryMeter, Button from shell import mkdir, cd -import fonts +from fonts import sans18 from micropython import const _POLLFREQ = const(15) # poll accelerometer data every X seconds, they will be averaged _WIN_L = const(300) # number of seconds between writing average accel values _RATIO = const(20) # must be _WIN_L / _POLLFREQ, means that data will be written every X data points -_FONT = fonts.sans18 +_FONT = sans18 class ZzzTrackerApp(): NAME = 'ZzzTrck' @@ -49,7 +49,7 @@ class ZzzTrackerApp(): def foreground(self): self._draw() - wasp.system.request_event(wasp.EventMask.TOUCH) + system.request_event(EventMask.TOUCH) def sleep(self): """keep running in the background""" @@ -66,8 +66,8 @@ class ZzzTrackerApp(): self.buff_z = 0 self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file - self._start_t = watch.rtc.get_time() # to display when recording started on screen - self.offset = const(int(watch.rtc.time())) # makes output more compact + self._start_t = rtc.get_time() # to display when recording started on screen + self.offset = const(int(rtc.time())) # makes output more compact # create one file per recording session: self.filep = "logs/sleep/" + str(self.offset) + ".csv" @@ -76,7 +76,7 @@ class ZzzTrackerApp(): if self.btn_off.touch(event): self._tracking = False self.start_t = None - wasp.system.cancel_alarm(self.next_al, self._trackOnce) + system.cancel_alarm(self.next_al, self._trackOnce) self._periodicSave() self.offset = None self._last_checkpoint = 0 @@ -85,14 +85,14 @@ class ZzzTrackerApp(): def _add_accel_alar(self): """set an alarm, due in _POLLFREQ minutes, to log the accelerometer data once""" - self.next_al = time.mktime(watch.rtc.get_localtime()) + _POLLFREQ - wasp.system.set_alarm(self.next_al, self._trackOnce) + self.next_al = mktime(rtc.get_localtime()) + _POLLFREQ + system.set_alarm(self.next_al, self._trackOnce) def _trackOnce(self): """get one data point of accelerometer every _POLLFREQ seconds and they are then averaged and stored every _WIN_L seconds""" if self._tracking: - acc = watch.accel.read_xyz() + acc = accel.read_xyz() self.buff_x += acc[0] self.buff_y += acc[1] self.buff_z += acc[2] @@ -115,26 +115,26 @@ class ZzzTrackerApp(): angl_avg = degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001))) val = [] - val.append(str(int(watch.rtc.time() - self.offset))) + val.append(str(int(rtc.time() - self.offset))) val.append(str(x_avg)[0:6]) val.append(str(y_avg)[0:6]) val.append(str(z_avg)[0:6]) val.append(str(angl_avg)[0:6]) - val.append(str(watch.battery.level())) + val.append(str(battery.level())) f = open(self.filep, "a") f.write(",".join(val) + "\n") f.close() self._last_checkpoint = self._data_point_nb - wasp.gc.collect() + gc.collect() def _draw(self): """GUI""" - draw = wasp.watch.drawable + draw = watch.drawable draw.fill(0) draw.set_font(_FONT) if self._tracking: - self.btn_off = widgets.Button(x=0, y=170, w=240, h=69, label="Stop tracking") + self.btn_off = Button(x=0, y=170, w=240, h=69, label="Stop tracking") self.btn_off.draw() h = str(self._start_t[0]) m = str(self._start_t[1]) @@ -147,10 +147,10 @@ class ZzzTrackerApp(): self.btn_on = None else: draw.string('Track your sleep' , 0, 70) - self.btn_on = widgets.Button(x=0, y=170, w=240, h=69, label="Start tracking") + self.btn_on = Button(x=0, y=170, w=240, h=69, label="Start tracking") self.btn_on.draw() self.btn_off = None - self.cl = widgets.Clock(True) + self.cl = Clock(True) self.cl.draw() - bat = widgets.BatteryMeter() + bat = BatteryMeter() bat.draw() From 29006d1e717f63ac4001e7d5dcee348542eff295 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 28 Jan 2022 18:01:51 +0100 Subject: [PATCH 071/485] refactor: much improved, implement alarm but without optimization --- ZzzTracker.py | 156 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 124 insertions(+), 32 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 26e05cd..4f61df6 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -17,18 +17,27 @@ Trying to log my sleep data for a few days prior to working on the algorithm from os import stat from wasp import watch, system, EventMask, gc -from math import atan, pow, degrees from time import mktime + from watch import rtc, battery, accel from widgets import Clock, BatteryMeter, Button from shell import mkdir, cd from fonts import sans18 + +from math import atan, pow, degrees, sqrt from micropython import const +from array import array -_POLLFREQ = const(15) # poll accelerometer data every X seconds, they will be averaged +_POLLFREQ = const(10) # poll accelerometer data every X seconds, they will be averaged _WIN_L = const(300) # number of seconds between writing average accel values -_RATIO = const(20) # must be _WIN_L / _POLLFREQ, means that data will be written every X data points +_RATIO = const(30) # must be _WIN_L / _POLLFREQ, means that data will be written every X data points + +_WU_ON = False # True to activate wake up alarm, False to disable +_WU_LAT = const(28800) # maximum seconds of sleep before waking you up, default 28800 = 8h, will compute best wake up time from _WU_LAT - _WU_ANTICIP seconds +_WU_ANT_ON = False +_WU_ANTICIP = const(1800) # default 1800 = 30 minutes + _FONT = sans18 class ZzzTrackerApp(): @@ -36,6 +45,7 @@ class ZzzTrackerApp(): def __init__(self): self._tracking = False # False = not tracking, True = currently tracking + self._WakingUp = False # when True, watch is currently vibrating to wake you up try: mkdir("logs/") except: # folder already exists @@ -61,31 +71,53 @@ class ZzzTrackerApp(): if self.btn_on.touch(event): self._tracking = True # accel data not yet written to disk: - self.buff_x = 0 - self.buff_y = 0 - self.buff_z = 0 + self._buff_x = 0 + self._buff_y = 0 + self._buff_z = 0 self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file - self._start_t = rtc.get_time() # to display when recording started on screen - self.offset = const(int(rtc.time())) # makes output more compact + self._offset = int(rtc.time()) # makes output more compact # create one file per recording session: - self.filep = "logs/sleep/" + str(self.offset) + ".csv" + self.filep = "logs/sleep/" + str(self._offset) + ".csv" self._add_accel_alar() - else: + + # alarm in _WU_LAT seconds after tracking started to wake you up + if _WU_ON: + self._WU_t = self._offset + _WU_LAT + system.set_alarm(self._WU_t, self._listen_to_ticks) + + # alarm in _WU_ANTICIP less seconds to compute best wake up time + if _WU_ANT_ON: + self._WU_a = self._WU_t - _WU_ANTICIP + system.set_alarm(self._WU_a, self._compute_best_WU) + elif self.btn_off: if self.btn_off.touch(event): - self._tracking = False - self.start_t = None - system.cancel_alarm(self.next_al, self._trackOnce) - self._periodicSave() - self.offset = None - self._last_checkpoint = 0 + self._disable_tracking() + elif self.btn_al: + if self.btn_al.touch(event): + self._WakingUp = False + self._disable_tracking() self._draw() + def _disable_tracking(self): + """called by touching "STOP TRACKING" or when computing best alarm time + to wake up you + disables tracking features and alarms""" + self._tracking = False + system.cancel_alarm(self.next_al, self._trackOnce) + if _WU_ON: + system.cancel_alarm(self._WU_t, self._listen_to_ticks) + if _WU_ANT_ON: + system.cancel_alarm(self._WU_a, self._compute_best_WU) + self._periodicSave() + self._offset = None + self._last_checkpoint = 0 + def _add_accel_alar(self): """set an alarm, due in _POLLFREQ minutes, to log the accelerometer data once""" - self.next_al = mktime(rtc.get_localtime()) + _POLLFREQ + self.next_al = watch.rtc.time() + _POLLFREQ system.set_alarm(self.next_al, self._trackOnce) def _trackOnce(self): @@ -93,9 +125,10 @@ class ZzzTrackerApp(): they are then averaged and stored every _WIN_L seconds""" if self._tracking: acc = accel.read_xyz() - self.buff_x += acc[0] - self.buff_y += acc[1] - self.buff_z += acc[2] + self._buff_x += acc[0] + self._buff_y += acc[1] + self._buff_z += acc[2] + del acc self._data_point_nb += 1 self._add_accel_alar() self._periodicSave() @@ -104,18 +137,18 @@ class ZzzTrackerApp(): """save data after averageing over a window to file""" n = self._data_point_nb - self._last_checkpoint if n >= _RATIO: - x_avg = self.buff_x / n - y_avg = self.buff_y / n - z_avg = self.buff_z / n - self.buff_x = 0 - self.buff_y = 0 - self.buff_z = 0 + x_avg = self._buff_x / n + y_avg = self._buff_y / n + z_avg = self._buff_z / n + self._buff_x = 0 + self._buff_y = 0 + self._buff_z = 0 # formula from https://www.nature.com/articles/s41598-018-31266-z angl_avg = degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001))) val = [] - val.append(str(int(rtc.time() - self.offset))) + val.append(str(int(rtc.time() - self._offset))) val.append(str(x_avg)[0:6]) val.append(str(y_avg)[0:6]) val.append(str(z_avg)[0:6]) @@ -125,32 +158,91 @@ class ZzzTrackerApp(): f = open(self.filep, "a") f.write(",".join(val) + "\n") f.close() + self._last_checkpoint = self._data_point_nb - gc.collect() + del x_avg, y_avg, z_avg, angl_avg, n, val + gc.collect() def _draw(self): """GUI""" draw = watch.drawable draw.fill(0) draw.set_font(_FONT) - if self._tracking: + if self._WakingUp: + self.btn_al = Button(x=0, y=170, w=240, h=69, label="STOP") + self.btn_al.draw() + self.btn_on = None + self.btn_off = None + elif self._tracking: self.btn_off = Button(x=0, y=170, w=240, h=69, label="Stop tracking") self.btn_off.draw() - h = str(self._start_t[0]) - m = str(self._start_t[1]) + h = str(watch.time.localtime(self._offset)[3]) + m = str(watch.time.localtime(self._offset)[4]) draw.string('Started at ' + h + ":" + m, 0, 70) draw.string("data:" + str(self._data_point_nb), 0, 90) try: draw.string("size:" + str(stat(self.filep)[6]), 0, 110) except: pass + if _WU_ON: + h = str(watch.time.localtime(self._offset + _WU_LAT)[3]) + m = str(watch.time.localtime(self._offset + _WU_LAT)[4]) + if _WU_ANT_ON: + word = " bef. " + else: + word = " at " + draw.string("Wake up" + word + h + ":" + m, 0, 130) self.btn_on = None + self.btn_al = None else: - draw.string('Track your sleep' , 0, 70) + draw.string('Sleep tracker' , 0, 70) self.btn_on = Button(x=0, y=170, w=240, h=69, label="Start tracking") self.btn_on.draw() self.btn_off = None + self.btn_al = None self.cl = Clock(True) self.cl.draw() bat = BatteryMeter() bat.draw() + + def _compute_best_WU(self): + """computes best wake up time from sleep data""" + return True # disabled for now + # stop tracking to save memory + self._disable_tracking() + gc.collect() + + # get angle over time + data = array("f") + f = open(self.filep, "r") + data.extend([float(line.split(",")[4]) for line in f.readlines()]) + f.close() + del f + + # center and scale + data2 = array("f") + data2.extend([x**2 for x in data]) + mean = sum(data) / len(data) + std = sqrt((sum(data2) / len(data2)) - pow(mean, 2)) + del data2 + for i in range(len(data)): + data[i] = (data[i] - mean) / std + + # find most appropriate cosine + # TODO + + gc.collect() + + def _listen_to_ticks(self): + """listen to ticks every second, telling the watch to vibrate""" + self._WakingUp = True + system.wake() + system.switch(self) + self._draw() + system.request_tick(1000) + + def tick(self, ticks): + """vibrate to wake you up""" + if self._WakingUp: + watch.vibrator.pulse(duty=50, ms=500) + system.keep_awake() From d81dd749d5b5fab08c4dc8261389a678c3e3a9f1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 11:53:02 +0100 Subject: [PATCH 072/485] use a single buffer instead of one for each axis --- ZzzTracker.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 4f61df6..10f171e 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -71,9 +71,7 @@ class ZzzTrackerApp(): if self.btn_on.touch(event): self._tracking = True # accel data not yet written to disk: - self._buff_x = 0 - self._buff_y = 0 - self._buff_z = 0 + self._buff = [] self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file self._offset = int(rtc.time()) # makes output more compact @@ -124,11 +122,7 @@ class ZzzTrackerApp(): """get one data point of accelerometer every _POLLFREQ seconds and they are then averaged and stored every _WIN_L seconds""" if self._tracking: - acc = accel.read_xyz() - self._buff_x += acc[0] - self._buff_y += acc[1] - self._buff_z += acc[2] - del acc + self._buff.append(accel.read_xyz()) self._data_point_nb += 1 self._add_accel_alar() self._periodicSave() @@ -137,12 +131,10 @@ class ZzzTrackerApp(): """save data after averageing over a window to file""" n = self._data_point_nb - self._last_checkpoint if n >= _RATIO: - x_avg = self._buff_x / n - y_avg = self._buff_y / n - z_avg = self._buff_z / n - self._buff_x = 0 - self._buff_y = 0 - self._buff_z = 0 + x_avg = sum([x[0] for x in self._buff]) / n + y_avg = sum([x[1] for x in self._buff]) / n + z_avg = sum([x[2] for x in self._buff]) / n + self._buff = [] # formula from https://www.nature.com/articles/s41598-018-31266-z angl_avg = degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001))) From 4cc4aff636a75d21f093e274207a68cc6f676fcd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 11:53:40 +0100 Subject: [PATCH 073/485] slightly more efficient code --- ZzzTracker.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 10f171e..576fe80 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -33,9 +33,9 @@ _POLLFREQ = const(10) # poll accelerometer data every X seconds, they will be a _WIN_L = const(300) # number of seconds between writing average accel values _RATIO = const(30) # must be _WIN_L / _POLLFREQ, means that data will be written every X data points -_WU_ON = False # True to activate wake up alarm, False to disable +_WU_ON = const(0) # const(1) to activate wake up alarm, const(0) to disable _WU_LAT = const(28800) # maximum seconds of sleep before waking you up, default 28800 = 8h, will compute best wake up time from _WU_LAT - _WU_ANTICIP seconds -_WU_ANT_ON = False +_WU_ANT_ON = const(0) _WU_ANTICIP = const(1800) # default 1800 = 30 minutes _FONT = sans18 @@ -139,16 +139,16 @@ class ZzzTrackerApp(): # formula from https://www.nature.com/articles/s41598-018-31266-z angl_avg = degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001))) - val = [] - val.append(str(int(rtc.time() - self._offset))) - val.append(str(x_avg)[0:6]) - val.append(str(y_avg)[0:6]) - val.append(str(z_avg)[0:6]) - val.append(str(angl_avg)[0:6]) - val.append(str(battery.level())) + val = array("f") + val.append(int(rtc.time() - self._offset)) + val.append(x_avg) + val.append(y_avg) + val.append(z_avg) + val.append(angl_avg) + val.append(battery.level()) f = open(self.filep, "a") - f.write(",".join(val) + "\n") + f.write(",".join([str(x)[0:8] for x in val]) + "\n") f.close() self._last_checkpoint = self._data_point_nb @@ -168,22 +168,18 @@ class ZzzTrackerApp(): elif self._tracking: self.btn_off = Button(x=0, y=170, w=240, h=69, label="Stop tracking") self.btn_off.draw() - h = str(watch.time.localtime(self._offset)[3]) - m = str(watch.time.localtime(self._offset)[4]) - draw.string('Started at ' + h + ":" + m, 0, 70) + draw.string('Started at ' + str(watch.time.localtime(self._offset)[3]) + ":" + str(watch.time.localtime(self._offset)[4]) , 0, 70) draw.string("data:" + str(self._data_point_nb), 0, 90) try: draw.string("size:" + str(stat(self.filep)[6]), 0, 110) except: pass if _WU_ON: - h = str(watch.time.localtime(self._offset + _WU_LAT)[3]) - m = str(watch.time.localtime(self._offset + _WU_LAT)[4]) if _WU_ANT_ON: word = " bef. " else: word = " at " - draw.string("Wake up" + word + h + ":" + m, 0, 130) + draw.string("Wake up" + word + str(watch.time.localtime(self._offset + _WU_LAT)[3]) + ":" + str(watch.time.localtime(self._offset + _WU_LAT)[4]), 0, 130) self.btn_on = None self.btn_al = None else: @@ -212,9 +208,8 @@ class ZzzTrackerApp(): del f # center and scale - data2 = array("f") - data2.extend([x**2 for x in data]) mean = sum(data) / len(data) + data2 = array("f", [x**2 for x in data]) std = sqrt((sum(data2) / len(data2)) - pow(mean, 2)) del data2 for i in range(len(data)): From a0901c4c41c0c61939752f4849795867a5fe0739 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 11:55:57 +0100 Subject: [PATCH 074/485] use arrays for buffer --- ZzzTracker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 576fe80..4b2ba18 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -71,7 +71,7 @@ class ZzzTrackerApp(): if self.btn_on.touch(event): self._tracking = True # accel data not yet written to disk: - self._buff = [] + self._buff = array("l") self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file self._offset = int(rtc.time()) # makes output more compact @@ -122,7 +122,7 @@ class ZzzTrackerApp(): """get one data point of accelerometer every _POLLFREQ seconds and they are then averaged and stored every _WIN_L seconds""" if self._tracking: - self._buff.append(accel.read_xyz()) + self._buff.extend(accel.read_xyz()) self._data_point_nb += 1 self._add_accel_alar() self._periodicSave() @@ -131,10 +131,10 @@ class ZzzTrackerApp(): """save data after averageing over a window to file""" n = self._data_point_nb - self._last_checkpoint if n >= _RATIO: - x_avg = sum([x[0] for x in self._buff]) / n - y_avg = sum([x[1] for x in self._buff]) / n - z_avg = sum([x[2] for x in self._buff]) / n - self._buff = [] + x_avg = sum(self._buff[0::3]) / n + y_avg = sum(self._buff[1::3]) / n + z_avg = sum(self._buff[2::3]) / n + del self._buff[:] # formula from https://www.nature.com/articles/s41598-018-31266-z angl_avg = degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001))) From 6b8a296b6a2cc4562de396b13962ad0e08e06066 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 12:13:12 +0100 Subject: [PATCH 075/485] fix: race condition --- ZzzTracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 4b2ba18..d870f5f 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -91,7 +91,8 @@ class ZzzTrackerApp(): system.set_alarm(self._WU_a, self._compute_best_WU) elif self.btn_off: if self.btn_off.touch(event): - self._disable_tracking() + if self._tracking: + self._disable_tracking() elif self.btn_al: if self.btn_al.touch(event): self._WakingUp = False From a8c0c4fb631d8c7ceb7fc4115b8de15a4a3e9df8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 12:13:31 +0100 Subject: [PATCH 076/485] fix: smaller font --- ZzzTracker.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index d870f5f..953e780 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -162,14 +162,12 @@ class ZzzTrackerApp(): draw.fill(0) draw.set_font(_FONT) if self._WakingUp: - self.btn_al = Button(x=0, y=170, w=240, h=69, label="STOP") - self.btn_al.draw() self.btn_on = None self.btn_off = None + self.btn_al = Button(x=0, y=170, w=240, h=69, label="STOP") + self.btn_al.draw() elif self._tracking: - self.btn_off = Button(x=0, y=170, w=240, h=69, label="Stop tracking") - self.btn_off.draw() - draw.string('Started at ' + str(watch.time.localtime(self._offset)[3]) + ":" + str(watch.time.localtime(self._offset)[4]) , 0, 70) + draw.string('Started at ' + ":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]]), 0, 70) draw.string("data:" + str(self._data_point_nb), 0, 90) try: draw.string("size:" + str(stat(self.filep)[6]), 0, 110) @@ -177,18 +175,20 @@ class ZzzTrackerApp(): pass if _WU_ON: if _WU_ANT_ON: - word = " bef. " + word = " before " else: word = " at " - draw.string("Wake up" + word + str(watch.time.localtime(self._offset + _WU_LAT)[3]) + ":" + str(watch.time.localtime(self._offset + _WU_LAT)[4]), 0, 130) + draw.string("Wake up" + word + ":".join([str(x) for x in watch.time.localtime(self._offset + _WU_LAT)[3:5]]), 0, 130) self.btn_on = None self.btn_al = None + self.btn_off = Button(x=0, y=170, w=240, h=69, label="Stop tracking") + self.btn_off.draw() else: draw.string('Sleep tracker' , 0, 70) - self.btn_on = Button(x=0, y=170, w=240, h=69, label="Start tracking") - self.btn_on.draw() self.btn_off = None self.btn_al = None + self.btn_on = Button(x=0, y=170, w=240, h=69, label="Start tracking") + self.btn_on.draw() self.cl = Clock(True) self.cl.draw() bat = BatteryMeter() From 0524c0eac4741f963fff11c04680d01ce234cac0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 12:13:44 +0100 Subject: [PATCH 077/485] testing --- ZzzTracker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ZzzTracker.py b/ZzzTracker.py index 953e780..2d7a0b4 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -218,6 +218,8 @@ class ZzzTrackerApp(): # find most appropriate cosine # TODO + best_offset = 0 + system.set_alarm(self._WU_t + best_offset, self._listen_to_ticks) gc.collect() From 66f52e96f97c5a2ece9778de7afca7602d9e05ca Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 12:13:56 +0100 Subject: [PATCH 078/485] turn on app when motor is activated --- ZzzTracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 2d7a0b4..954aea8 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -234,5 +234,6 @@ class ZzzTrackerApp(): def tick(self, ticks): """vibrate to wake you up""" if self._WakingUp: - watch.vibrator.pulse(duty=50, ms=500) + system.switch(self) system.keep_awake() + watch.vibrator.pulse(duty=50, ms=500) From 23a9c9fd255bc6b48b38df225df7ecb3eb54b835 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 12:42:45 +0100 Subject: [PATCH 079/485] fix: race condition --- ZzzTracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 954aea8..f7756ea 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -96,7 +96,8 @@ class ZzzTrackerApp(): elif self.btn_al: if self.btn_al.touch(event): self._WakingUp = False - self._disable_tracking() + if self._tracking: + self._disable_tracking() self._draw() def _disable_tracking(self): From 5e38f5eec3211dbf828141929cc13a2033834f88 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 12:43:00 +0100 Subject: [PATCH 080/485] style --- ZzzTracker.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index f7756ea..9d0499c 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -165,13 +165,14 @@ class ZzzTrackerApp(): if self._WakingUp: self.btn_on = None self.btn_off = None + draw.string('WAKE UP', 0, 70) self.btn_al = Button(x=0, y=170, w=240, h=69, label="STOP") self.btn_al.draw() elif self._tracking: draw.string('Started at ' + ":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]]), 0, 70) - draw.string("data:" + str(self._data_point_nb), 0, 90) + draw.string("data points:" + str(self._data_point_nb), 0, 90) try: - draw.string("size:" + str(stat(self.filep)[6]), 0, 110) + draw.string("file size:" + str(stat(self.filep)[6]), 0, 110) except: pass if _WU_ON: From 00a3284cf55301ee3fe804b976383ed37ecd53c6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 12:43:27 +0100 Subject: [PATCH 081/485] minor --- ZzzTracker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 9d0499c..651c06b 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -204,11 +204,12 @@ class ZzzTrackerApp(): gc.collect() # get angle over time - data = array("f") f = open(self.filep, "r") - data.extend([float(line.split(",")[4]) for line in f.readlines()]) + lines = f.readlines() f.close() - del f + if len(lines) == 1: + lines = lines[0].split("\n") + data = array("f", [float(line.split(",")[4]) for line in lines]) # center and scale mean = sum(data) / len(data) From 5929648993e3337746722f90ff8bec86fecaa135 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 12:45:14 +0100 Subject: [PATCH 082/485] docs --- ZzzTracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 651c06b..f43d2c4 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -35,7 +35,7 @@ _RATIO = const(30) # must be _WIN_L / _POLLFREQ, means that data will be writte _WU_ON = const(0) # const(1) to activate wake up alarm, const(0) to disable _WU_LAT = const(28800) # maximum seconds of sleep before waking you up, default 28800 = 8h, will compute best wake up time from _WU_LAT - _WU_ANTICIP seconds -_WU_ANT_ON = const(0) +_WU_ANT_ON = const(0) # const(1) to activate waking up before normal time _WU_ANTICIP = const(1800) # default 1800 = 30 minutes _FONT = sans18 From 40fa059b2fa07cbe801ecc3d022f83931445d126 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 12:45:26 +0100 Subject: [PATCH 083/485] style --- ZzzTracker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index f43d2c4..9524987 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -198,7 +198,6 @@ class ZzzTrackerApp(): def _compute_best_WU(self): """computes best wake up time from sleep data""" - return True # disabled for now # stop tracking to save memory self._disable_tracking() gc.collect() @@ -221,8 +220,8 @@ class ZzzTrackerApp(): # find most appropriate cosine # TODO - best_offset = 0 - system.set_alarm(self._WU_t + best_offset, self._listen_to_ticks) + self._earlier = 0 + system.set_alarm(self._WU_t + self._earlier, self._listen_to_ticks) gc.collect() From dcbada0ec8f461ff580543e4bb1d7caddbf2d630 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 19:02:02 +0100 Subject: [PATCH 084/485] fix: buff has to contain float --- ZzzTracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 9524987..9adb56f 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -71,7 +71,7 @@ class ZzzTrackerApp(): if self.btn_on.touch(event): self._tracking = True # accel data not yet written to disk: - self._buff = array("l") + self._buff = array("f") self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file self._offset = int(rtc.time()) # makes output more compact From 10416c9539edca2f60716fdf0af1a6db75d5cc97 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 31 Jan 2022 19:08:58 +0100 Subject: [PATCH 085/485] added link to forked repo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42d4360..7a212b7 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) * Status: - * Currently a sleep logger app, this will help run code on the simulator - * **Instructions**: (with modified wasp-os that exposes accel data) + * Currently building a sleep logger app, this will help run code on the simulator + * **Instructions**: (with a forked wasp-os that exposes accel data : [link](https://github.com/thiswillbeyourgithub/wasp-os)) * get the latest python file : ZzzTracker.py * compile it : `./micropython/mpy-cross/mpy-cross -mno-unicode -march=armv7m ZzzTracker.py` * send compiled : `./tools/wasptool --verbose --upload ZzzTracker.mpy --as apps/ZzzTracker.mpy --binary` From 5b67be4784d1d4f5648c626e1d1ab8160900ea84 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Feb 2022 19:12:21 +0100 Subject: [PATCH 086/485] fix: was not using arrays method correctly --- ZzzTracker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 9adb56f..e21a3e8 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -124,7 +124,7 @@ class ZzzTrackerApp(): """get one data point of accelerometer every _POLLFREQ seconds and they are then averaged and stored every _WIN_L seconds""" if self._tracking: - self._buff.extend(accel.read_xyz()) + [self._buff.append(x) for x in accel.read_xyz()] self._data_point_nb += 1 self._add_accel_alar() self._periodicSave() @@ -133,10 +133,10 @@ class ZzzTrackerApp(): """save data after averageing over a window to file""" n = self._data_point_nb - self._last_checkpoint if n >= _RATIO: - x_avg = sum(self._buff[0::3]) / n - y_avg = sum(self._buff[1::3]) / n - z_avg = sum(self._buff[2::3]) / n - del self._buff[:] + x_avg = sum([self._buff[i] for i in range(0, len(self._buff), 3)]) / n + y_avg = sum([self._buff[i] for i in range(1, len(self._buff), 3)]) / n + z_avg = sum([self._buff[i] for i in range(2, len(self._buff), 3)]) / n + self._buff = array("f") # formula from https://www.nature.com/articles/s41598-018-31266-z angl_avg = degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001))) From 0aa6c69fdacd98f3aaa0bf907d7130592a9f0415 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Feb 2022 19:12:42 +0100 Subject: [PATCH 087/485] minor : remove not needed import --- ZzzTracker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index e21a3e8..18fe649 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -17,7 +17,6 @@ Trying to log my sleep data for a few days prior to working on the algorithm from os import stat from wasp import watch, system, EventMask, gc -from time import mktime from watch import rtc, battery, accel from widgets import Clock, BatteryMeter, Button From d4af813cbc2880672708f022af704ec9d2c3e5e8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Feb 2022 19:17:52 +0100 Subject: [PATCH 088/485] new: added a debugging flag to quickly change values --- ZzzTracker.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 18fe649..c707e98 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -28,16 +28,34 @@ from micropython import const from array import array -_POLLFREQ = const(10) # poll accelerometer data every X seconds, they will be averaged -_WIN_L = const(300) # number of seconds between writing average accel values -_RATIO = const(30) # must be _WIN_L / _POLLFREQ, means that data will be written every X data points - -_WU_ON = const(0) # const(1) to activate wake up alarm, const(0) to disable -_WU_LAT = const(28800) # maximum seconds of sleep before waking you up, default 28800 = 8h, will compute best wake up time from _WU_LAT - _WU_ANTICIP seconds -_WU_ANT_ON = const(0) # const(1) to activate waking up before normal time -_WU_ANTICIP = const(1800) # default 1800 = 30 minutes - _FONT = sans18 +_DEBUGGING = const(0) +if not _DEBUGGING: + _POLLFREQ = const(10) + # poll accelerometer data every X seconds, they will be averaged + _WIN_L = const(300) + # number of seconds between writing average accel values + _RATIO = const(30) + # must be _WIN_L / _POLLFREQ, meaning data will be written every X points + + _WU_ON = const(1) + # const(1) to activate wake up alarm, const(0) to disable + _WU_LAT = const(28800) + # maximum seconds of sleep before waking you up, default 28800 = 8h, will + # compute best wake up time from _WU_LAT - _WU_ANTICIP seconds + _WU_ANT_ON = const(0) + # const(1) to activate waking up before normal time + _WU_ANTICIP = const(1800) + # default 1800 = 30 minutes +else: + _POLLFREQ = const(5) + _WIN_L = const(20) + _RATIO = const(4) + _WU_ON = const(1) + _WU_LAT = const(600) + _WU_ANT_ON = const(1) + _WU_ANTICIP = const(30) + class ZzzTrackerApp(): NAME = 'ZzzTrck' From 2627073f25922749f75e46c7208365a56f11645b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Feb 2022 20:10:15 +0100 Subject: [PATCH 089/485] minor :changed debug values --- ZzzTracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index c707e98..7c054b8 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -49,11 +49,11 @@ if not _DEBUGGING: # default 1800 = 30 minutes else: _POLLFREQ = const(5) - _WIN_L = const(20) - _RATIO = const(4) + _WIN_L = const(10) + _RATIO = const(2) _WU_ON = const(1) _WU_LAT = const(600) - _WU_ANT_ON = const(1) + _WU_ANT_ON = const(0) _WU_ANTICIP = const(30) From ea4cee7ffd40e250f83aaeaaf588a4beecb817e4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Feb 2022 20:10:25 +0100 Subject: [PATCH 090/485] minor --- ZzzTracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 7c054b8..06a9799 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -248,7 +248,7 @@ class ZzzTrackerApp(): system.wake() system.switch(self) self._draw() - system.request_tick(1000) + system.request_tick(period_ms=1001) def tick(self, ticks): """vibrate to wake you up""" From c9da01896f079d8d845ee67ced84ffec57d94d3f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Feb 2022 20:26:57 +0100 Subject: [PATCH 091/485] fix: debug block at the top --- ZzzTracker.py | 50 ++++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 06a9799..087d61a 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -27,34 +27,32 @@ from math import atan, pow, degrees, sqrt from micropython import const from array import array - _FONT = sans18 -_DEBUGGING = const(0) -if not _DEBUGGING: - _POLLFREQ = const(10) - # poll accelerometer data every X seconds, they will be averaged - _WIN_L = const(300) - # number of seconds between writing average accel values - _RATIO = const(30) - # must be _WIN_L / _POLLFREQ, meaning data will be written every X points - _WU_ON = const(1) - # const(1) to activate wake up alarm, const(0) to disable - _WU_LAT = const(28800) - # maximum seconds of sleep before waking you up, default 28800 = 8h, will - # compute best wake up time from _WU_LAT - _WU_ANTICIP seconds - _WU_ANT_ON = const(0) - # const(1) to activate waking up before normal time - _WU_ANTICIP = const(1800) - # default 1800 = 30 minutes -else: - _POLLFREQ = const(5) - _WIN_L = const(10) - _RATIO = const(2) - _WU_ON = const(1) - _WU_LAT = const(600) - _WU_ANT_ON = const(0) - _WU_ANTICIP = const(30) +# DEBUG MODE : comment the next paragraph and uncomment DEBUG BLOCK + +_POLLFREQ = const(10) # poll accelerometer data every X seconds, they will +# be averaged +_WIN_L = const(300) # number of seconds between writing average accel values +_RATIO = const(30) # must be _WIN_L / _POLLFREQ, meaning data will be +# written every X points + +_WU_ON = const(1) # const(1) to activate wake up alarm, const(0) to disable +_WU_LAT = const(28800) # maximum seconds of sleep before waking you up, +# default 28800 = 8h, will compute best wake up time +# from _WU_LAT - _WU_ANTICIP seconds +_WU_ANT_ON = const(0) # const(1) to activate waking up before normal time +_WU_ANTICIP = const(1800) # default 1800 = 30 minutes + + +# DEBUG BLOCK: +#_POLLFREQ = const(5) +#_WIN_L = const(10) +#_RATIO = const(2) +#_WU_ON = const(1) +#_WU_LAT = const(600) +#_WU_ANT_ON = const(0) +#_WU_ANTICIP = const(30) class ZzzTrackerApp(): From a56eb3a91c05001ac9efc9d55eecb56c40b09232 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 09:35:01 +0100 Subject: [PATCH 092/485] docs: mention pandas command --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7a212b7..067c569 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ * send compiled : `./tools/wasptool --verbose --upload ZzzTracker.mpy --as apps/ZzzTracker.mpy --binary` * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('apps.ZzzTracker.ZzzTrackerApp')` * run it! + * get back the data using `wasptool --pull` + * take a look at it using pandas : ` df = pd.read_csv("./first.night.csv", names=["time", "x_avg", "y_avg", "z_avg", "angl_avg", "battery"])` ## Roadmap / Currently planned features: **First step** From 32a1b10a96c4ce3c4d14cdfc5e5f54c6ae6f0a71 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 09:49:55 +0100 Subject: [PATCH 093/485] fix: was only vibrating once --- ZzzTracker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 087d61a..5be1f8b 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -244,13 +244,12 @@ class ZzzTrackerApp(): """listen to ticks every second, telling the watch to vibrate""" self._WakingUp = True system.wake() + system.keep_awake() system.switch(self) self._draw() - system.request_tick(period_ms=1001) + system.request_tick(period_ms=1000) def tick(self, ticks): """vibrate to wake you up""" if self._WakingUp: - system.switch(self) - system.keep_awake() watch.vibrator.pulse(duty=50, ms=500) From 565cbc08918aa19a9d65edb9c0b3d336e331c0ae Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 09:56:23 +0100 Subject: [PATCH 094/485] remove _RATIO var --- ZzzTracker.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 5be1f8b..1e02cab 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -31,10 +31,9 @@ _FONT = sans18 # DEBUG MODE : comment the next paragraph and uncomment DEBUG BLOCK -_POLLFREQ = const(10) # poll accelerometer data every X seconds, they will +_POLLFREQ = const(10) # get accelerometer data every X seconds, they will # be averaged -_WIN_L = const(300) # number of seconds between writing average accel values -_RATIO = const(30) # must be _WIN_L / _POLLFREQ, meaning data will be +_WIN_L = const(300) # number of seconds between storing average values to file # written every X points _WU_ON = const(1) # const(1) to activate wake up alarm, const(0) to disable @@ -48,7 +47,6 @@ _WU_ANTICIP = const(1800) # default 1800 = 30 minutes # DEBUG BLOCK: #_POLLFREQ = const(5) #_WIN_L = const(10) -#_RATIO = const(2) #_WU_ON = const(1) #_WU_LAT = const(600) #_WU_ANT_ON = const(0) @@ -147,7 +145,7 @@ class ZzzTrackerApp(): def _periodicSave(self): """save data after averageing over a window to file""" n = self._data_point_nb - self._last_checkpoint - if n >= _RATIO: + if n >= _WIN_L / _POLLFREQ: x_avg = sum([self._buff[i] for i in range(0, len(self._buff), 3)]) / n y_avg = sum([self._buff[i] for i in range(1, len(self._buff), 3)]) / n z_avg = sum([self._buff[i] for i in range(2, len(self._buff), 3)]) / n From dd0f4ae8d0c94378e0eb8b34fafbf3b8cf4958d7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 09:59:55 +0100 Subject: [PATCH 095/485] better default values --- ZzzTracker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 1e02cab..3b5314e 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -37,10 +37,10 @@ _WIN_L = const(300) # number of seconds between storing average values to file # written every X points _WU_ON = const(1) # const(1) to activate wake up alarm, const(0) to disable -_WU_LAT = const(28800) # maximum seconds of sleep before waking you up, -# default 28800 = 8h, will compute best wake up time -# from _WU_LAT - _WU_ANTICIP seconds -_WU_ANT_ON = const(0) # const(1) to activate waking up before normal time +_WU_LAT = const(27000) # maximum seconds of sleep before waking you up, +# default 27000 = 7h30 +_WU_ANT_ON = const(0) # set to 1 to activate waking you up at optimal time +# based on accelerometer data, at the earliest at _WU_LAT - _WU_ANTICIP _WU_ANTICIP = const(1800) # default 1800 = 30 minutes From 0fea39a9d18e5077ed45d15d2d08767eca0f9291 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 11:43:41 +0100 Subject: [PATCH 096/485] docs: add timestamp to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 067c569..aa71599 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## Note to reader: * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) -* Status: +* Status as of February 23rd: * Currently building a sleep logger app, this will help run code on the simulator * **Instructions**: (with a forked wasp-os that exposes accel data : [link](https://github.com/thiswillbeyourgithub/wasp-os)) * get the latest python file : ZzzTracker.py From 5942d8cce92861a005753bb589f5d04f4691c995 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 11:43:51 +0100 Subject: [PATCH 097/485] docs: better pandas instruction --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa71599..905c7f1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('apps.ZzzTracker.ZzzTrackerApp')` * run it! * get back the data using `wasptool --pull` - * take a look at it using pandas : ` df = pd.read_csv("./first.night.csv", names=["time", "x_avg", "y_avg", "z_avg", "angl_avg", "battery"])` + * take a look at it using pandas, for example using : ` df = pd.read_csv("./first.night.csv", names=["time", "x_avg", "y_avg", "z_avg", "angl_avg", "battery"])` (name and number of columns might change) ## Roadmap / Currently planned features: **First step** From 30d29de710e0dd045299f3c3af501702009f4c2c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 11:44:04 +0100 Subject: [PATCH 098/485] change default values --- ZzzTracker.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 3b5314e..431aa49 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -23,15 +23,15 @@ from widgets import Clock, BatteryMeter, Button from shell import mkdir, cd from fonts import sans18 -from math import atan, pow, degrees, sqrt +from math import atan, pow, degrees, sqrt, sin, pi from micropython import const from array import array _FONT = sans18 -# DEBUG MODE : comment the next paragraph and uncomment DEBUG BLOCK +# to activate DEBUG MODE : comment the next paragraph and uncomment DEBUG BLOCK -_POLLFREQ = const(10) # get accelerometer data every X seconds, they will +_POLLFREQ = const(5) # get accelerometer data every X seconds, they will # be averaged _WIN_L = const(300) # number of seconds between storing average values to file # written every X points @@ -45,12 +45,12 @@ _WU_ANTICIP = const(1800) # default 1800 = 30 minutes # DEBUG BLOCK: -#_POLLFREQ = const(5) -#_WIN_L = const(10) +#_POLLFREQ = const(1) +#_WIN_L = const(5) #_WU_ON = const(1) -#_WU_LAT = const(600) -#_WU_ANT_ON = const(0) -#_WU_ANTICIP = const(30) +#_WU_LAT = const(30) +#_WU_ANT_ON = const(1) +#_WU_ANTICIP = const(5) class ZzzTrackerApp(): From ecbc635ee00ff2bd1d73c2fef3aeaae698a5f8a1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 11:44:19 +0100 Subject: [PATCH 099/485] change the way data is stored in file --- ZzzTracker.py | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 431aa49..d6c892b 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -144,30 +144,27 @@ class ZzzTrackerApp(): def _periodicSave(self): """save data after averageing over a window to file""" - n = self._data_point_nb - self._last_checkpoint - if n >= _WIN_L / _POLLFREQ: - x_avg = sum([self._buff[i] for i in range(0, len(self._buff), 3)]) / n - y_avg = sum([self._buff[i] for i in range(1, len(self._buff), 3)]) / n - z_avg = sum([self._buff[i] for i in range(2, len(self._buff), 3)]) / n - self._buff = array("f") - - # formula from https://www.nature.com/articles/s41598-018-31266-z - angl_avg = degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001))) - - val = array("f") - val.append(int(rtc.time() - self._offset)) - val.append(x_avg) - val.append(y_avg) - val.append(z_avg) - val.append(angl_avg) - val.append(battery.level()) + if self._data_point_nb - self._last_checkpoint >= _WIN_L / _POLLFREQ: + x_avg = sum([self._buff[i] for i in range(0, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) + y_avg = sum([self._buff[i] for i in range(1, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) + z_avg = sum([self._buff[i] for i in range(2, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) + self._buff = array("f") # reseting array + self._buff.append(int(rtc.time() - self._offset)) + self._buff.append(x_avg) + self._buff.append(y_avg) + self._buff.append(z_avg) + self._buff.append(degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001)))) # formula from https://www.nature.com/articles/s41598-018-31266-z + self._buff.append(degrees(atan(x_avg / (pow(y_avg, 2) + pow(z_avg, 2) + 0.0000001)))) + self._buff.append(degrees(atan(y_avg / (pow(z_avg, 2) + pow(x_avg, 2) + 0.0000001)))) + del x_avg, y_avg, z_avg + self._buff.append(battery.voltage_mv()) # currently more accurate than percent f = open(self.filep, "a") - f.write(",".join([str(x)[0:8] for x in val]) + "\n") + f.write(",".join([str(x)[0:8] for x in self._buff]) + "\n") f.close() self._last_checkpoint = self._data_point_nb - del x_avg, y_avg, z_avg, angl_avg, n, val + self._buff = array("f") gc.collect() def _draw(self): From 4d8caf085e97ec4672b8276687df69fbe86cdd20 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 11:44:41 +0100 Subject: [PATCH 100/485] progress on computing optimal wake up time --- ZzzTracker.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index d6c892b..2e291d8 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -59,6 +59,7 @@ class ZzzTrackerApp(): def __init__(self): self._tracking = False # False = not tracking, True = currently tracking self._WakingUp = False # when True, watch is currently vibrating to wake you up + self._earlier = 0 try: mkdir("logs/") except: # folder already exists @@ -222,16 +223,32 @@ class ZzzTrackerApp(): # center and scale mean = sum(data) / len(data) - data2 = array("f", [x**2 for x in data]) - std = sqrt((sum(data2) / len(data2)) - pow(mean, 2)) - del data2 + std = sqrt((sum([x**2 for x in data]) / len(data)) - pow(mean, 2)) for i in range(len(data)): data[i] = (data[i] - mean) / std + del mean, std - # find most appropriate cosine - # TODO - self._earlier = 0 - system.set_alarm(self._WU_t + self._earlier, self._listen_to_ticks) + # fitting cosine of various offsets in minutes, the best fit has the + # period indicating best wake up time + fits = array("f") + period = 324000 # 90 minutes, average sleep cycle duration + offsets = [0, 300, 600, 900, 1200, 1500, 1800] + omega = 2 * pi / period + cnt = 0 + for offset in offsets: + fits.append( + sum( + [(sin(omega * t * _WIN_L + offset) - data[t])**2 for t in range(len(data))] # least mean square regression + )) + if fits[-1] == min(fits): + best_offset = offsets[cnt] + cnt += 1 + + # TODO find best wake up time + self._earlier = 10 + + system.set_alarm(max(self._WU_t - self._earlier, int(rtc.time()) + 3), self._listen_to_ticks) # wake up right away if computation took too much time + system.cancel_alarm(self._WU_t, self._listen_to_ticks) # cancel original alarm gc.collect() From f52ba0d72a6e5b951234ba79aeca6f2351ba584f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 11:44:56 +0100 Subject: [PATCH 101/485] show user how early the wake up was when alarm is ringing --- ZzzTracker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 2e291d8..6051aac 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -176,7 +176,11 @@ class ZzzTrackerApp(): if self._WakingUp: self.btn_on = None self.btn_off = None - draw.string('WAKE UP', 0, 70) + if self._earlier != 0: + msg = "WAKE UP (" + str(self._earlier/60)[0:2] + "m early)" + else: + msg = "WAKE UP" + draw.string(msg, 0, 70) self.btn_al = Button(x=0, y=170, w=240, h=69, label="STOP") self.btn_al.draw() elif self._tracking: From 2298b1823aa4f9b8ec49fc81e8df342d0490f00e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 12:14:00 +0100 Subject: [PATCH 102/485] progress on computing optimal wake up time 2 --- ZzzTracker.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 6051aac..ea3a0c9 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -233,25 +233,34 @@ class ZzzTrackerApp(): del mean, std # fitting cosine of various offsets in minutes, the best fit has the - # period indicating best wake up time + # period indicating best wake up time: fits = array("f") - period = 324000 # 90 minutes, average sleep cycle duration offsets = [0, 300, 600, 900, 1200, 1500, 1800] - omega = 2 * pi / period - cnt = 0 - for offset in offsets: + omega = 2 * pi / 324000 # 90 minutes, average sleep cycle duration + for cnt, offset in enumerate(offsets): # least square regression fits.append( - sum( - [(sin(omega * t * _WIN_L + offset) - data[t])**2 for t in range(len(data))] # least mean square regression - )) + sum([sin(omega * t * _WIN_L + offset) * data[t] for t in range(len(data))]) + -sum([(sin(omega * t * _WIN_L + offset) - data[t])**2 for t in range(len(data))]) + ) if fits[-1] == min(fits): best_offset = offsets[cnt] - cnt += 1 + del fits, offset, offsets, cnt - # TODO find best wake up time - self._earlier = 10 + # finding how early to wake up: + max_sin = 0 + for t in range(self._WU_t, self._WU_t - _WU_ANTICIP, -300): # counting backwards from original wake up time, steps of 5 minutes + s = sin(omega * t + best_offset) + if s > max_sin: + max_sin = s + self._earlier = -t # number of seconds earlier than wake up time + del max_sin, s - system.set_alarm(max(self._WU_t - self._earlier, int(rtc.time()) + 3), self._listen_to_ticks) # wake up right away if computation took too much time + print(self._earlier) + system.set_alarm( + min( + max(self._WU_t - self._earlier, int(rtc.time()) + 3), # not before right now + self._WU_t - 5 # not after original wake up time + ), self._listen_to_ticks) system.cancel_alarm(self._WU_t, self._listen_to_ticks) # cancel original alarm gc.collect() From 76378ea0e7263a29ff471905b56cc3ec51187512 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 16:22:46 +0100 Subject: [PATCH 103/485] new: removed indication of file size --- ZzzTracker.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index ea3a0c9..a992268 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -186,10 +186,6 @@ class ZzzTrackerApp(): elif self._tracking: draw.string('Started at ' + ":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]]), 0, 70) draw.string("data points:" + str(self._data_point_nb), 0, 90) - try: - draw.string("file size:" + str(stat(self.filep)[6]), 0, 110) - except: - pass if _WU_ON: if _WU_ANT_ON: word = " before " From f1fea9ca2d1ba131170e601f3c3ee881310cf7b4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 16:39:50 +0100 Subject: [PATCH 104/485] new: use a more intuitive self._page variable to keep track of app state --- ZzzTracker.py | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index a992268..0c56358 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -58,8 +58,8 @@ class ZzzTrackerApp(): def __init__(self): self._tracking = False # False = not tracking, True = currently tracking - self._WakingUp = False # when True, watch is currently vibrating to wake you up self._earlier = 0 + self._page = "START" # can be START / TRACKING / RINGING / WAITING_EARLY_WU try: mkdir("logs/") except: # folder already exists @@ -81,7 +81,7 @@ class ZzzTrackerApp(): def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" - if self.btn_on: + if self._page == "START": if self.btn_on.touch(event): self._tracking = True # accel data not yet written to disk: @@ -103,15 +103,15 @@ class ZzzTrackerApp(): if _WU_ANT_ON: self._WU_a = self._WU_t - _WU_ANTICIP system.set_alarm(self._WU_a, self._compute_best_WU) - elif self.btn_off: + self._page = "TRACKING" + elif self._page == "TRACKING": if self.btn_off.touch(event): - if self._tracking: - self._disable_tracking() - elif self.btn_al: + self._disable_tracking() + self._page = "START" + elif self._page == "RINGING": if self.btn_al.touch(event): - self._WakingUp = False - if self._tracking: - self._disable_tracking() + self._disable_tracking() + self._page = "START" self._draw() def _disable_tracking(self): @@ -125,8 +125,6 @@ class ZzzTrackerApp(): if _WU_ANT_ON: system.cancel_alarm(self._WU_a, self._compute_best_WU) self._periodicSave() - self._offset = None - self._last_checkpoint = 0 def _add_accel_alar(self): """set an alarm, due in _POLLFREQ minutes, to log the accelerometer data @@ -173,9 +171,7 @@ class ZzzTrackerApp(): draw = watch.drawable draw.fill(0) draw.set_font(_FONT) - if self._WakingUp: - self.btn_on = None - self.btn_off = None + if self._page == "RINGING": if self._earlier != 0: msg = "WAKE UP (" + str(self._earlier/60)[0:2] + "m early)" else: @@ -183,7 +179,7 @@ class ZzzTrackerApp(): draw.string(msg, 0, 70) self.btn_al = Button(x=0, y=170, w=240, h=69, label="STOP") self.btn_al.draw() - elif self._tracking: + elif self._page in ["TRACKING", "WAITING_EARLY_WU"]: draw.string('Started at ' + ":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]]), 0, 70) draw.string("data points:" + str(self._data_point_nb), 0, 90) if _WU_ON: @@ -192,16 +188,13 @@ class ZzzTrackerApp(): else: word = " at " draw.string("Wake up" + word + ":".join([str(x) for x in watch.time.localtime(self._offset + _WU_LAT)[3:5]]), 0, 130) - self.btn_on = None - self.btn_al = None self.btn_off = Button(x=0, y=170, w=240, h=69, label="Stop tracking") self.btn_off.draw() - else: + elif self._page == "START": draw.string('Sleep tracker' , 0, 70) - self.btn_off = None - self.btn_al = None self.btn_on = Button(x=0, y=170, w=240, h=69, label="Start tracking") self.btn_on.draw() + self.cl = Clock(True) self.cl.draw() bat = BatteryMeter() @@ -251,19 +244,18 @@ class ZzzTrackerApp(): self._earlier = -t # number of seconds earlier than wake up time del max_sin, s - print(self._earlier) system.set_alarm( min( max(self._WU_t - self._earlier, int(rtc.time()) + 3), # not before right now self._WU_t - 5 # not after original wake up time ), self._listen_to_ticks) - system.cancel_alarm(self._WU_t, self._listen_to_ticks) # cancel original alarm + self._page = "WAITING_EARLY_WU" gc.collect() def _listen_to_ticks(self): """listen to ticks every second, telling the watch to vibrate""" - self._WakingUp = True + self._page = "RINGING" system.wake() system.keep_awake() system.switch(self) @@ -272,5 +264,5 @@ class ZzzTrackerApp(): def tick(self, ticks): """vibrate to wake you up""" - if self._WakingUp: + if self._page == "RINGING": watch.vibrator.pulse(duty=50, ms=500) From 669c45458bcb2e2dd5f70235fc9cb8e1b4d7616d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 18:25:34 +0100 Subject: [PATCH 105/485] major version : untested algo, added settings panel --- README.md | 32 +++------- ZzzTracker.py | 173 ++++++++++++++++++++++++++++++++++---------------- 2 files changed, 129 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 905c7f1..fccb4b2 100644 --- a/README.md +++ b/README.md @@ -5,46 +5,32 @@ * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) * Status as of February 23rd: - * Currently building a sleep logger app, this will help run code on the simulator + * First version finished, the waking up algorithm is not at all tested * **Instructions**: (with a forked wasp-os that exposes accel data : [link](https://github.com/thiswillbeyourgithub/wasp-os)) * get the latest python file : ZzzTracker.py * compile it : `./micropython/mpy-cross/mpy-cross -mno-unicode -march=armv7m ZzzTracker.py` * send compiled : `./tools/wasptool --verbose --upload ZzzTracker.mpy --as apps/ZzzTracker.mpy --binary` * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('apps.ZzzTracker.ZzzTrackerApp')` * run it! - * get back the data using `wasptool --pull` + * if you want, get back the data using `wasptool --pull` * take a look at it using pandas, for example using : ` df = pd.read_csv("./first.night.csv", names=["time", "x_avg", "y_avg", "z_avg", "angl_avg", "battery"])` (name and number of columns might change) -## Roadmap / Currently planned features: -**First step** -* ~~focus on logging your sleep to accumulate data and share it on github~~DONE. The more data you have the easier it will be to calibrate the algorithm. - +## TODO **sleep tracking** -* tracks sleep using wrist motion data and occasional heart rate monitoring - * each night is recorded in a file that can be easily sent back to the phone -* rudimentary display of sleep graph on the device itself, with a quality score if I can find a good metric -* try to roughly infer the sleep stage *on the device itself* +* try to roughly infer the sleep stage directly on the device? * if you actually use the watch during the night, make sure to count it as wakefulness -**alarm clock** -* setting up an alarm should suggest the most appropriate sleep duration like what [sleepyti.me](http://sleepyti.me) does, so either sleep duration or by wake up time -* try to optimize the wake up time based on inferred sleep stage - **settings panel** -* to specify how early the watch can wake you -* to specify a battery threshold under which it should not keep tracking sleep, to make sure you don't drain the battery and end up missing the alarm clock +* specify a battery threshold under which it should not keep tracking sleep, to make sure you don't drain the battery and end up missing the alarm clock **misc** -* turn off the Bluetooth connection when no phone is connected -* turn off the screen during the night -* make sure to not use more than X% of the battery in all cases -* make sure to turn off if sleep lasts more than 12h (in which case the user forgot to disable it) +* turn off the Bluetooth connection when no phone is connected? +* confirmation screen when disabling sleep tracking * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. -* hardcode limits to avoid issues if heart rate is suddenly found to be through the roof or something -* maybe the least cpu intensive way to compute optimal wake up time would be to compute least square difference with sinusoidal of varying periods and phases. +* find a way to remove outliers of stored values **Features that I'm note sure yet** -* should the watch ask you after waking up to rate your sleep on a simple scale? +* should the watch ask you after waking up to rate your freshness at wake? ## Related links: * article with detailed implementation : https://www.nature.com/articles/s41598-018-31266-z diff --git a/ZzzTracker.py b/ZzzTracker.py index 0c56358..0899475 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -15,51 +15,35 @@ Current state: Trying to log my sleep data for a few days prior to working on the algorithm """ -from os import stat +import time from wasp import watch, system, EventMask, gc from watch import rtc, battery, accel -from widgets import Clock, BatteryMeter, Button +from widgets import Clock, BatteryMeter, Button, Spinner, Checkbox from shell import mkdir, cd from fonts import sans18 from math import atan, pow, degrees, sqrt, sin, pi -from micropython import const from array import array _FONT = sans18 -# to activate DEBUG MODE : comment the next paragraph and uncomment DEBUG BLOCK - -_POLLFREQ = const(5) # get accelerometer data every X seconds, they will -# be averaged -_WIN_L = const(300) # number of seconds between storing average values to file -# written every X points - -_WU_ON = const(1) # const(1) to activate wake up alarm, const(0) to disable -_WU_LAT = const(27000) # maximum seconds of sleep before waking you up, -# default 27000 = 7h30 -_WU_ANT_ON = const(0) # set to 1 to activate waking you up at optimal time -# based on accelerometer data, at the earliest at _WU_LAT - _WU_ANTICIP -_WU_ANTICIP = const(1800) # default 1800 = 30 minutes - - -# DEBUG BLOCK: -#_POLLFREQ = const(1) -#_WIN_L = const(5) -#_WU_ON = const(1) -#_WU_LAT = const(30) -#_WU_ANT_ON = const(1) -#_WU_ANTICIP = const(5) - - class ZzzTrackerApp(): NAME = 'ZzzTrck' def __init__(self): + self._wakeup_enabled = 1 + self._wakeup_ant_enabled = 1 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_ANTICIP + self._freq = 5 # get accelerometer data every X seconds, they will be averaged + self._store_freq = 300 # number of seconds between storing average values to file written every X points + self._wakeup_ant_latitude = 1800 # defaults 1800 = 30m + self._spinval_H = 7 # default wake up time + self._spinval_M = 30 + self._debug = False self._tracking = False # False = not tracking, True = currently tracking self._earlier = 0 - self._page = "START" # can be START / TRACKING / RINGING / WAITING_EARLY_WU + self._page = "START" # can be START / TRACKING / RINGING / WAITING_EARLY_WU / SETTINGS + try: mkdir("logs/") except: # folder already exists @@ -81,6 +65,7 @@ class ZzzTrackerApp(): def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" + no_full_draw = False if self._page == "START": if self.btn_on.touch(event): self._tracking = True @@ -94,16 +79,32 @@ class ZzzTrackerApp(): self.filep = "logs/sleep/" + str(self._offset) + ".csv" self._add_accel_alar() - # alarm in _WU_LAT seconds after tracking started to wake you up - if _WU_ON: - self._WU_t = self._offset + _WU_LAT + if self._debug: + self._freq = 1 + self._store_freq = 5 + self._wakeup_ant_latitude = 5 + + # alarm in self._SL_L seconds after tracking started to wake you up + self._SL_L = self._spinval_H*60*60 + self._spinval_M*60 + if self._wakeup_enabled: + now = rtc.get_localtime() + yyyy = now[0] + mm = now[1] + dd = now[2] + HH = self._spinval_H + MM = self._spinval_M + if HH < now[3] or (HH == now[3] and MM <= now[4]): + dd += 1 + self._WU_t = time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) system.set_alarm(self._WU_t, self._listen_to_ticks) - # alarm in _WU_ANTICIP less seconds to compute best wake up time - if _WU_ANT_ON: - self._WU_a = self._WU_t - _WU_ANTICIP + # alarm in self._wakeup_ant_latitude less seconds to compute best wake up time + if self._wakeup_ant_enabled: + self._WU_a = self._WU_t - self._wakeup_ant_latitude system.set_alarm(self._WU_a, self._compute_best_WU) self._page = "TRACKING" + elif self.btn_set.touch(event): + self._page = "SETTINGS" elif self._page == "TRACKING": if self.btn_off.touch(event): self._disable_tracking() @@ -112,7 +113,47 @@ class ZzzTrackerApp(): if self.btn_al.touch(event): self._disable_tracking() self._page = "START" - self._draw() + elif self._page == "SETTINGS": + no_full_draw = True + disable_both = False + if self.check_al.touch(event): + if self._wakeup_enabled == 1: + self._wakeup_enabled = 0 + disable_both = True + else: + self._wakeup_enabled = 1 + self.check_al.state = self._wakeup_enabled + self.check_al.update() + if self.check_anti.touch(event) or disable_both: + if self._wakeup_ant_enabled == 1 or disable_both: + self._wakeup_ant_enabled = 0 + self.check_anti.state = self._wakeup_ant_enabled + self._check_anti = None + self._draw() + elif self._wakeup_enabled == 1: + self._wakeup_ant_enabled = 1 + self.check_anti.state = self._wakeup_ant_enabled + self.check_anti.update() + elif self.hours.touch(event): + self._spinval_H = self.hours.value + self.hours.update() + elif self.min.touch(event): + self._spinval_M = self.min.value + self.min.update() + elif self.check_debug.touch(event): + if self._debug: + self._debug = False + else: + self._debug = True + self.check_debug.update() + elif self.btn_set_end.touch(event): + self._page = "START" + self._draw() + else: + self._draw() + + if no_full_draw is False: + self._draw() def _disable_tracking(self): """called by touching "STOP TRACKING" or when computing best alarm time @@ -120,21 +161,21 @@ class ZzzTrackerApp(): disables tracking features and alarms""" self._tracking = False system.cancel_alarm(self.next_al, self._trackOnce) - if _WU_ON: + if self._wakeup_enabled: system.cancel_alarm(self._WU_t, self._listen_to_ticks) - if _WU_ANT_ON: + if self._wakeup_ant_enabled: system.cancel_alarm(self._WU_a, self._compute_best_WU) self._periodicSave() def _add_accel_alar(self): - """set an alarm, due in _POLLFREQ minutes, to log the accelerometer data + """set an alarm, due in self._freq minutes, to log the accelerometer data once""" - self.next_al = watch.rtc.time() + _POLLFREQ + self.next_al = watch.rtc.time() + self._freq system.set_alarm(self.next_al, self._trackOnce) def _trackOnce(self): - """get one data point of accelerometer every _POLLFREQ seconds and - they are then averaged and stored every _WIN_L seconds""" + """get one data point of accelerometer every self._freq seconds and + they are then averaged and stored every self._store_freq seconds""" if self._tracking: [self._buff.append(x) for x in accel.read_xyz()] self._data_point_nb += 1 @@ -143,7 +184,7 @@ class ZzzTrackerApp(): def _periodicSave(self): """save data after averageing over a window to file""" - if self._data_point_nb - self._last_checkpoint >= _WIN_L / _POLLFREQ: + if self._data_point_nb - self._last_checkpoint >= self._store_freq / self._freq: x_avg = sum([self._buff[i] for i in range(0, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) y_avg = sum([self._buff[i] for i in range(1, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) z_avg = sum([self._buff[i] for i in range(2, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) @@ -182,23 +223,49 @@ class ZzzTrackerApp(): elif self._page in ["TRACKING", "WAITING_EARLY_WU"]: draw.string('Started at ' + ":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]]), 0, 70) draw.string("data points:" + str(self._data_point_nb), 0, 90) - if _WU_ON: - if _WU_ANT_ON: + if self._wakeup_enabled: + if self._wakeup_ant_enabled: word = " before " else: word = " at " - draw.string("Wake up" + word + ":".join([str(x) for x in watch.time.localtime(self._offset + _WU_LAT)[3:5]]), 0, 130) + draw.string("Wake up" + word + ":".join([str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]]), 0, 130) self.btn_off = Button(x=0, y=170, w=240, h=69, label="Stop tracking") self.btn_off.draw() elif self._page == "START": draw.string('Sleep tracker' , 0, 70) - self.btn_on = Button(x=0, y=170, w=240, h=69, label="Start tracking") + self.btn_on = Button(x=0, y=170, w=200, h=40, label="Start tracking") self.btn_on.draw() + self.btn_set = Button(x=201, y=170, w=38, h=40, label="S") + self.btn_set.draw() + elif self._page == "SETTINGS": + draw.string("Settings", 0, 0) + self.btn_set_end = Button(x=201, y=0, w=38, h=40, label="X") + self.btn_set_end.draw() - self.cl = Clock(True) - self.cl.draw() - bat = BatteryMeter() - bat.draw() + self.hours = Spinner(0, 5, 0, 23, 2) + self.hours.value = self._spinval_H + self.hours.draw() + self.min = Spinner(60, 5, 0, 59, 2) + self.min.value = self._spinval_M + self.min.draw() + + self.check_debug = Checkbox(x=0, y=120, label="Debug?") + self.check_debug.state = self._debug + self.check_debug.draw() + self.check_al = Checkbox(x=0, y=160, label="Alarm?") + self.check_al.state = self._wakeup_enabled + self.check_al.draw() + if self.check_al.state == 1: + self.check_anti = Checkbox(x=0, y=200, label="Anticipate?") + self.check_anti.state = self._wakeup_ant_enabled + self.check_anti.draw() + + + if self._page != "SETTINGS": + self.cl = Clock(True) + self.cl.draw() + bat = BatteryMeter() + bat.draw() def _compute_best_WU(self): """computes best wake up time from sleep data""" @@ -228,8 +295,8 @@ class ZzzTrackerApp(): omega = 2 * pi / 324000 # 90 minutes, average sleep cycle duration for cnt, offset in enumerate(offsets): # least square regression fits.append( - sum([sin(omega * t * _WIN_L + offset) * data[t] for t in range(len(data))]) - -sum([(sin(omega * t * _WIN_L + offset) - data[t])**2 for t in range(len(data))]) + sum([sin(omega * t * self._store_freq + offset) * data[t] for t in range(len(data))]) + -sum([(sin(omega * t * self._store_freq + offset) - data[t])**2 for t in range(len(data))]) ) if fits[-1] == min(fits): best_offset = offsets[cnt] @@ -237,7 +304,7 @@ class ZzzTrackerApp(): # finding how early to wake up: max_sin = 0 - for t in range(self._WU_t, self._WU_t - _WU_ANTICIP, -300): # counting backwards from original wake up time, steps of 5 minutes + for t in range(self._WU_t, self._WU_t - self._wakeup_ant_latitude, -300): # counting backwards from original wake up time, steps of 5 minutes s = sin(omega * t + best_offset) if s > max_sin: max_sin = s From 54b056dd2df1d27f51dd12fd7bda7ff2996cf750 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Feb 2022 19:58:12 +0100 Subject: [PATCH 106/485] comment 2 testing lines --- ZzzTracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 0899475..22bffdd 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -194,8 +194,8 @@ class ZzzTrackerApp(): self._buff.append(y_avg) self._buff.append(z_avg) self._buff.append(degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001)))) # formula from https://www.nature.com/articles/s41598-018-31266-z - self._buff.append(degrees(atan(x_avg / (pow(y_avg, 2) + pow(z_avg, 2) + 0.0000001)))) - self._buff.append(degrees(atan(y_avg / (pow(z_avg, 2) + pow(x_avg, 2) + 0.0000001)))) +# self._buff.append(degrees(atan(x_avg / (pow(y_avg, 2) + pow(z_avg, 2) + 0.0000001)))) +# self._buff.append(degrees(atan(y_avg / (pow(z_avg, 2) + pow(x_avg, 2) + 0.0000001)))) del x_avg, y_avg, z_avg self._buff.append(battery.voltage_mv()) # currently more accurate than percent From dcb14a637291faa4d5be260fb3e39faef2aa17d6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 11:21:10 +0100 Subject: [PATCH 107/485] use statusbar instead of clock and battery --- ZzzTracker.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 22bffdd..8260fee 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -19,7 +19,7 @@ import time from wasp import watch, system, EventMask, gc from watch import rtc, battery, accel -from widgets import Clock, BatteryMeter, Button, Spinner, Checkbox +from widgets import Button, Spinner, Checkbox, StatusBar from shell import mkdir, cd from fonts import sans18 @@ -262,10 +262,9 @@ class ZzzTrackerApp(): if self._page != "SETTINGS": - self.cl = Clock(True) - self.cl.draw() - bat = BatteryMeter() - bat.draw() + self.stat_bar = StatusBar() + self.stat_bar.clock = True + self.stat_bar.draw() def _compute_best_WU(self): """computes best wake up time from sleep data""" From 856ccca2cd23d3314eb421cbca6e898430a70ba7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 11:24:51 +0100 Subject: [PATCH 108/485] minor: styling --- ZzzTracker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ZzzTracker.py b/ZzzTracker.py index 8260fee..14b3b21 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -105,14 +105,17 @@ class ZzzTrackerApp(): self._page = "TRACKING" elif self.btn_set.touch(event): self._page = "SETTINGS" + elif self._page == "TRACKING": if self.btn_off.touch(event): self._disable_tracking() self._page = "START" + elif self._page == "RINGING": if self.btn_al.touch(event): self._disable_tracking() self._page = "START" + elif self._page == "SETTINGS": no_full_draw = True disable_both = False From 5bd095f8e9b74db4c60c7f69ac7e20bb6ff20f60 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 11:25:01 +0100 Subject: [PATCH 109/485] better docstring at the top --- ZzzTracker.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 14b3b21..c4be90c 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -6,13 +6,10 @@ # https://github.com/thiswillbeyourgithub/sleep_tracker_pinetime_wasp-os -End goal: -This app is designed to track accelerometer and heart rate data periodically -during the night. It can also compute the best time to wake you up, up to -30 minutes before the alarm you set up manually. +This app is designed to track accelerometer data throughout the night. It can +also compute the best time to wake you up, up to 30 minutes before the +alarm you set up manually. -Current state: -Trying to log my sleep data for a few days prior to working on the algorithm """ import time From 374cc649a8145b967fe564d37981f4ff26af7c7f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 11:34:38 +0100 Subject: [PATCH 110/485] smaller STOP button --- ZzzTracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index c4be90c..3f6a235 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -218,7 +218,7 @@ class ZzzTrackerApp(): else: msg = "WAKE UP" draw.string(msg, 0, 70) - self.btn_al = Button(x=0, y=170, w=240, h=69, label="STOP") + self.btn_al = Button(x=0, y=170, w=240, h=40, label="STOP") self.btn_al.draw() elif self._page in ["TRACKING", "WAITING_EARLY_WU"]: draw.string('Started at ' + ":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]]), 0, 70) From 74cecc4492cac324869020114316b06a4ccc1857 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 11:36:39 +0100 Subject: [PATCH 111/485] smaller STOP button 2 --- ZzzTracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 3f6a235..1bbcd0e 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -229,7 +229,7 @@ class ZzzTrackerApp(): else: word = " at " draw.string("Wake up" + word + ":".join([str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]]), 0, 130) - self.btn_off = Button(x=0, y=170, w=240, h=69, label="Stop tracking") + self.btn_off = Button(x=0, y=170, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == "START": draw.string('Sleep tracker' , 0, 70) From 3215fa638d9bfcc8303e514376e38430017b4bb5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 11:48:35 +0100 Subject: [PATCH 112/485] new: confirmation view when disabling tracking --- ZzzTracker.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 1bbcd0e..c979fb1 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -16,7 +16,7 @@ import time from wasp import watch, system, EventMask, gc from watch import rtc, battery, accel -from widgets import Button, Spinner, Checkbox, StatusBar +from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView from shell import mkdir, cd from fonts import sans18 @@ -37,6 +37,7 @@ class ZzzTrackerApp(): self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._debug = False + self.confirmation_view = None self._tracking = False # False = not tracking, True = currently tracking self._earlier = 0 self._page = "START" # can be START / TRACKING / RINGING / WAITING_EARLY_WU / SETTINGS @@ -103,10 +104,18 @@ class ZzzTrackerApp(): elif self.btn_set.touch(event): self._page = "SETTINGS" - elif self._page == "TRACKING": - if self.btn_off.touch(event): - self._disable_tracking() - self._page = "START" + elif self._page in ["TRACKING", "WAITING_EARLY_WU"]: + if self.confirmation_view is None: + no_full_draw = True + if self.btn_off.touch(event): + self.confirmation_view = ConfirmationView() + self.confirmation_view.draw("Stop tracking?") + else: + if self.confirmation_view.touch(event): + if self.confirmation_view.value: + self._disable_tracking() + self._page = "START" + self.confirmation_view = None elif self._page == "RINGING": if self.btn_al.touch(event): From fd400489c75c9f1fc85d330373a56046aaad3587 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 11:51:58 +0100 Subject: [PATCH 113/485] todo done: confirmation view when disabling tracking --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index fccb4b2..38f0625 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ **misc** * turn off the Bluetooth connection when no phone is connected? -* confirmation screen when disabling sleep tracking * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. * find a way to remove outliers of stored values From cd1e29c23e97fe02a43642c84cb3046f78dff3e1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 11:59:09 +0100 Subject: [PATCH 114/485] new: disable tracking if battery < 20% --- README.md | 2 -- ZzzTracker.py | 9 +++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 38f0625..4952976 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,6 @@ * try to roughly infer the sleep stage directly on the device? * if you actually use the watch during the night, make sure to count it as wakefulness -**settings panel** -* specify a battery threshold under which it should not keep tracking sleep, to make sure you don't drain the battery and end up missing the alarm clock **misc** * turn off the Bluetooth connection when no phone is connected? diff --git a/ZzzTracker.py b/ZzzTracker.py index c979fb1..49be8f2 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -22,8 +22,10 @@ from fonts import sans18 from math import atan, pow, degrees, sqrt, sin, pi from array import array +from micropython import const _FONT = sans18 +_BATTERY_THRESHOLD = const(20) # under 20% of battery, stop tracking and only keep the alarm class ZzzTrackerApp(): NAME = 'ZzzTrck' @@ -164,14 +166,15 @@ class ZzzTrackerApp(): if no_full_draw is False: self._draw() - def _disable_tracking(self): + def _disable_tracking(self, keep_alarm=False): """called by touching "STOP TRACKING" or when computing best alarm time to wake up you disables tracking features and alarms""" self._tracking = False system.cancel_alarm(self.next_al, self._trackOnce) if self._wakeup_enabled: - system.cancel_alarm(self._WU_t, self._listen_to_ticks) + if keep_alarm is False: # to keep the alarm when stopping because of low battery + system.cancel_alarm(self._WU_t, self._listen_to_ticks) if self._wakeup_ant_enabled: system.cancel_alarm(self._WU_a, self._compute_best_WU) self._periodicSave() @@ -190,6 +193,8 @@ class ZzzTrackerApp(): self._data_point_nb += 1 self._add_accel_alar() self._periodicSave() + if battery.level() <= _BATTERY_THRESHOLD: + self._disable_tracking(keep_alarm=True) def _periodicSave(self): """save data after averageing over a window to file""" From e8e155c4b5a9b2333e13c8252bd0bc5f53f37d59 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:00:12 +0100 Subject: [PATCH 115/485] minor: style --- ZzzTracker.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 49be8f2..aa8273c 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -39,7 +39,7 @@ class ZzzTrackerApp(): self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._debug = False - self.confirmation_view = None + self._conf_view = None self._tracking = False # False = not tracking, True = currently tracking self._earlier = 0 self._page = "START" # can be START / TRACKING / RINGING / WAITING_EARLY_WU / SETTINGS @@ -107,17 +107,17 @@ class ZzzTrackerApp(): self._page = "SETTINGS" elif self._page in ["TRACKING", "WAITING_EARLY_WU"]: - if self.confirmation_view is None: + if self._conf_view is None: no_full_draw = True if self.btn_off.touch(event): - self.confirmation_view = ConfirmationView() - self.confirmation_view.draw("Stop tracking?") + self._conf_view = ConfirmationView() + self._conf_view.draw("Stop tracking?") else: - if self.confirmation_view.touch(event): - if self.confirmation_view.value: + if self._conf_view.touch(event): + if self._conf_view.value: self._disable_tracking() self._page = "START" - self.confirmation_view = None + self._conf_view = None elif self._page == "RINGING": if self.btn_al.touch(event): From e1bcb4d0c22d124d7cc6beb6c538ee9ed1701269 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:12:26 +0100 Subject: [PATCH 116/485] use more efficient string concatenation --- ZzzTracker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index aa8273c..7867c9a 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -76,7 +76,7 @@ class ZzzTrackerApp(): self._offset = int(rtc.time()) # makes output more compact # create one file per recording session: - self.filep = "logs/sleep/" + str(self._offset) + ".csv" + self.filep = "logs/sleep/{}.csv".format(str(self._offset)) self._add_accel_alar() if self._debug: @@ -228,21 +228,21 @@ class ZzzTrackerApp(): draw.set_font(_FONT) if self._page == "RINGING": if self._earlier != 0: - msg = "WAKE UP (" + str(self._earlier/60)[0:2] + "m early)" + msg = "WAKE UP ({}m early)".format(str(self._earlier/60)[0:2]) else: msg = "WAKE UP" draw.string(msg, 0, 70) self.btn_al = Button(x=0, y=170, w=240, h=40, label="STOP") self.btn_al.draw() elif self._page in ["TRACKING", "WAITING_EARLY_WU"]: - draw.string('Started at ' + ":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]]), 0, 70) - draw.string("data points:" + str(self._data_point_nb), 0, 90) + draw.string('Started at {}'.format(":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]])), 0, 70) + draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) if self._wakeup_enabled: if self._wakeup_ant_enabled: word = " before " else: word = " at " - draw.string("Wake up" + word + ":".join([str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]]), 0, 130) + draw.string("Wake up {} {}".format(word, ":".join([str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]])), 0, 130) self.btn_off = Button(x=0, y=170, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == "START": From 336d672f5a704822f83e9b807038f887b08f34a2 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:12:33 +0100 Subject: [PATCH 117/485] minor: styling --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 4952976..f2823d1 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,7 @@ ## TODO **sleep tracking** * try to roughly infer the sleep stage directly on the device? - * if you actually use the watch during the night, make sure to count it as wakefulness - + * if you actually use the watch during the night, make sure to count it as wakefulness? **misc** * turn off the Bluetooth connection when no phone is connected? From 84a1d821e2bce00ba2a24aab0820d61d326d8112 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:12:52 +0100 Subject: [PATCH 118/485] fix: no redraw when clicking outside of buttons in settings page --- ZzzTracker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 7867c9a..e8f44c6 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -160,8 +160,6 @@ class ZzzTrackerApp(): elif self.btn_set_end.touch(event): self._page = "START" self._draw() - else: - self._draw() if no_full_draw is False: self._draw() From 6e0c02e9df1ed863b6e011cd7ac79b4ff4862197 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:13:03 +0100 Subject: [PATCH 119/485] fix: remove useless test line --- ZzzTracker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index e8f44c6..2e8d574 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -206,8 +206,6 @@ class ZzzTrackerApp(): self._buff.append(y_avg) self._buff.append(z_avg) self._buff.append(degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001)))) # formula from https://www.nature.com/articles/s41598-018-31266-z -# self._buff.append(degrees(atan(x_avg / (pow(y_avg, 2) + pow(z_avg, 2) + 0.0000001)))) -# self._buff.append(degrees(atan(y_avg / (pow(z_avg, 2) + pow(x_avg, 2) + 0.0000001)))) del x_avg, y_avg, z_avg self._buff.append(battery.voltage_mv()) # currently more accurate than percent From cbf3e5889740d874f05f0c48d45e7ffb4ae886a8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:19:56 +0100 Subject: [PATCH 120/485] remove call to gc.collect --- ZzzTracker.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 2e8d574..67af178 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -13,7 +13,7 @@ alarm you set up manually. """ import time -from wasp import watch, system, EventMask, gc +from wasp import watch, system, EventMask from watch import rtc, battery, accel from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView @@ -215,7 +215,6 @@ class ZzzTrackerApp(): self._last_checkpoint = self._data_point_nb self._buff = array("f") - gc.collect() def _draw(self): """GUI""" @@ -280,7 +279,6 @@ class ZzzTrackerApp(): """computes best wake up time from sleep data""" # stop tracking to save memory self._disable_tracking() - gc.collect() # get angle over time f = open(self.filep, "r") @@ -327,7 +325,6 @@ class ZzzTrackerApp(): ), self._listen_to_ticks) self._page = "WAITING_EARLY_WU" - gc.collect() def _listen_to_ticks(self): """listen to ticks every second, telling the watch to vibrate""" From 795853f09740dc134a0ef388794355a02ed42816 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:31:38 +0100 Subject: [PATCH 121/485] new: store offsets and average cycle length outside of RAM --- ZzzTracker.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 67af178..c716ef2 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -26,6 +26,8 @@ from micropython import const _FONT = sans18 _BATTERY_THRESHOLD = const(20) # under 20% of battery, stop tracking and only keep the alarm +_AVG_SLEEP_CYCL = const(32400) # 90 minutes, average sleep cycle duration +_OFFSETS = array("H", [0, 300, 600, 900, 1200, 1500, 1800]) class ZzzTrackerApp(): NAME = 'ZzzTrck' @@ -298,16 +300,15 @@ class ZzzTrackerApp(): # fitting cosine of various offsets in minutes, the best fit has the # period indicating best wake up time: fits = array("f") - offsets = [0, 300, 600, 900, 1200, 1500, 1800] - omega = 2 * pi / 324000 # 90 minutes, average sleep cycle duration - for cnt, offset in enumerate(offsets): # least square regression + omega = 2 * pi / _AVG_SLEEP_CYCL + for cnt, offset in enumerate(_OFFSETS): # least square regression fits.append( sum([sin(omega * t * self._store_freq + offset) * data[t] for t in range(len(data))]) -sum([(sin(omega * t * self._store_freq + offset) - data[t])**2 for t in range(len(data))]) ) if fits[-1] == min(fits): - best_offset = offsets[cnt] - del fits, offset, offsets, cnt + best_offset = _OFFSETS[cnt] + del fits, offset, _OFFSETS, cnt # finding how early to wake up: max_sin = 0 From e00621fd232176911a10173a560e1806d88d8e4a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:32:46 +0100 Subject: [PATCH 122/485] new: use binary strings whenever possible --- ZzzTracker.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index c716ef2..ddc3e30 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -44,7 +44,7 @@ class ZzzTrackerApp(): self._conf_view = None self._tracking = False # False = not tracking, True = currently tracking self._earlier = 0 - self._page = "START" # can be START / TRACKING / RINGING / WAITING_EARLY_WU / SETTINGS + self._page = b"STA" # can be START / TRACKING / TRA2 = tracking but with early wake up time computed / SETTINGS / RINGING try: mkdir("logs/") @@ -68,7 +68,7 @@ class ZzzTrackerApp(): def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" no_full_draw = False - if self._page == "START": + if self._page == b"STA": if self.btn_on.touch(event): self._tracking = True # accel data not yet written to disk: @@ -104,11 +104,11 @@ class ZzzTrackerApp(): if self._wakeup_ant_enabled: self._WU_a = self._WU_t - self._wakeup_ant_latitude system.set_alarm(self._WU_a, self._compute_best_WU) - self._page = "TRACKING" + self._page = b"TRA" elif self.btn_set.touch(event): - self._page = "SETTINGS" + self._page = b"SET" - elif self._page in ["TRACKING", "WAITING_EARLY_WU"]: + elif self._page.startswith(b"TRA"): if self._conf_view is None: no_full_draw = True if self.btn_off.touch(event): @@ -118,15 +118,15 @@ class ZzzTrackerApp(): if self._conf_view.touch(event): if self._conf_view.value: self._disable_tracking() - self._page = "START" + self._page = b"STA" self._conf_view = None - elif self._page == "RINGING": + elif self._page == b"RNG": if self.btn_al.touch(event): self._disable_tracking() - self._page = "START" + self._page = b"STA" - elif self._page == "SETTINGS": + elif self._page == b"SET": no_full_draw = True disable_both = False if self.check_al.touch(event): @@ -160,7 +160,7 @@ class ZzzTrackerApp(): self._debug = True self.check_debug.update() elif self.btn_set_end.touch(event): - self._page = "START" + self._page = b"STA" self._draw() if no_full_draw is False: @@ -211,8 +211,8 @@ class ZzzTrackerApp(): del x_avg, y_avg, z_avg self._buff.append(battery.voltage_mv()) # currently more accurate than percent - f = open(self.filep, "a") - f.write(",".join([str(x)[0:8] for x in self._buff]) + "\n") + f = open(self.filep, "ab") + f.write(b",".join([str(x)[0:8].encode() for x in self._buff]) + b"\n") f.close() self._last_checkpoint = self._data_point_nb @@ -223,7 +223,7 @@ class ZzzTrackerApp(): draw = watch.drawable draw.fill(0) draw.set_font(_FONT) - if self._page == "RINGING": + if self._page == b"RNG": if self._earlier != 0: msg = "WAKE UP ({}m early)".format(str(self._earlier/60)[0:2]) else: @@ -231,7 +231,7 @@ class ZzzTrackerApp(): draw.string(msg, 0, 70) self.btn_al = Button(x=0, y=170, w=240, h=40, label="STOP") self.btn_al.draw() - elif self._page in ["TRACKING", "WAITING_EARLY_WU"]: + elif self._page.startswith(b"TRA"): draw.string('Started at {}'.format(":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]])), 0, 70) draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) if self._wakeup_enabled: @@ -242,13 +242,13 @@ class ZzzTrackerApp(): draw.string("Wake up {} {}".format(word, ":".join([str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]])), 0, 130) self.btn_off = Button(x=0, y=170, w=240, h=40, label="Stop tracking") self.btn_off.draw() - elif self._page == "START": + elif self._page == b"STA": draw.string('Sleep tracker' , 0, 70) self.btn_on = Button(x=0, y=170, w=200, h=40, label="Start tracking") self.btn_on.draw() self.btn_set = Button(x=201, y=170, w=38, h=40, label="S") self.btn_set.draw() - elif self._page == "SETTINGS": + elif self._page == b"SET": draw.string("Settings", 0, 0) self.btn_set_end = Button(x=201, y=0, w=38, h=40, label="X") self.btn_set_end.draw() @@ -272,7 +272,7 @@ class ZzzTrackerApp(): self.check_anti.draw() - if self._page != "SETTINGS": + if self._page != b"SET": self.stat_bar = StatusBar() self.stat_bar.clock = True self.stat_bar.draw() @@ -283,11 +283,11 @@ class ZzzTrackerApp(): self._disable_tracking() # get angle over time - f = open(self.filep, "r") + f = open(self.filep, "rb") lines = f.readlines() f.close() - if len(lines) == 1: - lines = lines[0].split("\n") + if b"\n" in lines: + lines = lines[0].split(b"\n") data = array("f", [float(line.split(",")[4]) for line in lines]) # center and scale @@ -324,12 +324,12 @@ class ZzzTrackerApp(): max(self._WU_t - self._earlier, int(rtc.time()) + 3), # not before right now self._WU_t - 5 # not after original wake up time ), self._listen_to_ticks) - self._page = "WAITING_EARLY_WU" + self._page = b"TRA2" def _listen_to_ticks(self): """listen to ticks every second, telling the watch to vibrate""" - self._page = "RINGING" + self._page = b"RNG" system.wake() system.keep_awake() system.switch(self) @@ -338,5 +338,5 @@ class ZzzTrackerApp(): def tick(self, ticks): """vibrate to wake you up""" - if self._page == "RINGING": + if self._page == b"RNG": watch.vibrator.pulse(duty=50, ms=500) From 5d4a8552b98940f7b44057926c3e01c3b622ac50 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:34:53 +0100 Subject: [PATCH 123/485] minor --- ZzzTracker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index ddc3e30..7769d0a 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -168,8 +168,7 @@ class ZzzTrackerApp(): def _disable_tracking(self, keep_alarm=False): """called by touching "STOP TRACKING" or when computing best alarm time - to wake up you - disables tracking features and alarms""" + to wake up you disables tracking features and alarms""" self._tracking = False system.cancel_alarm(self.next_al, self._trackOnce) if self._wakeup_enabled: @@ -308,7 +307,7 @@ class ZzzTrackerApp(): ) if fits[-1] == min(fits): best_offset = _OFFSETS[cnt] - del fits, offset, _OFFSETS, cnt + del fits, offset, cnt # finding how early to wake up: max_sin = 0 From 0789be903ddbf8899ecb749e8387f0bc89525e12 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:40:24 +0100 Subject: [PATCH 124/485] style --- ZzzTracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 7769d0a..b4d37ce 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -238,7 +238,7 @@ class ZzzTrackerApp(): word = " before " else: word = " at " - draw.string("Wake up {} {}".format(word, ":".join([str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]])), 0, 130) + draw.string("Wake up{}{}".format(word, ":".join([str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]])), 0, 130) self.btn_off = Button(x=0, y=170, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == b"STA": From e6745617df82ac3bdf995f39e9c45f55042e094b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:43:21 +0100 Subject: [PATCH 125/485] new: re adds gc.collect everywhere --- ZzzTracker.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index b4d37ce..8c84220 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -15,7 +15,7 @@ alarm you set up manually. import time from wasp import watch, system, EventMask -from watch import rtc, battery, accel +from watch import rtc, battery, accel, gc from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView from shell import mkdir, cd from fonts import sans18 @@ -33,6 +33,7 @@ class ZzzTrackerApp(): NAME = 'ZzzTrck' def __init__(self): + gc.collect() self._wakeup_enabled = 1 self._wakeup_ant_enabled = 1 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_ANTICIP self._freq = 5 # get accelerometer data every X seconds, they will be averaged @@ -58,15 +59,18 @@ class ZzzTrackerApp(): cd("..") def foreground(self): + gc.collect() self._draw() system.request_event(EventMask.TOUCH) def sleep(self): """keep running in the background""" + gc.collect() return False def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" + gc.collect() no_full_draw = False if self._page == b"STA": if self.btn_on.touch(event): @@ -177,6 +181,7 @@ class ZzzTrackerApp(): if self._wakeup_ant_enabled: system.cancel_alarm(self._WU_a, self._compute_best_WU) self._periodicSave() + gc.collect() def _add_accel_alar(self): """set an alarm, due in self._freq minutes, to log the accelerometer data @@ -194,6 +199,7 @@ class ZzzTrackerApp(): self._periodicSave() if battery.level() <= _BATTERY_THRESHOLD: self._disable_tracking(keep_alarm=True) + gc.collect() def _periodicSave(self): """save data after averageing over a window to file""" @@ -324,10 +330,12 @@ class ZzzTrackerApp(): self._WU_t - 5 # not after original wake up time ), self._listen_to_ticks) self._page = b"TRA2" + gc.collect() def _listen_to_ticks(self): """listen to ticks every second, telling the watch to vibrate""" + gc.collect() self._page = b"RNG" system.wake() system.keep_awake() From 40bf395c51032a1302458e4cde6063526a296e20 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:48:28 +0100 Subject: [PATCH 126/485] fix: wrong import of gc --- ZzzTracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 8c84220..7f37af0 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -13,9 +13,9 @@ alarm you set up manually. """ import time -from wasp import watch, system, EventMask +from wasp import watch, system, EventMask, gc -from watch import rtc, battery, accel, gc +from watch import rtc, battery, accel from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView from shell import mkdir, cd from fonts import sans18 From 66bd04ece0f40249d0c608c8d5f180490ea7a658 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:48:38 +0100 Subject: [PATCH 127/485] remove debug flag --- ZzzTracker.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 7f37af0..445e9a9 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -41,7 +41,6 @@ class ZzzTrackerApp(): self._wakeup_ant_latitude = 1800 # defaults 1800 = 30m self._spinval_H = 7 # default wake up time self._spinval_M = 30 - self._debug = False self._conf_view = None self._tracking = False # False = not tracking, True = currently tracking self._earlier = 0 @@ -85,11 +84,6 @@ class ZzzTrackerApp(): self.filep = "logs/sleep/{}.csv".format(str(self._offset)) self._add_accel_alar() - if self._debug: - self._freq = 1 - self._store_freq = 5 - self._wakeup_ant_latitude = 5 - # alarm in self._SL_L seconds after tracking started to wake you up self._SL_L = self._spinval_H*60*60 + self._spinval_M*60 if self._wakeup_enabled: @@ -157,12 +151,6 @@ class ZzzTrackerApp(): elif self.min.touch(event): self._spinval_M = self.min.value self.min.update() - elif self.check_debug.touch(event): - if self._debug: - self._debug = False - else: - self._debug = True - self.check_debug.update() elif self.btn_set_end.touch(event): self._page = b"STA" self._draw() @@ -265,9 +253,6 @@ class ZzzTrackerApp(): self.min.value = self._spinval_M self.min.draw() - self.check_debug = Checkbox(x=0, y=120, label="Debug?") - self.check_debug.state = self._debug - self.check_debug.draw() self.check_al = Checkbox(x=0, y=160, label="Alarm?") self.check_al.state = self._wakeup_enabled self.check_al.draw() From 49d8805a89af1358b471e1d2c2dcd93f4ddeb6d7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 12:57:12 +0100 Subject: [PATCH 128/485] new: store values as const instead of in the class --- ZzzTracker.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 445e9a9..7914b39 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -28,6 +28,9 @@ _FONT = sans18 _BATTERY_THRESHOLD = const(20) # under 20% of battery, stop tracking and only keep the alarm _AVG_SLEEP_CYCL = const(32400) # 90 minutes, average sleep cycle duration _OFFSETS = array("H", [0, 300, 600, 900, 1200, 1500, 1800]) +_FREQ = const(5) # get accelerometer data every X seconds, they will be averaged +_STORE_FREQ = const(300) # number of seconds between storing average values to file written every X points +_ANTICIP_LEN = const(1800) # defaults 1800 = 30m class ZzzTrackerApp(): NAME = 'ZzzTrck' @@ -36,9 +39,6 @@ class ZzzTrackerApp(): gc.collect() self._wakeup_enabled = 1 self._wakeup_ant_enabled = 1 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_ANTICIP - self._freq = 5 # get accelerometer data every X seconds, they will be averaged - self._store_freq = 300 # number of seconds between storing average values to file written every X points - self._wakeup_ant_latitude = 1800 # defaults 1800 = 30m self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._conf_view = None @@ -98,9 +98,9 @@ class ZzzTrackerApp(): self._WU_t = time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) system.set_alarm(self._WU_t, self._listen_to_ticks) - # alarm in self._wakeup_ant_latitude less seconds to compute best wake up time + # alarm in _ANTICIP_LEN less seconds to compute best wake up time if self._wakeup_ant_enabled: - self._WU_a = self._WU_t - self._wakeup_ant_latitude + self._WU_a = self._WU_t - _ANTICIP_LEN system.set_alarm(self._WU_a, self._compute_best_WU) self._page = b"TRA" elif self.btn_set.touch(event): @@ -172,14 +172,14 @@ class ZzzTrackerApp(): gc.collect() def _add_accel_alar(self): - """set an alarm, due in self._freq minutes, to log the accelerometer data + """set an alarm, due in _FREQ minutes, to log the accelerometer data once""" - self.next_al = watch.rtc.time() + self._freq + self.next_al = watch.rtc.time() + _FREQ system.set_alarm(self.next_al, self._trackOnce) def _trackOnce(self): - """get one data point of accelerometer every self._freq seconds and - they are then averaged and stored every self._store_freq seconds""" + """get one data point of accelerometer every _FREQ seconds and + they are then averaged and stored every _STORE_FREQ seconds""" if self._tracking: [self._buff.append(x) for x in accel.read_xyz()] self._data_point_nb += 1 @@ -191,7 +191,7 @@ class ZzzTrackerApp(): def _periodicSave(self): """save data after averageing over a window to file""" - if self._data_point_nb - self._last_checkpoint >= self._store_freq / self._freq: + if self._data_point_nb - self._last_checkpoint >= _STORE_FREQ / _FREQ: x_avg = sum([self._buff[i] for i in range(0, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) y_avg = sum([self._buff[i] for i in range(1, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) z_avg = sum([self._buff[i] for i in range(2, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) @@ -293,8 +293,8 @@ class ZzzTrackerApp(): omega = 2 * pi / _AVG_SLEEP_CYCL for cnt, offset in enumerate(_OFFSETS): # least square regression fits.append( - sum([sin(omega * t * self._store_freq + offset) * data[t] for t in range(len(data))]) - -sum([(sin(omega * t * self._store_freq + offset) - data[t])**2 for t in range(len(data))]) + sum([sin(omega * t * _STORE_FREQ + offset) * data[t] for t in range(len(data))]) + -sum([(sin(omega * t * _STORE_FREQ + offset) - data[t])**2 for t in range(len(data))]) ) if fits[-1] == min(fits): best_offset = _OFFSETS[cnt] @@ -302,7 +302,7 @@ class ZzzTrackerApp(): # finding how early to wake up: max_sin = 0 - for t in range(self._WU_t, self._WU_t - self._wakeup_ant_latitude, -300): # counting backwards from original wake up time, steps of 5 minutes + for t in range(self._WU_t, self._WU_t - _ANTICIP_LEN, -300): # counting backwards from original wake up time, steps of 5 minutes s = sin(omega * t + best_offset) if s > max_sin: max_sin = s From 656799882fca4508b2b719f3455072c6dc9d2704 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 13:01:23 +0100 Subject: [PATCH 129/485] new: cache objet reference for better performance --- ZzzTracker.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/ZzzTracker.py b/ZzzTracker.py index 7914b39..f94f853 100644 --- a/ZzzTracker.py +++ b/ZzzTracker.py @@ -191,21 +191,22 @@ class ZzzTrackerApp(): def _periodicSave(self): """save data after averageing over a window to file""" + buff = self._buff if self._data_point_nb - self._last_checkpoint >= _STORE_FREQ / _FREQ: - x_avg = sum([self._buff[i] for i in range(0, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) - y_avg = sum([self._buff[i] for i in range(1, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) - z_avg = sum([self._buff[i] for i in range(2, len(self._buff), 3)]) / (self._data_point_nb - self._last_checkpoint) - self._buff = array("f") # reseting array - self._buff.append(int(rtc.time() - self._offset)) - self._buff.append(x_avg) - self._buff.append(y_avg) - self._buff.append(z_avg) - self._buff.append(degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001)))) # formula from https://www.nature.com/articles/s41598-018-31266-z + x_avg = sum([buff[i] for i in range(0, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) + y_avg = sum([buff[i] for i in range(1, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) + z_avg = sum([buff[i] for i in range(2, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) + buff = array("f") # reseting array + buff.append(int(rtc.time() - self._offset)) + buff.append(x_avg) + buff.append(y_avg) + buff.append(z_avg) + buff.append(degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001)))) # formula from https://www.nature.com/articles/s41598-018-31266-z del x_avg, y_avg, z_avg - self._buff.append(battery.voltage_mv()) # currently more accurate than percent + buff.append(battery.voltage_mv()) # currently more accurate than percent f = open(self.filep, "ab") - f.write(b",".join([str(x)[0:8].encode() for x in self._buff]) + b"\n") + f.write(b",".join([str(x)[0:8].encode() for x in buff]) + b"\n") f.close() self._last_checkpoint = self._data_point_nb @@ -302,7 +303,8 @@ class ZzzTrackerApp(): # finding how early to wake up: max_sin = 0 - for t in range(self._WU_t, self._WU_t - _ANTICIP_LEN, -300): # counting backwards from original wake up time, steps of 5 minutes + WU_t = self._WU_t + for t in range(WU_t, WU_t - _ANTICIP_LEN, -300): # counting backwards from original wake up time, steps of 5 minutes s = sin(omega * t + best_offset) if s > max_sin: max_sin = s @@ -311,8 +313,8 @@ class ZzzTrackerApp(): system.set_alarm( min( - max(self._WU_t - self._earlier, int(rtc.time()) + 3), # not before right now - self._WU_t - 5 # not after original wake up time + max(WU_t - self._earlier, int(rtc.time()) + 3), # not before right now + WU_t - 5 # not after original wake up time ), self._listen_to_ticks) self._page = b"TRA2" gc.collect() From 7b8dcd94e7488fe29c8a78c315882cb4cf2cd895 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 13:50:20 +0100 Subject: [PATCH 130/485] renameed to SleepTk --- README.md | 16 ++++++++-------- ZzzTracker.py => SleepTk.py | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) rename ZzzTracker.py => SleepTk.py (99%) diff --git a/README.md b/README.md index f2823d1..890e693 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) * Status as of February 23rd: * First version finished, the waking up algorithm is not at all tested - * **Instructions**: (with a forked wasp-os that exposes accel data : [link](https://github.com/thiswillbeyourgithub/wasp-os)) - * get the latest python file : ZzzTracker.py - * compile it : `./micropython/mpy-cross/mpy-cross -mno-unicode -march=armv7m ZzzTracker.py` - * send compiled : `./tools/wasptool --verbose --upload ZzzTracker.mpy --as apps/ZzzTracker.mpy --binary` - * register compiled : `./tools/wasptool --verbose --eval "wasp.system.register('apps.ZzzTracker.ZzzTrackerApp')` - * run it! - * if you want, get back the data using `wasptool --pull` - * take a look at it using pandas, for example using : ` df = pd.read_csv("./first.night.csv", names=["time", "x_avg", "y_avg", "z_avg", "angl_avg", "battery"])` (name and number of columns might change) + * **Instructions**: + *(for now you need my forked wasp-os that exposes accelerometer data* + * download [my wasp-os fork](https://github.com/thiswillbeyourgithub/wasp-os) + * download the latest app : SleepTk.py + * put the latest app in wasp-os/wasp/apps/SleepTk.py + * compile and install wasp-os + * run the app + * *if you want, you can get back the data using `wasptool --pull`, to take a look using pandas : ` df = pd.read_csv("./first.night.csv", names=["time", "x_avg", "y_avg", "z_avg", "angl_avg", "battery"])` (name and number of columns might change)* ## TODO **sleep tracking** diff --git a/ZzzTracker.py b/SleepTk.py similarity index 99% rename from ZzzTracker.py rename to SleepTk.py index f94f853..92007b4 100644 --- a/ZzzTracker.py +++ b/SleepTk.py @@ -6,7 +6,7 @@ # https://github.com/thiswillbeyourgithub/sleep_tracker_pinetime_wasp-os -This app is designed to track accelerometer data throughout the night. It can +SleepTk is designed to track accelerometer data throughout the night. It can also compute the best time to wake you up, up to 30 minutes before the alarm you set up manually. @@ -32,8 +32,8 @@ _FREQ = const(5) # get accelerometer data every X seconds, they will be average _STORE_FREQ = const(300) # number of seconds between storing average values to file written every X points _ANTICIP_LEN = const(1800) # defaults 1800 = 30m -class ZzzTrackerApp(): - NAME = 'ZzzTrck' +class SleepTkApp(): + NAME = 'SleepTk' def __init__(self): gc.collect() From 96aa9a6fae29eb6600f6b82941e1ffaa66fd87a6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 13:54:32 +0100 Subject: [PATCH 131/485] put buttons a bit lower --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 92007b4..075d58c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -234,13 +234,13 @@ class SleepTkApp(): else: word = " at " draw.string("Wake up{}{}".format(word, ":".join([str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]])), 0, 130) - self.btn_off = Button(x=0, y=170, w=240, h=40, label="Stop tracking") + self.btn_off = Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == b"STA": draw.string('Sleep tracker' , 0, 70) - self.btn_on = Button(x=0, y=170, w=200, h=40, label="Start tracking") + self.btn_on = Button(x=0, y=200, w=200, h=40, label="Start tracking") self.btn_on.draw() - self.btn_set = Button(x=201, y=170, w=38, h=40, label="S") + self.btn_set = Button(x=201, y=200, w=38, h=40, label="S") self.btn_set.draw() elif self._page == b"SET": draw.string("Settings", 0, 0) From 9c6097d2db85215e70ad4c40c2ed0ce988b3f4e7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 14:30:49 +0100 Subject: [PATCH 132/485] new: way better gui --- SleepTk.py | 104 +++++++++++++++++++++++++++++------------------------ 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 075d58c..4b0910a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -30,7 +30,7 @@ _AVG_SLEEP_CYCL = const(32400) # 90 minutes, average sleep cycle duration _OFFSETS = array("H", [0, 300, 600, 900, 1200, 1500, 1800]) _FREQ = const(5) # get accelerometer data every X seconds, they will be averaged _STORE_FREQ = const(300) # number of seconds between storing average values to file written every X points -_ANTICIP_LEN = const(1800) # defaults 1800 = 30m +_SMART_LEN = const(1800) # defaults 1800 = 30m class SleepTkApp(): NAME = 'SleepTk' @@ -38,7 +38,7 @@ class SleepTkApp(): def __init__(self): gc.collect() self._wakeup_enabled = 1 - self._wakeup_ant_enabled = 1 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_ANTICIP + self._wakeup_smart_enabled = 1 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._conf_view = None @@ -98,9 +98,9 @@ class SleepTkApp(): self._WU_t = time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) system.set_alarm(self._WU_t, self._listen_to_ticks) - # alarm in _ANTICIP_LEN less seconds to compute best wake up time - if self._wakeup_ant_enabled: - self._WU_a = self._WU_t - _ANTICIP_LEN + # alarm in _SMART_LEN less seconds to compute best wake up time + if self._wakeup_smart_enabled: + self._WU_a = self._WU_t - _SMART_LEN system.set_alarm(self._WU_a, self._compute_best_WU) self._page = b"TRA" elif self.btn_set.touch(event): @@ -126,31 +126,39 @@ class SleepTkApp(): elif self._page == b"SET": no_full_draw = True - disable_both = False + disable_all = False if self.check_al.touch(event): if self._wakeup_enabled == 1: self._wakeup_enabled = 0 - disable_both = True + disable_all = True else: self._wakeup_enabled = 1 + no_full_draw = False self.check_al.state = self._wakeup_enabled self.check_al.update() - if self.check_anti.touch(event) or disable_both: - if self._wakeup_ant_enabled == 1 or disable_both: - self._wakeup_ant_enabled = 0 - self.check_anti.state = self._wakeup_ant_enabled - self._check_anti = None + + if disable_all: + self._wakeup_smart_enabled = 0 + self.check_smart.state = self._wakeup_smart_enabled + self._check_smart = None self._draw() + + elif self.check_smart.touch(event): + if self._wakeup_smart_enabled == 1: + self._wakeup_smart_enabled = 0 + self.check_smart.state = self._wakeup_smart_enabled + self._check_smart = None elif self._wakeup_enabled == 1: - self._wakeup_ant_enabled = 1 - self.check_anti.state = self._wakeup_ant_enabled - self.check_anti.update() - elif self.hours.touch(event): - self._spinval_H = self.hours.value - self.hours.update() - elif self.min.touch(event): - self._spinval_M = self.min.value - self.min.update() + self._wakeup_smart_enabled = 1 + self.check_smart.state = self._wakeup_smart_enabled + self.check_smart.update() + self.check_smart.draw() + elif self._spin_H.touch(event): + self._spinval_H = self._spin_H.value + self._spin_H.update() + elif self._spin_M.touch(event): + self._spinval_M = self._spin_M.value + self._spin_M.update() elif self.btn_set_end.touch(event): self._page = b"STA" self._draw() @@ -166,7 +174,7 @@ class SleepTkApp(): if self._wakeup_enabled: if keep_alarm is False: # to keep the alarm when stopping because of low battery system.cancel_alarm(self._WU_t, self._listen_to_ticks) - if self._wakeup_ant_enabled: + if self._wakeup_smart_enabled: system.cancel_alarm(self._WU_a, self._compute_best_WU) self._periodicSave() gc.collect() @@ -229,44 +237,46 @@ class SleepTkApp(): draw.string('Started at {}'.format(":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]])), 0, 70) draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) if self._wakeup_enabled: - if self._wakeup_ant_enabled: - word = " before " - else: - word = " at " - draw.string("Wake up{}{}".format(word, ":".join([str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]])), 0, 130) + word = "Alarm at" + if self._wakeup_smart_enabled: + word = "Alarm before " + ti = [str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]] + draw.string("{:2}{:2}".format(word, ":".join(ti)), 0, 130) self.btn_off = Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == b"STA": - draw.string('Sleep tracker' , 0, 70) + draw.string('Sleep tracker with' , 0, 60) + draw.string('alarm and smart alarm.' , 0, 80) + draw.string('Wake you up to 30m' , 0, 100) + draw.string('before alarm.' , 0, 120) + draw.string('ALPHA SOFTWARE.' , 0, 140) self.btn_on = Button(x=0, y=200, w=200, h=40, label="Start tracking") self.btn_on.draw() - self.btn_set = Button(x=201, y=200, w=38, h=40, label="S") + self.btn_set = Button(x=201, y=200, w=39, h=40, label="S") self.btn_set.draw() elif self._page == b"SET": - draw.string("Settings", 0, 0) - self.btn_set_end = Button(x=201, y=0, w=38, h=40, label="X") + self.btn_set_end = Button(x=201, y=200, w=39, h=40, label="X") self.btn_set_end.draw() - self.hours = Spinner(0, 5, 0, 23, 2) - self.hours.value = self._spinval_H - self.hours.draw() - self.min = Spinner(60, 5, 0, 59, 2) - self.min.value = self._spinval_M - self.min.draw() + if self._wakeup_enabled: + self._spin_H = Spinner(10, 140, 0, 23, 2) + self._spin_H.value = self._spinval_H + self._spin_H.draw() + self._spin_M = Spinner(100, 140, 0, 59, 2) + self._spin_M.value = self._spinval_M + self._spin_M.draw() - self.check_al = Checkbox(x=0, y=160, label="Alarm?") + self.check_al = Checkbox(x=0, y=40, label="Alarm") self.check_al.state = self._wakeup_enabled self.check_al.draw() if self.check_al.state == 1: - self.check_anti = Checkbox(x=0, y=200, label="Anticipate?") - self.check_anti.state = self._wakeup_ant_enabled - self.check_anti.draw() + self.check_smart = Checkbox(x=0, y=80, label="Smart alarm") + self.check_smart.state = self._wakeup_smart_enabled + self.check_smart.draw() - - if self._page != b"SET": - self.stat_bar = StatusBar() - self.stat_bar.clock = True - self.stat_bar.draw() + self.stat_bar = StatusBar() + self.stat_bar.clock = True + self.stat_bar.draw() def _compute_best_WU(self): """computes best wake up time from sleep data""" @@ -304,7 +314,7 @@ class SleepTkApp(): # finding how early to wake up: max_sin = 0 WU_t = self._WU_t - for t in range(WU_t, WU_t - _ANTICIP_LEN, -300): # counting backwards from original wake up time, steps of 5 minutes + for t in range(WU_t, WU_t - _SMART_LEN, -300): # counting backwards from original wake up time, steps of 5 minutes s = sin(omega * t + best_offset) if s > max_sin: max_sin = s From 9c859ce20c0fb57e14dcde8780d75a2f0740e04a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 14:33:41 +0100 Subject: [PATCH 133/485] docs --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 890e693..69d279b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# Waspos Sleep Tracker -**Goal:** sleep tracker for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os), that wakes you up at the best time. +# SleepTk : a sleep tracker and smart alarm for wasp-os +**Goal:** sleep tracker and smart alarm for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os), that wakes you up at the best time. ## Note to reader: * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) -* Status as of February 23rd: - * First version finished, the waking up algorithm is not at all tested +* Status as of February 2022: + * Finished but the smart algorithm is not at all tested yet * **Instructions**: - *(for now you need my forked wasp-os that exposes accelerometer data* + *(for now you need my forked wasp-os that exposes accelerometer data) * download [my wasp-os fork](https://github.com/thiswillbeyourgithub/wasp-os) * download the latest app : SleepTk.py * put the latest app in wasp-os/wasp/apps/SleepTk.py From d805186108fef63b4c6301fb30d9695cd7e59b7c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 14:40:04 +0100 Subject: [PATCH 134/485] new: added screenshots --- README.md | 5 +++++ screenshots/settings_page.png | Bin 0 -> 6847 bytes screenshots/start_page.png | Bin 0 -> 7733 bytes screenshots/tracking_page.png | Bin 0 -> 6680 bytes 4 files changed, 5 insertions(+) create mode 100644 screenshots/settings_page.png create mode 100644 screenshots/start_page.png create mode 100644 screenshots/tracking_page.png diff --git a/README.md b/README.md index 69d279b..731968f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ * run the app * *if you want, you can get back the data using `wasptool --pull`, to take a look using pandas : ` df = pd.read_csv("./first.night.csv", names=["time", "x_avg", "y_avg", "z_avg", "angl_avg", "battery"])` (name and number of columns might change)* +# Screenshots: +![start](./screenshots/start_page.png) +![settings](./screenshots/settings_page.png) +![tracking](./screenshots/tracking_page.png) + ## TODO **sleep tracking** * try to roughly infer the sleep stage directly on the device? diff --git a/screenshots/settings_page.png b/screenshots/settings_page.png new file mode 100644 index 0000000000000000000000000000000000000000..5f25e99239922a267f235b3ca9d97a03444b77de GIT binary patch literal 6847 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 A0ssI2 literal 0 HcmV?d00001 diff --git a/screenshots/tracking_page.png b/screenshots/tracking_page.png new file mode 100644 index 0000000000000000000000000000000000000000..519bc747de00b47eba6e8e06bebed8175c61f5c7 GIT binary patch literal 6680 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 literal 0 HcmV?d00001 From f3785f7bc06851e9047501e923fd9f80f1959db0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 16:46:38 +0100 Subject: [PATCH 135/485] minor: styling --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4b0910a..042e8ed 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -237,7 +237,7 @@ class SleepTkApp(): draw.string('Started at {}'.format(":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]])), 0, 70) draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) if self._wakeup_enabled: - word = "Alarm at" + word = "Alarm at " if self._wakeup_smart_enabled: word = "Alarm before " ti = [str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]] From a7464ed69e859d09ccfd93789f7237ea46432f43 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 16:59:44 +0100 Subject: [PATCH 136/485] fix: cannot stop tracking if pressed back button in confirmation view --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index 042e8ed..27d789a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -58,6 +58,7 @@ class SleepTkApp(): cd("..") def foreground(self): + self._conf_view = None gc.collect() self._draw() system.request_event(EventMask.TOUCH) From b2667c1343fe6169c94a334e753fa57fe0f351f6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 17:00:20 +0100 Subject: [PATCH 137/485] fix: removed unused SL_L variable + fix wrong alarm time displayed --- SleepTk.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 27d789a..8aa37aa 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -85,8 +85,7 @@ class SleepTkApp(): self.filep = "logs/sleep/{}.csv".format(str(self._offset)) self._add_accel_alar() - # alarm in self._SL_L seconds after tracking started to wake you up - self._SL_L = self._spinval_H*60*60 + self._spinval_M*60 + # setting up alarm if self._wakeup_enabled: now = rtc.get_localtime() yyyy = now[0] @@ -241,7 +240,7 @@ class SleepTkApp(): word = "Alarm at " if self._wakeup_smart_enabled: word = "Alarm before " - ti = [str(x) for x in watch.time.localtime(self._offset + self._SL_L)[3:5]] + ti = [str(x) for x in watch.time.localtime(self._WU_t)[3:5]] draw.string("{:2}{:2}".format(word, ":".join(ti)), 0, 130) self.btn_off = Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() From fb6b9a46d10423611682139ef05a8330379ebf9e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 17:14:25 +0100 Subject: [PATCH 138/485] minor: style --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 8aa37aa..34804b5 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -166,13 +166,13 @@ class SleepTkApp(): if no_full_draw is False: self._draw() - def _disable_tracking(self, keep_alarm=False): + def _disable_tracking(self, keep_main_alarm=False): """called by touching "STOP TRACKING" or when computing best alarm time to wake up you disables tracking features and alarms""" self._tracking = False system.cancel_alarm(self.next_al, self._trackOnce) if self._wakeup_enabled: - if keep_alarm is False: # to keep the alarm when stopping because of low battery + if keep_main_alarm is False: # to keep the alarm when stopping because of low battery system.cancel_alarm(self._WU_t, self._listen_to_ticks) if self._wakeup_smart_enabled: system.cancel_alarm(self._WU_a, self._compute_best_WU) @@ -194,7 +194,7 @@ class SleepTkApp(): self._add_accel_alar() self._periodicSave() if battery.level() <= _BATTERY_THRESHOLD: - self._disable_tracking(keep_alarm=True) + self._disable_tracking(keep_main_alarm=True) gc.collect() def _periodicSave(self): From 4b3767851c5c0b064262fcf3ad832f1c2cea9629 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 17:14:52 +0100 Subject: [PATCH 139/485] notification when battery low + fix setting value of smart alarm flag --- SleepTk.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 34804b5..02d9119 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -195,6 +195,14 @@ class SleepTkApp(): self._periodicSave() if battery.level() <= _BATTERY_THRESHOLD: self._disable_tracking(keep_main_alarm=True) + self._wakeup_smart_enabled = 0 + h, m = watch.time.localtime(time.time())[3:5] + system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", + "title": "Bat <20%", + "body": "Stopped \ +tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ +on.".format(h, m, _BATTERY_THRESHOLD)}) + gc.collect() def _periodicSave(self): From 3335d8a3716fb0f835b8e4e2ccaecea7d2328170 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 17:21:44 +0100 Subject: [PATCH 140/485] fix: can refresh tracking screen by clicking outside of buttons --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 02d9119..108eb9b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -108,10 +108,10 @@ class SleepTkApp(): elif self._page.startswith(b"TRA"): if self._conf_view is None: - no_full_draw = True if self.btn_off.touch(event): self._conf_view = ConfirmationView() self._conf_view.draw("Stop tracking?") + no_full_draw = True else: if self._conf_view.touch(event): if self._conf_view.value: From 711d594d2dac952d8b38861b750405f71b466dff Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 17:49:53 +0100 Subject: [PATCH 141/485] new: store _page value as byte instead of string --- SleepTk.py | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 108eb9b..2d0559c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -32,6 +32,13 @@ _FREQ = const(5) # get accelerometer data every X seconds, they will be average _STORE_FREQ = const(300) # number of seconds between storing average values to file written every X points _SMART_LEN = const(1800) # defaults 1800 = 30m +# page values: +_START = const(0) +_TRACKING = const(1) +_TRACKING2 = const(2) +_SETTINGS = const(3) +_RINGING = const(4) + class SleepTkApp(): NAME = 'SleepTk' @@ -44,7 +51,7 @@ class SleepTkApp(): self._conf_view = None self._tracking = False # False = not tracking, True = currently tracking self._earlier = 0 - self._page = b"STA" # can be START / TRACKING / TRA2 = tracking but with early wake up time computed / SETTINGS / RINGING + self._page = _START try: mkdir("logs/") @@ -72,7 +79,7 @@ class SleepTkApp(): """either start trackign or disable it, draw the screen in all cases""" gc.collect() no_full_draw = False - if self._page == b"STA": + if self._page == _START: if self.btn_on.touch(event): self._tracking = True # accel data not yet written to disk: @@ -102,11 +109,11 @@ class SleepTkApp(): if self._wakeup_smart_enabled: self._WU_a = self._WU_t - _SMART_LEN system.set_alarm(self._WU_a, self._compute_best_WU) - self._page = b"TRA" + self._page = _TRACKING elif self.btn_set.touch(event): - self._page = b"SET" + self._page = _SETTINGS - elif self._page.startswith(b"TRA"): + elif self._page == _TRACKING or self._page == _TRACKING2: if self._conf_view is None: if self.btn_off.touch(event): self._conf_view = ConfirmationView() @@ -116,15 +123,15 @@ class SleepTkApp(): if self._conf_view.touch(event): if self._conf_view.value: self._disable_tracking() - self._page = b"STA" + self._page = _START self._conf_view = None - elif self._page == b"RNG": + elif self._page == _RINGING: if self.btn_al.touch(event): self._disable_tracking() - self._page = b"STA" + self._page = _START - elif self._page == b"SET": + elif self._page == _SETTINGS: no_full_draw = True disable_all = False if self.check_al.touch(event): @@ -160,7 +167,7 @@ class SleepTkApp(): self._spinval_M = self._spin_M.value self._spin_M.update() elif self.btn_set_end.touch(event): - self._page = b"STA" + self._page = _START self._draw() if no_full_draw is False: @@ -233,7 +240,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) draw = watch.drawable draw.fill(0) draw.set_font(_FONT) - if self._page == b"RNG": + if self._page == _RINGING: if self._earlier != 0: msg = "WAKE UP ({}m early)".format(str(self._earlier/60)[0:2]) else: @@ -241,7 +248,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) draw.string(msg, 0, 70) self.btn_al = Button(x=0, y=170, w=240, h=40, label="STOP") self.btn_al.draw() - elif self._page.startswith(b"TRA"): + elif self._page == _TRACKING or self._page == _TRACKING2: draw.string('Started at {}'.format(":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]])), 0, 70) draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) if self._wakeup_enabled: @@ -252,7 +259,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) draw.string("{:2}{:2}".format(word, ":".join(ti)), 0, 130) self.btn_off = Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() - elif self._page == b"STA": + elif self._page == _START: draw.string('Sleep tracker with' , 0, 60) draw.string('alarm and smart alarm.' , 0, 80) draw.string('Wake you up to 30m' , 0, 100) @@ -262,7 +269,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self.btn_on.draw() self.btn_set = Button(x=201, y=200, w=39, h=40, label="S") self.btn_set.draw() - elif self._page == b"SET": + elif self._page == _SETTINGS: self.btn_set_end = Button(x=201, y=200, w=39, h=40, label="X") self.btn_set_end.draw() @@ -334,14 +341,14 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) max(WU_t - self._earlier, int(rtc.time()) + 3), # not before right now WU_t - 5 # not after original wake up time ), self._listen_to_ticks) - self._page = b"TRA2" + self._page = _TRACKING2 gc.collect() def _listen_to_ticks(self): """listen to ticks every second, telling the watch to vibrate""" gc.collect() - self._page = b"RNG" + self._page = _RINGING system.wake() system.keep_awake() system.switch(self) @@ -350,5 +357,5 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def tick(self, ticks): """vibrate to wake you up""" - if self._page == b"RNG": + if self._page == _RINGING: watch.vibrator.pulse(duty=50, ms=500) From f16fdcb13cc6afb7f2b5360c13d6d731597c2156 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 24 Feb 2022 21:05:31 +0100 Subject: [PATCH 142/485] fix: type error --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 2d0559c..fb6ce5a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -302,7 +302,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f = open(self.filep, "rb") lines = f.readlines() f.close() - if b"\n" in lines: + if len(lines) == 0: lines = lines[0].split(b"\n") data = array("f", [float(line.split(",")[4]) for line in lines]) From 5ae59ec92fb372003dd8109f722879bce95e0f15 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 09:36:32 +0100 Subject: [PATCH 143/485] docs: mention alarm is working and stable --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 731968f..0a8029e 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ ## Note to reader: * I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) -* Status as of February 2022: - * Finished but the smart algorithm is not at all tested yet +* Status as of end of February 2022: + * Finished the UI and the alarm but the smart alarm implementation is not at all tested. * **Instructions**: *(for now you need my forked wasp-os that exposes accelerometer data) * download [my wasp-os fork](https://github.com/thiswillbeyourgithub/wasp-os) From 3808d55e1ebf4c6df17af17762f34b596338c1fc Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:02:56 +0100 Subject: [PATCH 144/485] fix: more efficient and good looking string writing --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index fb6ce5a..eb1131c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -255,8 +255,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) word = "Alarm at " if self._wakeup_smart_enabled: word = "Alarm before " - ti = [str(x) for x in watch.time.localtime(self._WU_t)[3:5]] - draw.string("{:2}{:2}".format(word, ":".join(ti)), 0, 130) + ti = watch.time.localtime(self._WU_t) + draw.string("{}{:02d}:{:02d}".format(word, ti[3], ti[4]), 0, 130) self.btn_off = Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == _START: From e8dc9c04ed97adb175652bc5ad27b2de4a0cb0fd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:28:31 +0100 Subject: [PATCH 145/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0a8029e..b4bf161 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ * if you actually use the watch during the night, make sure to count it as wakefulness? **misc** +* the settings should be a scrolled page * turn off the Bluetooth connection when no phone is connected? * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. * find a way to remove outliers of stored values From 6117b10e5d7ab9852e7f8b7d5556cb674286ebf3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:28:43 +0100 Subject: [PATCH 146/485] misc: added comment --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index eb1131c..977582f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -201,6 +201,7 @@ class SleepTkApp(): self._add_accel_alar() self._periodicSave() if battery.level() <= _BATTERY_THRESHOLD: + # strop tracking if battery low self._disable_tracking(keep_main_alarm=True) self._wakeup_smart_enabled = 0 h, m = watch.time.localtime(time.time())[3:5] From a2c1ec6b97a0c9862eecc5a70a92e274f704f199 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:29:18 +0100 Subject: [PATCH 147/485] new: wrappeed smart alarm in try block, write to file if exception, does NOT cut original alarm clock --- SleepTk.py | 96 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 977582f..ebaf781 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -296,53 +296,65 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _compute_best_WU(self): """computes best wake up time from sleep data""" - # stop tracking to save memory - self._disable_tracking() + try: + # stop tracking to save memory, keep the alarm just in case + self._disable_tracking(keep_main_alarm=True) - # get angle over time - f = open(self.filep, "rb") - lines = f.readlines() - f.close() - if len(lines) == 0: - lines = lines[0].split(b"\n") - data = array("f", [float(line.split(",")[4]) for line in lines]) + # get angle over time + f = open(self.filep, "rb") + lines = f.readlines() + f.close() + if len(lines) == 0: + lines = lines[0].split(b"\n") + data = array("f", [float(line.split(",")[4]) for line in lines]) - # center and scale - mean = sum(data) / len(data) - std = sqrt((sum([x**2 for x in data]) / len(data)) - pow(mean, 2)) - for i in range(len(data)): - data[i] = (data[i] - mean) / std - del mean, std + # center and scale + mean = sum(data) / len(data) + std = sqrt((sum([x**2 for x in data]) / len(data)) - pow(mean, 2)) + for i in range(len(data)): + data[i] = (data[i] - mean) / std + del mean, std - # fitting cosine of various offsets in minutes, the best fit has the - # period indicating best wake up time: - fits = array("f") - omega = 2 * pi / _AVG_SLEEP_CYCL - for cnt, offset in enumerate(_OFFSETS): # least square regression - fits.append( - sum([sin(omega * t * _STORE_FREQ + offset) * data[t] for t in range(len(data))]) - -sum([(sin(omega * t * _STORE_FREQ + offset) - data[t])**2 for t in range(len(data))]) - ) - if fits[-1] == min(fits): - best_offset = _OFFSETS[cnt] - del fits, offset, cnt + # fitting cosine of various offsets in minutes, the best fit has the + # period indicating best wake up time: + fits = array("f") + omega = 2 * pi / _AVG_SLEEP_CYCL + for cnt, offset in enumerate(_OFFSETS): # least square regression + fits.append( + sum([sin(omega * t * _STORE_FREQ + offset) * data[t] for t in range(len(data))]) + -sum([(sin(omega * t * _STORE_FREQ + offset) - data[t])**2 for t in range(len(data))]) + ) + if fits[-1] == min(fits): + best_offset = _OFFSETS[cnt] + del fits, offset, cnt - # finding how early to wake up: - max_sin = 0 - WU_t = self._WU_t - for t in range(WU_t, WU_t - _SMART_LEN, -300): # counting backwards from original wake up time, steps of 5 minutes - s = sin(omega * t + best_offset) - if s > max_sin: - max_sin = s - self._earlier = -t # number of seconds earlier than wake up time - del max_sin, s + # finding how early to wake up: + max_sin = 0 + WU_t = self._WU_t + for t in range(WU_t, WU_t - _SMART_LEN, -300): # counting backwards from original wake up time, steps of 5 minutes + s = sin(omega * t + best_offset) + if s > max_sin: + max_sin = s + self._earlier = -t # number of seconds earlier than wake up time + del max_sin, s - system.set_alarm( - min( - max(WU_t - self._earlier, int(rtc.time()) + 3), # not before right now - WU_t - 5 # not after original wake up time - ), self._listen_to_ticks) - self._page = _TRACKING2 + system.set_alarm( + min( + WU_t - 5, # not after original wake up time + max(WU_t - self._earlier, int(rtc.time()) + 3) # not before right now + ), self._listen_to_ticks) + self._page = _TRACKING2 + gc.collect() + except Exception as e: + gc.collect() + t = watch.time.localtime(time.time()) + msg = "Exception occured at {}h{}m: '{}'%".format(t[3], t[4], str(e))) + system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", + "title": "Smart alarm error", + "body": msg}) + f = open("smart_alarm_error_{}.txt".format(int(time.time())), "wb") + f.write(msg.encode()) + f.close() gc.collect() From 72701221a87ee768e5bb288bb368687a206ee5be Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:30:07 +0100 Subject: [PATCH 148/485] renamed compute_best_WU to smart_alarm_compute --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index ebaf781..a0e83b7 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -108,7 +108,7 @@ class SleepTkApp(): # alarm in _SMART_LEN less seconds to compute best wake up time if self._wakeup_smart_enabled: self._WU_a = self._WU_t - _SMART_LEN - system.set_alarm(self._WU_a, self._compute_best_WU) + system.set_alarm(self._WU_a, self._smart_alarm_compute) self._page = _TRACKING elif self.btn_set.touch(event): self._page = _SETTINGS @@ -182,7 +182,7 @@ class SleepTkApp(): if keep_main_alarm is False: # to keep the alarm when stopping because of low battery system.cancel_alarm(self._WU_t, self._listen_to_ticks) if self._wakeup_smart_enabled: - system.cancel_alarm(self._WU_a, self._compute_best_WU) + system.cancel_alarm(self._WU_a, self._smart_alarm_compute) self._periodicSave() gc.collect() @@ -294,7 +294,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self.stat_bar.clock = True self.stat_bar.draw() - def _compute_best_WU(self): + def _smart_alarm_compute(self): """computes best wake up time from sleep data""" try: # stop tracking to save memory, keep the alarm just in case From e423e8501fe8d7f9d2079dfa85fbce656449579b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:35:53 +0100 Subject: [PATCH 149/485] style: renamed button STOP to WAKE UP --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index a0e83b7..4a442c5 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -247,7 +247,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) else: msg = "WAKE UP" draw.string(msg, 0, 70) - self.btn_al = Button(x=0, y=170, w=240, h=40, label="STOP") + self.btn_al = Button(x=0, y=70, w=240, h=140, label="WAKE UP") self.btn_al.draw() elif self._page == _TRACKING or self._page == _TRACKING2: draw.string('Started at {}'.format(":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]])), 0, 70) From 86ea5d5b22a6d2864ed3dca3d7daa965251c8f9e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:36:29 +0100 Subject: [PATCH 150/485] more efficient and easy to read start time printing --- SleepTk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4a442c5..31120ab 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -250,7 +250,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self.btn_al = Button(x=0, y=70, w=240, h=140, label="WAKE UP") self.btn_al.draw() elif self._page == _TRACKING or self._page == _TRACKING2: - draw.string('Started at {}'.format(":".join([str(x) for x in watch.time.localtime(self._offset)[3:5]])), 0, 70) + ti = watch.time.localtime(self._offset) + draw.string('Started at {:2d}:{:2d}'.format(ti[3], ti[4]), 0, 70) draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) if self._wakeup_enabled: word = "Alarm at " From 67d145d7d0123835db60de6b6fecafb95094a502 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:36:45 +0100 Subject: [PATCH 151/485] PRE RELEASE instead of ALPHA software --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 31120ab..3436294 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -266,7 +266,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) draw.string('alarm and smart alarm.' , 0, 80) draw.string('Wake you up to 30m' , 0, 100) draw.string('before alarm.' , 0, 120) - draw.string('ALPHA SOFTWARE.' , 0, 140) + draw.string('PRE RELEASE.' , 0, 160) self.btn_on = Button(x=0, y=200, w=200, h=40, label="Start tracking") self.btn_on.draw() self.btn_set = Button(x=201, y=200, w=39, h=40, label="S") From 65d5e9eff53fb75c49fef017bd832ddd900af4a0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:36:51 +0100 Subject: [PATCH 152/485] fix: missing ) --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 3436294..643b9be 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -349,7 +349,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) except Exception as e: gc.collect() t = watch.time.localtime(time.time()) - msg = "Exception occured at {}h{}m: '{}'%".format(t[3], t[4], str(e))) + msg = "Exception occured at {}h{}m: '{}'%".format(t[3], t[4], str(e)) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Smart alarm error", "body": msg}) From 5f2d19716d281a805d5f3c15740e71fd507a58ff Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:41:27 +0100 Subject: [PATCH 153/485] new: can turn ringing off by pressing physical button --- SleepTk.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 643b9be..81663b2 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -68,13 +68,21 @@ class SleepTkApp(): self._conf_view = None gc.collect() self._draw() - system.request_event(EventMask.TOUCH) + system.request_event(EventMask.TOUCH | + EventMask.BUTTON) def sleep(self): """keep running in the background""" gc.collect() return False + def press(self, button, state): + "stop ringing alarm if pressed physical button" + if state: + if self._page == _RINGING: + self._disable_tracking() + self._page = _START + def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" gc.collect() From b0a44fd56fbe919ee4ef3b416ede74156c5e373e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 11:52:19 +0100 Subject: [PATCH 154/485] style: draw buttons before strings --- SleepTk.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 81663b2..fa6d792 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -270,15 +270,16 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self.btn_off = Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == _START: + self.btn_on = Button(x=0, y=200, w=200, h=40, label="Start tracking") + self.btn_on.draw() + self.btn_set = Button(x=201, y=200, w=39, h=40, label="S") + self.btn_set.draw() + draw.set_font(_FONT) draw.string('Sleep tracker with' , 0, 60) draw.string('alarm and smart alarm.' , 0, 80) draw.string('Wake you up to 30m' , 0, 100) draw.string('before alarm.' , 0, 120) draw.string('PRE RELEASE.' , 0, 160) - self.btn_on = Button(x=0, y=200, w=200, h=40, label="Start tracking") - self.btn_on.draw() - self.btn_set = Button(x=201, y=200, w=39, h=40, label="S") - self.btn_set.draw() elif self._page == _SETTINGS: self.btn_set_end = Button(x=201, y=200, w=39, h=40, label="X") self.btn_set_end.draw() From 06a59a98f1db764318eb07d2d28a88160de17fca Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 12:07:05 +0100 Subject: [PATCH 155/485] feat: scrolling indicator to access settings via swipe + disabled smart alarm by default --- README.md | 1 - SleepTk.py | 52 ++++++++++++++++++++++++++++------------------------ 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index b4bf161..0a8029e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ * if you actually use the watch during the night, make sure to count it as wakefulness? **misc** -* the settings should be a scrolled page * turn off the Bluetooth connection when no phone is connected? * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. * find a way to remove outliers of stored values diff --git a/SleepTk.py b/SleepTk.py index fa6d792..0267ba7 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -16,7 +16,7 @@ import time from wasp import watch, system, EventMask, gc from watch import rtc, battery, accel -from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView +from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView, ScrollIndicator from shell import mkdir, cd from fonts import sans18 @@ -45,7 +45,7 @@ class SleepTkApp(): def __init__(self): gc.collect() self._wakeup_enabled = 1 - self._wakeup_smart_enabled = 1 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART + self._wakeup_smart_enabled = 0 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._conf_view = None @@ -69,6 +69,7 @@ class SleepTkApp(): gc.collect() self._draw() system.request_event(EventMask.TOUCH | + EventMask.SWIPE_UPDOWN | EventMask.BUTTON) def sleep(self): @@ -83,6 +84,15 @@ class SleepTkApp(): self._disable_tracking() self._page = _START + def swipe(self, event): + "switches between start page and settings page" + if self._page == _START: + self._page = _SETTINGS + self._draw() + elif self._page == _SETTINGS: + self._page = _START + self._draw() + def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" gc.collect() @@ -118,8 +128,6 @@ class SleepTkApp(): self._WU_a = self._WU_t - _SMART_LEN system.set_alarm(self._WU_a, self._smart_alarm_compute) self._page = _TRACKING - elif self.btn_set.touch(event): - self._page = _SETTINGS elif self._page == _TRACKING or self._page == _TRACKING2: if self._conf_view is None: @@ -174,9 +182,6 @@ class SleepTkApp(): elif self._spin_M.touch(event): self._spinval_M = self._spin_M.value self._spin_M.update() - elif self.btn_set_end.touch(event): - self._page = _START - self._draw() if no_full_draw is False: self._draw() @@ -270,32 +275,31 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self.btn_off = Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == _START: - self.btn_on = Button(x=0, y=200, w=200, h=40, label="Start tracking") + self.btn_on = Button(x=0, y=200, w=240, h=40, label="Start tracking") self.btn_on.draw() - self.btn_set = Button(x=201, y=200, w=39, h=40, label="S") - self.btn_set.draw() + self.si = ScrollIndicator() + self.si.draw() draw.set_font(_FONT) draw.string('Sleep tracker with' , 0, 60) draw.string('alarm and smart alarm.' , 0, 80) - draw.string('Wake you up to 30m' , 0, 100) - draw.string('before alarm.' , 0, 120) + if not self._wakeup_smart_enabled: + # no need to remind it after the first time + draw.string('Swipe down for settings' , 0, 100) + else: + draw.string('Wake you up to 30m' , 0, 120) + draw.string('earlier.' , 0, 140) draw.string('PRE RELEASE.' , 0, 160) elif self._page == _SETTINGS: - self.btn_set_end = Button(x=201, y=200, w=39, h=40, label="X") - self.btn_set_end.draw() - - if self._wakeup_enabled: - self._spin_H = Spinner(10, 140, 0, 23, 2) - self._spin_H.value = self._spinval_H - self._spin_H.draw() - self._spin_M = Spinner(100, 140, 0, 59, 2) - self._spin_M.value = self._spinval_M - self._spin_M.draw() - self.check_al = Checkbox(x=0, y=40, label="Alarm") self.check_al.state = self._wakeup_enabled self.check_al.draw() - if self.check_al.state == 1: + if self._wakeup_enabled: + self._spin_H = Spinner(30, 120, 0, 23, 2) + self._spin_H.value = self._spinval_H + self._spin_H.draw() + self._spin_M = Spinner(150, 120, 0, 59, 2) + self._spin_M.value = self._spinval_M + self._spin_M.draw() self.check_smart = Checkbox(x=0, y=80, label="Smart alarm") self.check_smart.state = self._wakeup_smart_enabled self.check_smart.draw() From 9816e111aa2381fe5201895d62b70743385517a9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 12:13:17 +0100 Subject: [PATCH 156/485] testing: don't enter sleep while calculating best wake up time --- SleepTk.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 0267ba7..e8755b8 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -44,6 +44,7 @@ class SleepTkApp(): def __init__(self): gc.collect() + self._is_computing = False self._wakeup_enabled = 1 self._wakeup_smart_enabled = 0 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._spinval_H = 7 # default wake up time @@ -73,9 +74,13 @@ class SleepTkApp(): EventMask.BUTTON) def sleep(self): - """keep running in the background""" + """stop sleeping when calculating smart alarm time""" + gc.collect() + if self._is_computing: + return False + + def background(self): gc.collect() - return False def press(self, button, state): "stop ringing alarm if pressed physical button" @@ -310,6 +315,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _smart_alarm_compute(self): """computes best wake up time from sleep data""" + self._is_computing = True try: # stop tracking to save memory, keep the alarm just in case self._disable_tracking(keep_main_alarm=True) @@ -369,6 +375,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f = open("smart_alarm_error_{}.txt".format(int(time.time())), "wb") f.write(msg.encode()) f.close() + finally: + self._is_computing = False gc.collect() From 16546435f883991a78db094aff1c2e8e38f50501 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 12:17:22 +0100 Subject: [PATCH 157/485] remove useless scrolling indicator --- SleepTk.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index e8755b8..7d8b98d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -14,9 +14,9 @@ alarm you set up manually. import time from wasp import watch, system, EventMask, gc - from watch import rtc, battery, accel -from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView, ScrollIndicator + +from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView from shell import mkdir, cd from fonts import sans18 @@ -282,8 +282,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) elif self._page == _START: self.btn_on = Button(x=0, y=200, w=240, h=40, label="Start tracking") self.btn_on.draw() - self.si = ScrollIndicator() - self.si.draw() draw.set_font(_FONT) draw.string('Sleep tracker with' , 0, 60) draw.string('alarm and smart alarm.' , 0, 80) From 3f277228142b380c2543021bfdebd1a677569c8a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 13:20:29 +0100 Subject: [PATCH 158/485] fix: timestamp format was wrong --- SleepTk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 7d8b98d..32d2df2 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -25,6 +25,7 @@ from array import array from micropython import const _FONT = sans18 +_TIMESTAMP = const(946684800) _BATTERY_THRESHOLD = const(20) # under 20% of battery, stop tracking and only keep the alarm _AVG_SLEEP_CYCL = const(32400) # 90 minutes, average sleep cycle duration _OFFSETS = array("H", [0, 300, 600, 900, 1200, 1500, 1800]) @@ -109,7 +110,7 @@ class SleepTkApp(): self._buff = array("f") self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file - self._offset = int(rtc.time()) # makes output more compact + self._offset = int(rtc.time()) + _TIMESTAMP # makes output more compact # create one file per recording session: self.filep = "logs/sleep/{}.csv".format(str(self._offset)) From 7d74435c98ff38e08f12c5a14e60a9a8fe8a9dd5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 14:08:14 +0100 Subject: [PATCH 159/485] sqrt, pow, and degrees were awfully slow --- SleepTk.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 32d2df2..b22b728 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -20,7 +20,7 @@ from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView from shell import mkdir, cd from fonts import sans18 -from math import atan, pow, degrees, sqrt, sin, pi +from math import atan, sin from array import array from micropython import const @@ -33,6 +33,10 @@ _FREQ = const(5) # get accelerometer data every X seconds, they will be average _STORE_FREQ = const(300) # number of seconds between storing average values to file written every X points _SMART_LEN = const(1800) # defaults 1800 = 30m +# math related : +_DEGREE = const(5729578) # result of 180/pi*100_000, for conversion +_PIPI = const(628318) # result of 2*pi*100_000 + # page values: _START = const(0) _TRACKING = const(1) @@ -244,7 +248,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff.append(x_avg) buff.append(y_avg) buff.append(z_avg) - buff.append(degrees(atan(z_avg / (pow(x_avg, 2) + pow(y_avg, 2) + 0.0000001)))) # formula from https://www.nature.com/articles/s41598-018-31266-z + buff.append(abs(atan(z_avg / x_avg**2 + y_avg**2))) + # formula from https://www.nature.com/articles/s41598-018-31266-z del x_avg, y_avg, z_avg buff.append(battery.voltage_mv()) # currently more accurate than percent @@ -329,7 +334,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # center and scale mean = sum(data) / len(data) - std = sqrt((sum([x**2 for x in data]) / len(data)) - pow(mean, 2)) + std = ((sum([x**2 for x in data]) / len(data)) - mean**2)**0.5 for i in range(len(data)): data[i] = (data[i] - mean) / std del mean, std @@ -337,7 +342,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # fitting cosine of various offsets in minutes, the best fit has the # period indicating best wake up time: fits = array("f") - omega = 2 * pi / _AVG_SLEEP_CYCL + omega = _PIPI / 100000 * _AVG_SLEEP_CYCL / 2 # 2 * pi * period for cnt, offset in enumerate(_OFFSETS): # least square regression fits.append( sum([sin(omega * t * _STORE_FREQ + offset) * data[t] for t in range(len(data))]) From e162c49fcc1f99f6f345adb8627cf647fa887d2e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 14:29:19 +0100 Subject: [PATCH 160/485] allow waking up up to 45m + comments --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index b22b728..4d267bd 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -28,10 +28,10 @@ _FONT = sans18 _TIMESTAMP = const(946684800) _BATTERY_THRESHOLD = const(20) # under 20% of battery, stop tracking and only keep the alarm _AVG_SLEEP_CYCL = const(32400) # 90 minutes, average sleep cycle duration -_OFFSETS = array("H", [0, 300, 600, 900, 1200, 1500, 1800]) +_OFFSETS = array("H", [0, 300, 600, 900, 1200, 1500, 1800]) # try to fit sinus of different offsets, separated by 5 minutes _FREQ = const(5) # get accelerometer data every X seconds, they will be averaged _STORE_FREQ = const(300) # number of seconds between storing average values to file written every X points -_SMART_LEN = const(1800) # defaults 1800 = 30m +_SMART_LEN = const(2700) # can wake you up in any interval up to 45 minutes before # math related : _DEGREE = const(5729578) # result of 180/pi*100_000, for conversion @@ -295,7 +295,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # no need to remind it after the first time draw.string('Swipe down for settings' , 0, 100) else: - draw.string('Wake you up to 30m' , 0, 120) + draw.string('Wake you up to 45m' , 0, 120) draw.string('earlier.' , 0, 140) draw.string('PRE RELEASE.' , 0, 160) elif self._page == _SETTINGS: From 8544d6fceee6c3a7deac93494064a6bc3119d050 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 14:29:46 +0100 Subject: [PATCH 161/485] allow waking up up to 45m + comments --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4d267bd..724f24d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -7,7 +7,7 @@ # https://github.com/thiswillbeyourgithub/sleep_tracker_pinetime_wasp-os SleepTk is designed to track accelerometer data throughout the night. It can -also compute the best time to wake you up, up to 30 minutes before the +also compute the best time to wake you up, up to 45 minutes before the alarm you set up manually. """ From f2d3c1a5e7efa4814864dd943a6b5482a3fe077c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 14:29:58 +0100 Subject: [PATCH 162/485] don't need _DEGREE actually --- SleepTk.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 724f24d..29abbc6 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -34,7 +34,6 @@ _STORE_FREQ = const(300) # number of seconds between storing average values to _SMART_LEN = const(2700) # can wake you up in any interval up to 45 minutes before # math related : -_DEGREE = const(5729578) # result of 180/pi*100_000, for conversion _PIPI = const(628318) # result of 2*pi*100_000 # page values: From 88f63329b0668cb50223c39b67594f3a42aa9f34 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 14:30:24 +0100 Subject: [PATCH 163/485] comment warning that atan is actually faster than taylor series --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 29abbc6..2548dd6 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -247,7 +247,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff.append(x_avg) buff.append(y_avg) buff.append(z_avg) - buff.append(abs(atan(z_avg / x_avg**2 + y_avg**2))) + buff.append(abs(atan(z_avg / x_avg**2 + y_avg**2))) # note: math.atan() is faster than using a taylor serie # formula from https://www.nature.com/articles/s41598-018-31266-z del x_avg, y_avg, z_avg buff.append(battery.voltage_mv()) # currently more accurate than percent From c90ba5ff7ed82b84473840255bc5e63090e33d67 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 14:34:52 +0100 Subject: [PATCH 164/485] fix: _TIMESTAMP was added at the wrong var --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 2548dd6..a0f1fa0 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -25,7 +25,7 @@ from array import array from micropython import const _FONT = sans18 -_TIMESTAMP = const(946684800) +_TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _BATTERY_THRESHOLD = const(20) # under 20% of battery, stop tracking and only keep the alarm _AVG_SLEEP_CYCL = const(32400) # 90 minutes, average sleep cycle duration _OFFSETS = array("H", [0, 300, 600, 900, 1200, 1500, 1800]) # try to fit sinus of different offsets, separated by 5 minutes @@ -113,10 +113,10 @@ class SleepTkApp(): self._buff = array("f") self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file - self._offset = int(rtc.time()) + _TIMESTAMP # makes output more compact + self._offset = int(rtc.time()) # makes output more compact # create one file per recording session: - self.filep = "logs/sleep/{}.csv".format(str(self._offset)) + self.filep = "logs/sleep/{}.csv".format(str(self._offset + _TIMESTAMP)) self._add_accel_alar() # setting up alarm From 024d06f13682eaba15a78b5cc98461492ef368de Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 14:51:05 +0100 Subject: [PATCH 165/485] more efficient way to write to file --- SleepTk.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index a0f1fa0..a70ead7 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -253,7 +253,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff.append(battery.voltage_mv()) # currently more accurate than percent f = open(self.filep, "ab") - f.write(b",".join([str(x)[0:8].encode() for x in buff]) + b"\n") + for x in buff[:-1]: + f.write("{:08d}".format(x).encode()) + f.write(b",") + f.write("{:08d}".format(buff[-1]).encode()) + f.write(b"\n") f.close() self._last_checkpoint = self._data_point_nb From b3b41c0cdb7038b0e2b3b2cb8a2555b3270b61f7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 15:49:41 +0100 Subject: [PATCH 166/485] minor: renamed _tracking to _is_tracking --- SleepTk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index a70ead7..997fdfc 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -48,13 +48,13 @@ class SleepTkApp(): def __init__(self): gc.collect() - self._is_computing = False self._wakeup_enabled = 1 self._wakeup_smart_enabled = 0 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._conf_view = None - self._tracking = False # False = not tracking, True = currently tracking + self._is_computing = False + self._is_tracking = False self._earlier = 0 self._page = _START @@ -108,7 +108,7 @@ class SleepTkApp(): no_full_draw = False if self._page == _START: if self.btn_on.touch(event): - self._tracking = True + self._is_tracking = True # accel data not yet written to disk: self._buff = array("f") self._data_point_nb = 0 # total number of data points so far @@ -198,7 +198,7 @@ class SleepTkApp(): def _disable_tracking(self, keep_main_alarm=False): """called by touching "STOP TRACKING" or when computing best alarm time to wake up you disables tracking features and alarms""" - self._tracking = False + self._is_tracking = False system.cancel_alarm(self.next_al, self._trackOnce) if self._wakeup_enabled: if keep_main_alarm is False: # to keep the alarm when stopping because of low battery @@ -217,7 +217,7 @@ class SleepTkApp(): def _trackOnce(self): """get one data point of accelerometer every _FREQ seconds and they are then averaged and stored every _STORE_FREQ seconds""" - if self._tracking: + if self._is_tracking: [self._buff.append(x) for x in accel.read_xyz()] self._data_point_nb += 1 self._add_accel_alar() From 86e4735788748a9ce8a7a5934c27ebfd55e2cd51 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 17:27:48 +0100 Subject: [PATCH 167/485] don't waste space by writing 0 to the file --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 997fdfc..2e878a0 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -254,9 +254,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f = open(self.filep, "ab") for x in buff[:-1]: - f.write("{:08d}".format(x).encode()) + f.write("{}".format(x).encode()) f.write(b",") - f.write("{:08d}".format(buff[-1]).encode()) + f.write("{}".format(buff[-1]).encode()) f.write(b"\n") f.close() From 0a6806d8b13c8deef12fa4611a04a106d4c65efa Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 17:28:20 +0100 Subject: [PATCH 168/485] fix: read data character by character --- SleepTk.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 2e878a0..a206c15 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -327,13 +327,28 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # stop tracking to save memory, keep the alarm just in case self._disable_tracking(keep_main_alarm=True) - # get angle over time - f = open(self.filep, "rb") - lines = f.readlines() + + # read file one character at a time, to get only the 4th + # value of each row, which is the arm angle + data = array("f") + buff = b"" + cnt = 0 + f = open(fname, "rb") + while True: + char = f.read(1) + if char == b",": + cnt += 1 + elif char == b"\n": + data.append(float(buff)) + cnt = 0 + buff = b"" + elif cnt == 4: + buff = buff + char + elif char == b"": + break f.close() - if len(lines) == 0: - lines = lines[0].split(b"\n") - data = array("f", [float(line.split(",")[4]) for line in lines]) + del f, char, buff + gc.collect() # center and scale mean = sum(data) / len(data) From 41f2b51a3148c144970aee61fa024c3283730ef4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 17:29:01 +0100 Subject: [PATCH 169/485] smart alarm: smoother and center + scale with clipping between -1 ans 1 --- SleepTk.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index a206c15..640db9c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -350,12 +350,19 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del f, char, buff gc.collect() - # center and scale + # smoothen several times + for j in range(5): + for i in range(1, len(data)-2): + data[i] += data[i-1] + data[i+1] + data[i] /= 3 + + # center and scale and clip between -1 and 1 mean = sum(data) / len(data) std = ((sum([x**2 for x in data]) / len(data)) - mean**2)**0.5 for i in range(len(data)): - data[i] = (data[i] - mean) / std + data[i] = min(1, max(-1, (data[i] - mean) / std)) del mean, std + gc.collect() # fitting cosine of various offsets in minutes, the best fit has the # period indicating best wake up time: From 973db51ae5472719e587cebd3ea264c8210aa1e0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 19:22:32 +0100 Subject: [PATCH 170/485] up to 40 minutes before actually --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 640db9c..8904ddd 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -7,7 +7,7 @@ # https://github.com/thiswillbeyourgithub/sleep_tracker_pinetime_wasp-os SleepTk is designed to track accelerometer data throughout the night. It can -also compute the best time to wake you up, up to 45 minutes before the +also compute the best time to wake you up, up to 40 minutes before the alarm you set up manually. """ From 9c41de6bd326bd8b6b2446a8d64ce75a1d3d653b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 19:23:35 +0100 Subject: [PATCH 171/485] major refactor: smart alarm might just work! --- SleepTk.py | 107 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 8904ddd..d00d3b0 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -24,24 +24,24 @@ from math import atan, sin from array import array from micropython import const -_FONT = sans18 -_TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date -_BATTERY_THRESHOLD = const(20) # under 20% of battery, stop tracking and only keep the alarm -_AVG_SLEEP_CYCL = const(32400) # 90 minutes, average sleep cycle duration -_OFFSETS = array("H", [0, 300, 600, 900, 1200, 1500, 1800]) # try to fit sinus of different offsets, separated by 5 minutes -_FREQ = const(5) # get accelerometer data every X seconds, they will be averaged -_STORE_FREQ = const(300) # number of seconds between storing average values to file written every X points -_SMART_LEN = const(2700) # can wake you up in any interval up to 45 minutes before - -# math related : +# HARDCODED VARIABLES: _PIPI = const(628318) # result of 2*pi*100_000 - -# page values: -_START = const(0) +_CONV = const(100000) # to get 2*pi +_START = const(0) # page values _TRACKING = const(1) _TRACKING2 = const(2) _SETTINGS = const(3) _RINGING = const(4) +_FONT = sans18 +_TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date +_FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_STORE_FREQ = const(300) # process data and store to file every X seconds +_SLEEP_CYCL_TRY = array("H", [4800, 5400, 6000]) # sleep cycle length to try (80, 90 and 100 minutes) +_BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only keep the alarm + +# user can want to edit this: +_OFFSETS = array("H", [0, 600, 1200, 1800, 2400]) # possible offsets of sinus to try to fit to data (40 minutes, by increment of 10 minutes) + class SleepTkApp(): NAME = 'SleepTk' @@ -132,9 +132,9 @@ class SleepTkApp(): self._WU_t = time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) system.set_alarm(self._WU_t, self._listen_to_ticks) - # alarm in _SMART_LEN less seconds to compute best wake up time + # wake up SleepTk 2min before earliest possible wake up if self._wakeup_smart_enabled: - self._WU_a = self._WU_t - _SMART_LEN + self._WU_a = self._WU_t - _OFFSETS[-1] - 120 system.set_alarm(self._WU_a, self._smart_alarm_compute) self._page = _TRACKING @@ -298,7 +298,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # no need to remind it after the first time draw.string('Swipe down for settings' , 0, 100) else: - draw.string('Wake you up to 45m' , 0, 120) + draw.string('Wake you up to 40m' , 0, 120) draw.string('earlier.' , 0, 140) draw.string('PRE RELEASE.' , 0, 160) elif self._page == _SETTINGS: @@ -327,13 +327,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # stop tracking to save memory, keep the alarm just in case self._disable_tracking(keep_main_alarm=True) - # read file one character at a time, to get only the 4th # value of each row, which is the arm angle data = array("f") buff = b"" cnt = 0 - f = open(fname, "rb") + f = open(self.filep, "rb") while True: char = f.read(1) if char == b",": @@ -347,53 +346,71 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) elif char == b"": break f.close() - del f, char, buff + del f, char, buff, cnt gc.collect() # smoothen several times - for j in range(5): + for j in range(15): for i in range(1, len(data)-2): data[i] += data[i-1] + data[i+1] data[i] /= 3 + del i, j # center and scale and clip between -1 and 1 mean = sum(data) / len(data) std = ((sum([x**2 for x in data]) / len(data)) - mean**2)**0.5 for i in range(len(data)): data[i] = min(1, max(-1, (data[i] - mean) / std)) - del mean, std + del mean, std, i gc.collect() - # fitting cosine of various offsets in minutes, the best fit has the - # period indicating best wake up time: - fits = array("f") - omega = _PIPI / 100000 * _AVG_SLEEP_CYCL / 2 # 2 * pi * period - for cnt, offset in enumerate(_OFFSETS): # least square regression - fits.append( - sum([sin(omega * t * _STORE_FREQ + offset) * data[t] for t in range(len(data))]) - -sum([(sin(omega * t * _STORE_FREQ + offset) - data[t])**2 for t in range(len(data))]) - ) - if fits[-1] == min(fits): - best_offset = _OFFSETS[cnt] - del fits, offset, cnt + # for each sleep cycle, do a least square regression on each + # possible offset value of a sinusoidal wave with the same + # frequency as the sleep cycle + bof_p_cycl = array("H", [0] * len(_SLEEP_CYCL_TRY)) # best offset found per cycle + bfi_p_cycl = array("f", [0] * len(_SLEEP_CYCL_TRY)) # fit value for best offset + for cnt_cycle, cycle in enumerate(_SLEEP_CYCL_TRY): + omega = (_PIPI / _CONV) / (cycle * 2) # 2 * pi * frequency + fits = array("f") + # least square regression: + for cnt_offs, offset in enumerate(_OFFSETS): + fits.append(sum( + [(sin(omega * (i*_STORE_FREQ + offset)) - data[i])**4 + for i in range(len(data))] + )) + if fits[-1] == min(fits): + bof_p_cycl[cnt_cycle] = _OFFSETS[cnt_offs] + bfi_p_cycl[cnt_cycle] = min(fits) + del fits, offset, cnt_cycle, cnt_offs, data, cycle, omega + gc.collect() + + # find sleep cycle and offset with the least fit: + for i, fit in enumerate(bfi_p_cycl): + if fit == min(bfi_p_cycl): + best_offset = bof_p_cycl[i] + best_omega = (_PIPI / _CONV) / (_SLEEP_CYCL_TRY[i] * 2) + break + del bof_p_cycl, bfi_p_cycl, i, fit + gc.collect() # finding how early to wake up: - max_sin = 0 + max_sin = -1 WU_t = self._WU_t - for t in range(WU_t, WU_t - _SMART_LEN, -300): # counting backwards from original wake up time, steps of 5 minutes - s = sin(omega * t + best_offset) + # counting backwards from original wake up time + for t in range(WU_t, WU_t - _OFFSETS[-1], -_STORE_FREQ): + s = sin(best_omega * (t + best_offset)) if s > max_sin: max_sin = s - self._earlier = -t # number of seconds earlier than wake up time - del max_sin, s - - system.set_alarm( - min( - WU_t - 5, # not after original wake up time - max(WU_t - self._earlier, int(rtc.time()) + 3) # not before right now - ), self._listen_to_ticks) - self._page = _TRACKING2 + earlier = t # number of seconds earlier than wake up time + del max_sin, s, t, best_offset, best_omega gc.collect() + + system.set_alarm(min(WU_t - 5, # not after original wake up time + max(WU_t - earlier, # not before right now + int(rtc.time()) + 3) + ), self._listen_to_ticks) + self._earlier = earlier + self._page = _TRACKING2 except Exception as e: gc.collect() t = watch.time.localtime(time.time()) From 77359dd0c930419d6ccee135a6945eef2b7e854e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 19:31:55 +0100 Subject: [PATCH 172/485] store angle average as the first column, to make it faster to read --- SleepTk.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index d00d3b0..3019c2f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -243,11 +243,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) y_avg = sum([buff[i] for i in range(1, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) z_avg = sum([buff[i] for i in range(2, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) buff = array("f") # reseting array + buff.append(abs(atan(z_avg / x_avg**2 + y_avg**2))) # note: math.atan() is faster than using a taylor serie buff.append(int(rtc.time() - self._offset)) buff.append(x_avg) buff.append(y_avg) buff.append(z_avg) - buff.append(abs(atan(z_avg / x_avg**2 + y_avg**2))) # note: math.atan() is faster than using a taylor serie # formula from https://www.nature.com/articles/s41598-018-31266-z del x_avg, y_avg, z_avg buff.append(battery.voltage_mv()) # currently more accurate than percent @@ -327,26 +327,30 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # stop tracking to save memory, keep the alarm just in case self._disable_tracking(keep_main_alarm=True) - # read file one character at a time, to get only the 4th + # read file one character at a time, to get only the 1st # value of each row, which is the arm angle data = array("f") buff = b"" - cnt = 0 f = open(self.filep, "rb") + skip = False while True: char = f.read(1) - if char == b",": - cnt += 1 - elif char == b"\n": + if char == b",": # start ignoring after the first col + skip = True + continue + if char == b"\n": + skip = False # stop skipping because reading a new line data.append(float(buff)) - cnt = 0 buff = b"" - elif cnt == 4: - buff = buff + char - elif char == b"": + continue + + if char == b"": break + elif not skip: + buff += char + f.close() - del f, char, buff, cnt + del f, char, buff gc.collect() # smoothen several times From 53574a30e107fd58e3a37397dc5b7db162667cf8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 19:37:13 +0100 Subject: [PATCH 173/485] comments --- SleepTk.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 3019c2f..30fe895 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -343,10 +343,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) data.append(float(buff)) buff = b"" continue - - if char == b"": + if char == b"": # end of file break - elif not skip: + elif not skip: # digit of arm angle value buff += char f.close() From 10d7099bee5475d82d0b16ce8c8f4ef432ebedad Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 19:37:28 +0100 Subject: [PATCH 174/485] set first data to something high because you were awake --- SleepTk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 30fe895..a8f68aa 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -352,6 +352,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del f, char, buff gc.collect() + # the first value HAS to be high because you were still awake + data[0] = max(data) + # smoothen several times for j in range(15): for i in range(1, len(data)-2): From c36c164442b567f560525f7ebc3005355d62f2b3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 19:38:38 +0100 Subject: [PATCH 175/485] docs: add instruction to use pandas --- README.md | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a8029e..b7ad8ae 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ * put the latest app in wasp-os/wasp/apps/SleepTk.py * compile and install wasp-os * run the app - * *if you want, you can get back the data using `wasptool --pull`, to take a look using pandas : ` df = pd.read_csv("./first.night.csv", names=["time", "x_avg", "y_avg", "z_avg", "angl_avg", "battery"])` (name and number of columns might change)* + * *if you want, you can get back the data using `wasptool --pull`, then running the commands suggested below. # Screenshots: ![start](./screenshots/start_page.png) @@ -38,3 +38,38 @@ * very interesting research paper on the topic : https://academic.oup.com/sleep/article/42/12/zsz180/5549536 * maybe coding a 1D convolution is a good way to extract peaks * list of ways to find local maxima in python : https://blog.finxter.com/how-to-find-local-minima-in-1d-and-2d-numpy-arrays/ + https://pythonawesome.com/overview-of-the-peaks-dectection-algorithms-available-in-python/ + + + + +## Pandas integration: +Commands the author uses to take a look a the data using pandas: +``` +fname = "./logs/sleep/YOUR_TIME.csv" + +import pandas as pd +from math import atan + +df = pd.read_csv(fname, names=["angl_avg", "time", "x_avg", "y_avg", "z_avg", "battery"]) +offset = int(fname.split("/")[-1].split(".csv")[0]) +df["human_time"] = pd.to_datetime(df["time"]+offset, unit='s') +df["hours"] = df["human_time"].dt.time +df = df.set_index("hours") +df["angl_avg"].plot() + +# testing different fusion formulae: +def fusion(x, y, z): + values = z / (x**2 + y**2) + for i in range(len(values)-2): # rolling average + values[i+1] = (values[i] + values[i+1] + values[i+2])/3 + return [abs(atan(val)) for val in values] # arctan then absolute value + +x = df["x_avg"].values ; y = df["y_avg"].values ; z = df["z_avg"].values + +df["angl_avg"] = fusion(x, y, z) +df["f2"] = fusion(y, z, x) +df["f3"] = fusion(z, x, y) + +# plotting +df["f1"].plot() ; df["f2"].plot() ; df["f3"].plot() +``` From c23673193a443507d47c55ba71aa3fcd4a1410aa Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 19:39:17 +0100 Subject: [PATCH 176/485] remove pandas code to test fusion def --- README.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/README.md b/README.md index b7ad8ae..0da492d 100644 --- a/README.md +++ b/README.md @@ -56,20 +56,4 @@ df["human_time"] = pd.to_datetime(df["time"]+offset, unit='s') df["hours"] = df["human_time"].dt.time df = df.set_index("hours") df["angl_avg"].plot() - -# testing different fusion formulae: -def fusion(x, y, z): - values = z / (x**2 + y**2) - for i in range(len(values)-2): # rolling average - values[i+1] = (values[i] + values[i+1] + values[i+2])/3 - return [abs(atan(val)) for val in values] # arctan then absolute value - -x = df["x_avg"].values ; y = df["y_avg"].values ; z = df["z_avg"].values - -df["angl_avg"] = fusion(x, y, z) -df["f2"] = fusion(y, z, x) -df["f3"] = fusion(z, x, y) - -# plotting -df["f1"].plot() ; df["f2"].plot() ; df["f3"].plot() ``` From 55bae16aa922a25bcfb47eeaedeaf6b902a742ac Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 19:48:08 +0100 Subject: [PATCH 177/485] add screenshot of example night data --- README.md | 1 + screenshots/example_night.png | Bin 0 -> 30019 bytes 2 files changed, 1 insertion(+) create mode 100644 screenshots/example_night.png diff --git a/README.md b/README.md index 0da492d..aab9b21 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,4 @@ df["hours"] = df["human_time"].dt.time df = df.set_index("hours") df["angl_avg"].plot() ``` +![night example](./screenshots/example_night.png) diff --git a/screenshots/example_night.png b/screenshots/example_night.png new file mode 100644 index 0000000000000000000000000000000000000000..104ae354e1e848fc38eb6fa3107a0f8013298ac9 GIT binary patch literal 30019 zcmbrmWmweF*DgE;0)oI$(#ViXiF62x0}L(FARr*!-Jl3ELzf_}Lzgs2BPlu3Al(g0 zH}A&(bIyBR=RME+;T%7BF*Co}x%OK3y4Sr2^-@XZ-d(D@5D4U+ECT)t0=abu0=Y4S za}&Hn*qaj$fjof7!k??TCvQ%9c&jaq3;x;Y5sy(g^fABL{e*Gfg-zV>gh_4?fl7wBqC;9I?&u~!JFL3@(zJx?D9^4F=KRTP3nCLa-?QnQi zgaz7SiCFPS>Wzzv`uYm@`=|7=$ejB6r=p^|>+0acXXfSQ1VP0;lz}b-s$l`@I7Qod|PYlgX@XBgnZ>SjMTpep83C+C89iX^#h^! zahGO8bX3$2he>L?I)|)o*X>D%}aqFLe80&n+x@B z13eST!tbJJ2xPRi$CrDPFpi2ZUbK*DzIOQAc7JE0YNtr*1VdU8Z;VqjOps?0%M9nPTC%&KJ^l?9&57I%vB>JBO4wbj^-HH zS&7iL4*HEGgE9d-VV@j-1OcJ=*&z2zDXU5k#2;Kku!REu6&Yf#Z zeU@QCP2pKp8GoodakSL4&-7Le{PaZTzxa~rDG-D^7<&uPfQKXjBW6(4aN6TEfla!qMYq3y^gj+xEnrS zNQh3<{B5UB^H@#@4<{g}hN}4bUh3M_A6)GCT^%)E`Dkd2#3+kvYioCI*Bt(~s;#Zv zsofC|X?x1w#l2|LmI2*umYw8$*II@& zAksVg^Fiyxf-uw`gPMbXAB`NTivpc zIeRosMbW@l#|YlvGXR#qCcRu|>X7{8KmFA`6H(#4cG@qq5a$27+3hO*k0oG=dW0Ro z1m^T7CZypZcqC=>WE0_RI1K%ZWZ7`)3d`DVuhI!%-eBJ(Lsw2lVP9xfh$PXSgE{JD zWk$IFrU91`DF~}qs+U$5R_)wE`V$c=%06hJx(*3!eT{@V$W?UoB3pg*e?~*hXAOdl z)V#-~L68V?wN!fzY!?#_Dl5Q5i@t2^<5Tf}J3{n?wPy2RiXcVDr8AT?cN9h(09TEM zS|J2Be0(ooQZF~&guHVsRxQhHljXTnIAjIIl|(RzK&DRRH!^@IyeNvg3lZ+^1xRJCDFM?wQ!0Ju!+GxDv@1yBnt{6pfftqS(^?? z#s0m6-+KQx;=ye#n9AZ%A`ckpqmdj=g>aZP=?%y`Soyv%VdcF%AsumEf2Cd=1{Fjs zRt?G5RbcAP(}yp}VLG4FB1sG5Jvq^m2-2H-zly=S#Lmw^N51lar*UG#LJwv=^w%rE z5-aF;%>J5Qz5&yL$}Sz@OQMNBr;?`+=G+Mq0}K3bT^@Vb(U%hSpHChSQG5VXpau{9 zvQW!hu+8-%O~PZPSZX~GthqsBi=N($d{iO=4jyD?pUzRuf35E~(G)pk zrGk^X;YX5T!(Vw}oz|=+Y+?x&1vO zr|v=j_U`oles-_<*=@)J{GjeLLp?&Mo&YriRDrU)Sen)sA$DN+VxsA#XTjWINHCoN zzn`?HLd4WTVPS2J4Oedb9mtnwY5aul-rlCkC2VaR2Rck2BH7-KaY2Lovc7c-oOc|p zBUe+&2E#;LLZ6>)uJ4u2hbh+g4P%DJfJ?cL1Np+of3F_y?5kflgcn6DiH@WkXO^@= z=XxHeED%%`afohrmEya~%Z1Ky3v$ba4$xYbO&`WTwJkFEO?DH)rl4p{4^2r+dvG9` z5deC=8=a2K!OU$teb3aFrw|>movN%2L~hp`eUxj%vco6`dwL$6u%w)})TGikl+yPS zfb-h6bx`IQCxwG*E%De}uB#LK@T1tgADx((ILQ5d6&>=OyJkL^eN0bq%6V{gLbE`N z;df~VCS&kzJ0OiAtM|(W05}4hz%uUfo??kJ#C3Yig9Lmn%l~Hz*ude=$_( z`1JCR-rmtGF_QW-9k1^xn{1-2#sHQ!Yu$VlxM3Hn;Rp={`aOScMfuC|E^YX}qaq&0 z7wQS&03dr;QEg#v)ocyzf|jtH zPHYHHVB$w{m=48wwkeYmV$rqsXmeauMFr|E`za5=yqcO*4nmLh-0W|)+ch~k7;5wF z$!cC^=FE(Yw;<-$*Vh}1cP%+;%izw%KUQLFF#tiQeJ_n~_(&)k?J`sS{SQYxW%+uw zumfDp$8nSX76d{e(U>+fg^WZZk|T4nvpZJyGPAPo*vSISq}3<%JufS3`PbJMRg;eL zf8xm4HN=i4odBq4JRL5Y_Pe_1=Bn*k*-N=>0)VJ3{AXVu!ImZu-TS#B5D0CX)-HX9 zabY@FOT=Z*y$(aT*$_-<<#i}PNeb#V*}Wp0VFS7HgT@*9A+3vA4u?ak%Dn*b%{2%9 z(7m1@dr_U^;pRBNn&&GLS0`)PX<)W{``ESsDA~j13+DPbk2f4J~f70+mkE% zj6D`wd??HB=kt6-n460m^tXPhpFsjTgvv9Ca0}|E#~C9Yu<$foW@Io`=uC}|Q`Tjc zmGN@gY{tzI1T8GZ8#J6OCt>!jv9PdYWo2DlT(V~}M>mY66HdEWAMwcDw;I|N8%`JP zzwKjRmxOGnFL)!?7_;X%?@Xm$)f>q55&}_eB}Vm06#f*viGmXU2a9Q{tC#qlUR>to z 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 literal 0 HcmV?d00001 From 0918fa1b82c2862fe1445339a24036eb8088770b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 19:48:56 +0100 Subject: [PATCH 178/485] smoothen twice 5 times with averaging in between --- SleepTk.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index a8f68aa..453e91f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -353,14 +353,16 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) gc.collect() # the first value HAS to be high because you were still awake - data[0] = max(data) + if data[0] < 0.75*max(data): + data[0] = 0.75*max(data) # smoothen several times - for j in range(15): + for j in range(5): for i in range(1, len(data)-2): data[i] += data[i-1] + data[i+1] data[i] /= 3 del i, j + gc.collect() # center and scale and clip between -1 and 1 mean = sum(data) / len(data) @@ -370,6 +372,14 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del mean, std, i gc.collect() + # smoothen several times + for j in range(5): + for i in range(1, len(data)-2): + data[i] += data[i-1] + data[i+1] + data[i] /= 3 + del i, j + gc.collect() + # for each sleep cycle, do a least square regression on each # possible offset value of a sinusoidal wave with the same # frequency as the sleep cycle From bddd551f21c15d8e4970a3dd254bba5a74532d4f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 20:13:29 +0100 Subject: [PATCH 179/485] style --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index aab9b21..0e2ce15 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ ## Pandas integration: Commands the author uses to take a look a the data using pandas: + ``` fname = "./logs/sleep/YOUR_TIME.csv" @@ -57,4 +58,5 @@ df["hours"] = df["human_time"].dt.time df = df.set_index("hours") df["angl_avg"].plot() ``` + ![night example](./screenshots/example_night.png) From 9dfb6fdd7be2e469e11774a4d2a6cdb699a8c265 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 20:15:10 +0100 Subject: [PATCH 180/485] feat: add timer when computing smart alarm --- SleepTk.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 453e91f..0dd51e6 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -13,7 +13,7 @@ alarm you set up manually. """ import time -from wasp import watch, system, EventMask, gc +from wasp import watch, system, EventMask, gc, machine from watch import rtc, battery, accel from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView @@ -323,7 +323,10 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _smart_alarm_compute(self): """computes best wake up time from sleep data""" self._is_computing = True + gc.collect() try: + timer = machine.Timer(id=1, period=8000000) + timer.start() # stop tracking to save memory, keep the alarm just in case self._disable_tracking(keep_main_alarm=True) @@ -427,6 +430,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) ), self._listen_to_ticks) self._earlier = earlier self._page = _TRACKING2 + elapsed = timer.time() + timer.stop() + msg = "Finished computing best wake up time in {}".format(elapsed) + system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", + "title": "Finished smart alarm computation", + "body": msg}) except Exception as e: gc.collect() t = watch.time.localtime(time.time()) @@ -438,6 +447,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f.write(msg.encode()) f.close() finally: + t.stop() self._is_computing = False gc.collect() From 0ee2059951b28d4d04842e130c730869b998c6dd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 20:22:50 +0100 Subject: [PATCH 181/485] new: store diff value of x/y/z instead of raw values. Then called the result fusion instead of angl_avg --- README.md | 4 ++-- SleepTk.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0e2ce15..7eaac92 100644 --- a/README.md +++ b/README.md @@ -51,12 +51,12 @@ fname = "./logs/sleep/YOUR_TIME.csv" import pandas as pd from math import atan -df = pd.read_csv(fname, names=["angl_avg", "time", "x_avg", "y_avg", "z_avg", "battery"]) +df = pd.read_csv(fname, names=["fusion_value", "time", "x_diff", "y_diff", "z_diff", "battery"]) offset = int(fname.split("/")[-1].split(".csv")[0]) df["human_time"] = pd.to_datetime(df["time"]+offset, unit='s') df["hours"] = df["human_time"].dt.time df = df.set_index("hours") -df["angl_avg"].plot() +df["fusion_value"].plot() ``` ![night example](./screenshots/example_night.png) diff --git a/SleepTk.py b/SleepTk.py index 0dd51e6..a233388 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -239,17 +239,17 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """save data after averageing over a window to file""" buff = self._buff if self._data_point_nb - self._last_checkpoint >= _STORE_FREQ / _FREQ: - x_avg = sum([buff[i] for i in range(0, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) - y_avg = sum([buff[i] for i in range(1, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) - z_avg = sum([buff[i] for i in range(2, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) + x_diff = sum([abs(abs(buff[i]) - abs(buff[i+1])) for i in range(0, len(buff)-1, 3)]) / (self._data_point_nb - self._last_checkpoint) + y_diff = sum([abs(abs(buff[i]) - abs(buff[i+1])) for i in range(1, len(buff)-1, 3)]) / (self._data_point_nb - self._last_checkpoint) + z_diff = sum([abs(abs(buff[i]) - abs(buff[i+1])) for i in range(2, len(buff)-1, 3)]) / (self._data_point_nb - self._last_checkpoint) buff = array("f") # reseting array - buff.append(abs(atan(z_avg / x_avg**2 + y_avg**2))) # note: math.atan() is faster than using a taylor serie + buff.append(abs(atan(z_diff / x_diff**2 + y_diff**2))) # note: math.atan() is faster than using a taylor serie + # formula from https://www.nature.com/articles/s41598-018-31266-z buff.append(int(rtc.time() - self._offset)) - buff.append(x_avg) - buff.append(y_avg) - buff.append(z_avg) - # formula from https://www.nature.com/articles/s41598-018-31266-z - del x_avg, y_avg, z_avg + buff.append(x_diff) + buff.append(y_diff) + buff.append(z_diff) + del x_diff, y_diff, z_diff buff.append(battery.voltage_mv()) # currently more accurate than percent f = open(self.filep, "ab") @@ -331,7 +331,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._disable_tracking(keep_main_alarm=True) # read file one character at a time, to get only the 1st - # value of each row, which is the arm angle + # value of each row, which is the fusion value data = array("f") buff = b"" f = open(self.filep, "rb") @@ -348,7 +348,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) continue if char == b"": # end of file break - elif not skip: # digit of arm angle value + elif not skip: # add digit of the fusion value buff += char f.close() From 0997c808f87f8dd8b5cf2b470157e8a03aa916d8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 20:37:20 +0100 Subject: [PATCH 182/485] print best sleep cycle length in notification --- SleepTk.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index a233388..3de3f6d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -421,7 +421,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) if s > max_sin: max_sin = s earlier = t # number of seconds earlier than wake up time - del max_sin, s, t, best_offset, best_omega + del max_sin, s, t gc.collect() system.set_alarm(min(WU_t - 5, # not after original wake up time @@ -432,7 +432,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._page = _TRACKING2 elapsed = timer.time() timer.stop() - msg = "Finished computing best wake up time in {}".format(elapsed) + msg = "Finished computing best wake up time in {}. Best fitting \ + offset: {}. Best sleep cycle duration: {:.2f}h".format(elapsed, best_offset, (_PIPI / _CONV / best_omega / 2 / 60 / 60)) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Finished smart alarm computation", "body": msg}) From f00357a5d7147b1eca33a64b28d374496fc5321e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 20:39:43 +0100 Subject: [PATCH 183/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7eaac92..5531f98 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ * if you actually use the watch during the night, make sure to count it as wakefulness? **misc** +* pressing the back button should return to home menu * turn off the Bluetooth connection when no phone is connected? * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. * find a way to remove outliers of stored values From 03a1466f0fc066278ab2cf236185d985e51702a7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 20:42:46 +0100 Subject: [PATCH 184/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5531f98..3964a52 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ * if you actually use the watch during the night, make sure to count it as wakefulness? **misc** +* recreate outdated UI screenshot + data sample * pressing the back button should return to home menu * turn off the Bluetooth connection when no phone is connected? * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. From aef3b0483692e249cc54762ab4c300e4814877c5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 22:27:28 +0100 Subject: [PATCH 185/485] refactored readme --- README.md | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 3964a52..c8f44f0 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,40 @@ # SleepTk : a sleep tracker and smart alarm for wasp-os -**Goal:** sleep tracker and smart alarm for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os), that wakes you up at the best time. +**Goal:** privacy friendly sleep tracker with smart alarm for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). -## Note to reader: -* I created this repository before even receiving my pine time and despite a very busy schedule to make sure no one else starts a similar project and end up duplicating efforts for nothing :) +## **How to install**: +*(for now you need my slightly forked wasp-os that allows to use accelerometer data)* +* download the latest [forked wasp-os](https://github.com/thiswillbeyourgithub/wasp-os) +* download the latest [SleepTk.py](./SleepTk.py) +* put the latest app in `wasp-os/wasp/apps/SleepTk.py` +* compile `wasp-os`: `make submodules && make softdevice && make BOARD=pinetime all && echo "SUCCESS"` +* upload it to your pinetime: `./tools/ota-dfu/dfu.py -z build-pinetime/micropython.zip -a XX:XX:XX:XX:XX:XX --legacy` +* reboot the watch and enjoy `SleepTk` + +### Note to reader: * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) -* Status as of end of February 2022: - * Finished the UI and the alarm but the smart alarm implementation is not at all tested. - * **Instructions**: - *(for now you need my forked wasp-os that exposes accelerometer data) - * download [my wasp-os fork](https://github.com/thiswillbeyourgithub/wasp-os) - * download the latest app : SleepTk.py - * put the latest app in wasp-os/wasp/apps/SleepTk.py - * compile and install wasp-os - * run the app - * *if you want, you can get back the data using `wasptool --pull`, then running the commands suggested below. +* Status as of end of February 2022: *UI (**done**), regular alarm (**done**), smart alarm (**mostly done but untested**)* +* you can download your sleep data file using `wasptool --pull logs/sleep/TIMESTAMP.csv`. I added below a suggestion of workflow to load it into [pandas](https://pypi.org/project/pandas/). # Screenshots: ![start](./screenshots/start_page.png) ![settings](./screenshots/settings_page.png) ![tracking](./screenshots/tracking_page.png) +![night example](./screenshots/example_night.png) ## TODO -**sleep tracking** -* try to roughly infer the sleep stage directly on the device? - * if you actually use the watch during the night, make sure to count it as wakefulness? - **misc** -* recreate outdated UI screenshot + data sample +* retake outdated UI screenshot + data sample * pressing the back button should return to home menu -* turn off the Bluetooth connection when no phone is connected? -* ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. -* find a way to remove outliers of stored values +* turn off the Bluetooth connection when beginning tracking + +**sleep tracking** +* infer light and deep sleep directly on the device **Features that I'm note sure yet** +* log smart alarm data to file +* log heart rate data every 10 minutes * should the watch ask you after waking up to rate your freshness at wake? +* ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. ## Related links: * article with detailed implementation : https://www.nature.com/articles/s41598-018-31266-z @@ -60,5 +61,3 @@ df["hours"] = df["human_time"].dt.time df = df.set_index("hours") df["fusion_value"].plot() ``` - -![night example](./screenshots/example_night.png) From 1f6c8d30b389d72d54fcd6759332bcab1ec90d81 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 22:34:06 +0100 Subject: [PATCH 186/485] mention features --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index c8f44f0..dfb55e6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # SleepTk : a sleep tracker and smart alarm for wasp-os **Goal:** privacy friendly sleep tracker with smart alarm for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). +## Features: +* **sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file +* alarm clock: wakes you up at a specific time +* **smart alarm clock**: can wake you up to 40 minutes before the set time to make sure you wake up feeling refreshed. +* **privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download if if needed) +* open source + ## **How to install**: *(for now you need my slightly forked wasp-os that allows to use accelerometer data)* * download the latest [forked wasp-os](https://github.com/thiswillbeyourgithub/wasp-os) From 92741ee6a8c8717b7c85d994b0e7a627e348118c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 22:54:05 +0100 Subject: [PATCH 187/485] skip useless atan import --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index dfb55e6..6d00de1 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,6 @@ Commands the author uses to take a look a the data using pandas: fname = "./logs/sleep/YOUR_TIME.csv" import pandas as pd -from math import atan - df = pd.read_csv(fname, names=["fusion_value", "time", "x_diff", "y_diff", "z_diff", "battery"]) offset = int(fname.split("/")[-1].split(".csv")[0]) df["human_time"] = pd.to_datetime(df["time"]+offset, unit='s') From cb7655bf0ea2d49388a5b7ce8540d01d0c50cd44 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 22:57:05 +0100 Subject: [PATCH 188/485] notify when starting computation --- SleepTk.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 3de3f6d..4ca8682 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -324,6 +324,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """computes best wake up time from sleep data""" self._is_computing = True gc.collect() + t = watch.time.localtime(time.time()) + system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", + "title": "Starting smart alarm computation", + "body": "Starting computation for the smart alarm at ".format(t[3], t[4]) + }) + del t try: timer = machine.Timer(id=1, period=8000000) timer.start() From ef2b777ee6e4fa02a896602ac9058705f1cb1d42 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 22:59:20 +0100 Subject: [PATCH 189/485] fusion value is now sum of diffs of each accel axis --- SleepTk.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 4ca8682..8ceb504 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -243,14 +243,16 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) y_diff = sum([abs(abs(buff[i]) - abs(buff[i+1])) for i in range(1, len(buff)-1, 3)]) / (self._data_point_nb - self._last_checkpoint) z_diff = sum([abs(abs(buff[i]) - abs(buff[i+1])) for i in range(2, len(buff)-1, 3)]) / (self._data_point_nb - self._last_checkpoint) buff = array("f") # reseting array - buff.append(abs(atan(z_diff / x_diff**2 + y_diff**2))) # note: math.atan() is faster than using a taylor serie + buff.append(x_diff + y_diff + z_diff) + #buff.append(abs(atan(z_diff / x_diff**2 + y_diff**2))) # formula from https://www.nature.com/articles/s41598-018-31266-z + # note: math.atan() is faster than using a taylor serie buff.append(int(rtc.time() - self._offset)) buff.append(x_diff) buff.append(y_diff) buff.append(z_diff) del x_diff, y_diff, z_diff - buff.append(battery.voltage_mv()) # currently more accurate than percent + buff.append(battery.voltage_mv()) # somewhat more accurate than percent f = open(self.filep, "ab") for x in buff[:-1]: From 94a2390a2a0147323c52e4a7d60096d5221fd222 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 23:02:29 +0100 Subject: [PATCH 190/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6d00de1..0f21756 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ ## TODO **misc** * retake outdated UI screenshot + data sample +* create a quick `.py` script to fetch the latest `TIMESTAMP.csv` * pressing the back button should return to home menu * turn off the Bluetooth connection when beginning tracking From d0a65e72b084e735d3703903d09e14eb92b061de Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 23:02:53 +0100 Subject: [PATCH 191/485] minor --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 8ceb504..146e789 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -20,14 +20,14 @@ from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView from shell import mkdir, cd from fonts import sans18 -from math import atan, sin +from math import sin from array import array from micropython import const # HARDCODED VARIABLES: _PIPI = const(628318) # result of 2*pi*100_000 _CONV = const(100000) # to get 2*pi -_START = const(0) # page values +_START = const(0) # page values: _TRACKING = const(1) _TRACKING2 = const(2) _SETTINGS = const(3) @@ -39,7 +39,7 @@ _STORE_FREQ = const(300) # process data and store to file every X seconds _SLEEP_CYCL_TRY = array("H", [4800, 5400, 6000]) # sleep cycle length to try (80, 90 and 100 minutes) _BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only keep the alarm -# user can want to edit this: +# user might want to edit this: _OFFSETS = array("H", [0, 600, 1200, 1800, 2400]) # possible offsets of sinus to try to fit to data (40 minutes, by increment of 10 minutes) From 6b9e37bd6006517c94c656e9dc3f544c64d358b3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Feb 2022 23:03:12 +0100 Subject: [PATCH 192/485] reduce accel freq to once every 30 seconds --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 146e789..96355e9 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -34,7 +34,7 @@ _SETTINGS = const(3) _RINGING = const(4) _FONT = sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_FREQ = const(30) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(300) # process data and store to file every X seconds _SLEEP_CYCL_TRY = array("H", [4800, 5400, 6000]) # sleep cycle length to try (80, 90 and 100 minutes) _BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only keep the alarm From c6b08100f2c6974896e23c1af0fe2c0e0d878bc1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 26 Feb 2022 14:50:10 +0100 Subject: [PATCH 193/485] todo --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f21756..bf08c8e 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,9 @@ ## TODO **misc** -* retake outdated UI screenshot + data sample +* retake outdated UI screenshot + data sample with the right time * create a quick `.py` script to fetch the latest `TIMESTAMP.csv` +* add a small factor that increases omega over the night. Because sleep cycle tend to be shorter over the night. That would really help the fitting * pressing the back button should return to home menu * turn off the Bluetooth connection when beginning tracking From e63a8d2fd36a18949895521827fa0482930b7c78 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 26 Feb 2022 15:06:48 +0100 Subject: [PATCH 194/485] docs: mention flexibility as a feature --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bf08c8e..3d34f83 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ## Features: * **sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file +* **Flexible**: does not make too many assumption regarding time to fall asleep, sleep cycle duration etc. SleepTk tries various data to see what fits best for your profile. If you still want to customize things, all the hardcoded and commented settings are easily accessible at the top of the file. * alarm clock: wakes you up at a specific time * **smart alarm clock**: can wake you up to 40 minutes before the set time to make sure you wake up feeling refreshed. * **privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download if if needed) From 9d9cc462e0ef72fcdef96199deac15746df2a12b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 26 Feb 2022 15:06:57 +0100 Subject: [PATCH 195/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3d34f83..b8ffbc8 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ * retake outdated UI screenshot + data sample with the right time * create a quick `.py` script to fetch the latest `TIMESTAMP.csv` * add a small factor that increases omega over the night. Because sleep cycle tend to be shorter over the night. That would really help the fitting +* move signal processing function to a separate class * pressing the back button should return to home menu * turn off the Bluetooth connection when beginning tracking From 2132a5eb6d39637a07830f27be7794f29e7a9e9e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 26 Feb 2022 15:12:45 +0100 Subject: [PATCH 196/485] style: prettier hour printing --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 96355e9..fe824a4 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -280,7 +280,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self.btn_al.draw() elif self._page == _TRACKING or self._page == _TRACKING2: ti = watch.time.localtime(self._offset) - draw.string('Started at {:2d}:{:2d}'.format(ti[3], ti[4]), 0, 70) + draw.string('Started at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 70) draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) if self._wakeup_enabled: word = "Alarm at " From 5e9dad82c5cbdec447ab5ae0c9bd91f1c9fd748a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 26 Feb 2022 15:18:36 +0100 Subject: [PATCH 197/485] changed my mind: arm angle is more accurate than summing the difference of axis --- README.md | 6 ++++-- SleepTk.py | 25 ++++++++++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b8ffbc8..a9a32d7 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,12 @@ Commands the author uses to take a look a the data using pandas: fname = "./logs/sleep/YOUR_TIME.csv" import pandas as pd -df = pd.read_csv(fname, names=["fusion_value", "time", "x_diff", "y_diff", "z_diff", "battery"]) +from math import atan + +df = pd.read_csv(fname, names=["angl_avg", "time", "x_avg", "y_avg", "z_avg", "battery"]) offset = int(fname.split("/")[-1].split(".csv")[0]) df["human_time"] = pd.to_datetime(df["time"]+offset, unit='s') df["hours"] = df["human_time"].dt.time df = df.set_index("hours") -df["fusion_value"].plot() +df["angl_avg"].plot() ``` diff --git a/SleepTk.py b/SleepTk.py index fe824a4..07eaea9 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -20,7 +20,7 @@ from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView from shell import mkdir, cd from fonts import sans18 -from math import sin +from math import sin, atan from array import array from micropython import const @@ -239,20 +239,19 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """save data after averageing over a window to file""" buff = self._buff if self._data_point_nb - self._last_checkpoint >= _STORE_FREQ / _FREQ: - x_diff = sum([abs(abs(buff[i]) - abs(buff[i+1])) for i in range(0, len(buff)-1, 3)]) / (self._data_point_nb - self._last_checkpoint) - y_diff = sum([abs(abs(buff[i]) - abs(buff[i+1])) for i in range(1, len(buff)-1, 3)]) / (self._data_point_nb - self._last_checkpoint) - z_diff = sum([abs(abs(buff[i]) - abs(buff[i+1])) for i in range(2, len(buff)-1, 3)]) / (self._data_point_nb - self._last_checkpoint) + x_avg = sum([buff[i] for i in range(0, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) + y_avg = sum([buff[i] for i in range(1, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) + z_avg = sum([buff[i] for i in range(2, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) buff = array("f") # reseting array - buff.append(x_diff + y_diff + z_diff) - #buff.append(abs(atan(z_diff / x_diff**2 + y_diff**2))) + buff.append(abs(atan(z_avg / (x_avg**2 + y_avg**2)))) # formula from https://www.nature.com/articles/s41598-018-31266-z # note: math.atan() is faster than using a taylor serie buff.append(int(rtc.time() - self._offset)) - buff.append(x_diff) - buff.append(y_diff) - buff.append(z_diff) - del x_diff, y_diff, z_diff - buff.append(battery.voltage_mv()) # somewhat more accurate than percent + buff.append(x_avg) + buff.append(y_avg) + buff.append(z_avg) + del x_avg, y_avg, z_avg + buff.append(battery.voltage_mv()) # currently more accurate than percent f = open(self.filep, "ab") for x in buff[:-1]: @@ -339,7 +338,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._disable_tracking(keep_main_alarm=True) # read file one character at a time, to get only the 1st - # value of each row, which is the fusion value + # value of each row, which is the arm angle data = array("f") buff = b"" f = open(self.filep, "rb") @@ -356,7 +355,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) continue if char == b"": # end of file break - elif not skip: # add digit of the fusion value + elif not skip: # digit of arm angle value buff += char f.close() From cdf3a42d9c0eaf31131979b80e19e249ea4479f4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 26 Feb 2022 15:19:01 +0100 Subject: [PATCH 198/485] new: set maximum digits written to file to 7 --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 07eaea9..38e9a2e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -255,9 +255,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f = open(self.filep, "ab") for x in buff[:-1]: - f.write("{}".format(x).encode()) + f.write("{:7f}".format(x).encode()) f.write(b",") - f.write("{}".format(buff[-1]).encode()) + f.write("{:7f}".format(buff[-1]).encode()) f.write(b"\n") f.close() From 959b879a8042ac1966c1c9c781ae7e0d8c7f9c19 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 26 Feb 2022 15:20:03 +0100 Subject: [PATCH 199/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a9a32d7..8d4999c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ * create a quick `.py` script to fetch the latest `TIMESTAMP.csv` * add a small factor that increases omega over the night. Because sleep cycle tend to be shorter over the night. That would really help the fitting * move signal processing function to a separate class +* test clipping all values above `3 x [mean value]`, to avoid too high peaks * pressing the back button should return to home menu * turn off the Bluetooth connection when beginning tracking From 0c427aedbf93e0ae0d2eb2f0c4973d59f4156ba9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 26 Feb 2022 17:52:28 +0100 Subject: [PATCH 200/485] feat: addd script to pull automatically last night's data --- README.md | 4 ++-- pull_latest_sleep_data.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 pull_latest_sleep_data.py diff --git a/README.md b/README.md index 8d4999c..e4506ed 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,12 @@ * compile `wasp-os`: `make submodules && make softdevice && make BOARD=pinetime all && echo "SUCCESS"` * upload it to your pinetime: `./tools/ota-dfu/dfu.py -z build-pinetime/micropython.zip -a XX:XX:XX:XX:XX:XX --legacy` * reboot the watch and enjoy `SleepTk` +* *optional: download your latest sleep data using the script `pull_latest_sleep_data.py`* ### Note to reader: * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) * Status as of end of February 2022: *UI (**done**), regular alarm (**done**), smart alarm (**mostly done but untested**)* -* you can download your sleep data file using `wasptool --pull logs/sleep/TIMESTAMP.csv`. I added below a suggestion of workflow to load it into [pandas](https://pypi.org/project/pandas/). +* you can download your sleep data file using the file `pull_latest_sleep_data`. A suggested workflow to load it into [pandas](https://pypi.org/project/pandas/) can be found at the bottom of the page. # Screenshots: ![start](./screenshots/start_page.png) @@ -32,7 +33,6 @@ ## TODO **misc** * retake outdated UI screenshot + data sample with the right time -* create a quick `.py` script to fetch the latest `TIMESTAMP.csv` * add a small factor that increases omega over the night. Because sleep cycle tend to be shorter over the night. That would really help the fitting * move signal processing function to a separate class * test clipping all values above `3 x [mean value]`, to avoid too high peaks diff --git a/pull_latest_sleep_data.py b/pull_latest_sleep_data.py new file mode 100644 index 0000000..5b28ee3 --- /dev/null +++ b/pull_latest_sleep_data.py @@ -0,0 +1,21 @@ +#!/usr/local/bin/python3 + + +import os +import subprocess +import shlex +import re + +ls_cmd = './tools/wasptool --verbose --eval \'from shell import ls ; ls(\"logs/sleep\")\'' +ls_cmd = shlex.split(ls_cmd) # properly split args +print("Listing remote files...") +out = subprocess.check_output(ls_cmd).decode() +files = re.findall(r"\d*\.csv", out) +print(f"Found files {', '.join(files)}") +latest = files[-1] +print(f"Most recent file is: {latest}") + +pull_cmd = f'./tools/wasptool --verbose --pull logs/sleep/{latest}' +os.system(pull_cmd) + +print(f"Succesfully downloaded file to 'logs/sleep/{latest}'") From 31ce2ddc24f5fd59821e743090bbbac1bf6c0fee Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 15:26:48 +0100 Subject: [PATCH 201/485] new: ability to download all files + skip already present --- pull_latest_sleep_data.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pull_latest_sleep_data.py b/pull_latest_sleep_data.py index 5b28ee3..17c9f0e 100644 --- a/pull_latest_sleep_data.py +++ b/pull_latest_sleep_data.py @@ -6,16 +6,29 @@ import subprocess import shlex import re -ls_cmd = './tools/wasptool --verbose --eval \'from shell import ls ; ls(\"logs/sleep\")\'' +mode = "all" # download "all" files or only "latest" +assert mode in ["all", "latest"] + +ls_cmd = './tools/wasptool --verbose --eval \'from shell import ls ; ls(\"/flash/logs/sleep/\")\'' ls_cmd = shlex.split(ls_cmd) # properly split args print("Listing remote files...") out = subprocess.check_output(ls_cmd).decode() files = re.findall(r"\d*\.csv", out) print(f"Found files {', '.join(files)}") -latest = files[-1] -print(f"Most recent file is: {latest}") -pull_cmd = f'./tools/wasptool --verbose --pull logs/sleep/{latest}' -os.system(pull_cmd) +if mode == "latest": + to_dl = files[-1] -print(f"Succesfully downloaded file to 'logs/sleep/{latest}'") +elif mode == "all": + to_dl = files + +print("\n\n") +for fi in to_dl: + if os.path.exists(f"./logs/sleep/{fi}"): + print(f"Skipping file {fi}: already exists") + else: + print(f"Downloading file '{fi}'") + pull_cmd = f'./tools/wasptool --verbose --pull logs/sleep/{fi}' + os.system(pull_cmd) + print(f"Succesfully downloaded to './logs/sleep/{fi}'") + print("\n\n") From a54bdb5994969a039c29e9508afceef5f0d74a4e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 15:33:02 +0100 Subject: [PATCH 202/485] run gc.collect first --- pull_latest_sleep_data.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pull_latest_sleep_data.py b/pull_latest_sleep_data.py index 17c9f0e..deff0e8 100644 --- a/pull_latest_sleep_data.py +++ b/pull_latest_sleep_data.py @@ -7,20 +7,25 @@ import shlex import re mode = "all" # download "all" files or only "latest" -assert mode in ["all", "latest"] +print("\n\nRunning gc.collect()...") +mem_cmd = './tools/wasptool --verbose --eval \'wasp.gc.collect()\'' +os.system(mem_cmd) + +print("\n\nListing remote files...") ls_cmd = './tools/wasptool --verbose --eval \'from shell import ls ; ls(\"/flash/logs/sleep/\")\'' ls_cmd = shlex.split(ls_cmd) # properly split args -print("Listing remote files...") out = subprocess.check_output(ls_cmd).decode() files = re.findall(r"\d*\.csv", out) print(f"Found files {', '.join(files)}") + if mode == "latest": to_dl = files[-1] - elif mode == "all": to_dl = files +else: + raise Exception("Wrong value for 'mode'") print("\n\n") for fi in to_dl: From 90f4b08f26721d9f2fd09c03f752cc2cbf6c6b27 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 15:33:11 +0100 Subject: [PATCH 203/485] remove useless import atan --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e4506ed..c2bd6e9 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,6 @@ Commands the author uses to take a look a the data using pandas: fname = "./logs/sleep/YOUR_TIME.csv" import pandas as pd -from math import atan df = pd.read_csv(fname, names=["angl_avg", "time", "x_avg", "y_avg", "z_avg", "battery"]) offset = int(fname.split("/")[-1].split(".csv")[0]) From 22726ae3b52070db2f0ca3fa10e841cd19e10a49 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 15:44:31 +0100 Subject: [PATCH 204/485] fix: store battery voltage as int --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 38e9a2e..c2bf2a1 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -251,7 +251,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff.append(y_avg) buff.append(z_avg) del x_avg, y_avg, z_avg - buff.append(battery.voltage_mv()) # currently more accurate than percent + buff.append(int(battery.voltage_mv())) # currently more accurate than percent f = open(self.filep, "ab") for x in buff[:-1]: From c2e509435e2fd1399335752b78bc52253c67400a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 18:19:27 +0100 Subject: [PATCH 205/485] better workflow suggestion --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index c2bd6e9..6196e7c 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,14 @@ Commands the author uses to take a look a the data using pandas: fname = "./logs/sleep/YOUR_TIME.csv" import pandas as pd +from array import array +from matplotlib import pyplot as plt df = pd.read_csv(fname, names=["angl_avg", "time", "x_avg", "y_avg", "z_avg", "battery"]) offset = int(fname.split("/")[-1].split(".csv")[0]) df["human_time"] = pd.to_datetime(df["time"]+offset, unit='s') df["hours"] = df["human_time"].dt.time df = df.set_index("hours") +data = array("f", df["angl_avg"].values[:-4]) df["angl_avg"].plot() ``` From 1f49e6ccde0b466202a855a30c0ea99cad692c29 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 18:20:11 +0100 Subject: [PATCH 206/485] new: move processing signal to a new function --- README.md | 1 - SleepTk.py | 122 +++++++++++++++++++++++++++++------------------------ 2 files changed, 67 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 6196e7c..1ee830a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ **misc** * retake outdated UI screenshot + data sample with the right time * add a small factor that increases omega over the night. Because sleep cycle tend to be shorter over the night. That would really help the fitting -* move signal processing function to a separate class * test clipping all values above `3 x [mean value]`, to avoid too high peaks * pressing the back button should return to home menu * turn off the Bluetooth connection when beginning tracking diff --git a/SleepTk.py b/SleepTk.py index c2bf2a1..2d3a724 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -321,6 +321,72 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self.stat_bar.clock = True self.stat_bar.draw() + def _signal_processing(self, data): + """signal processing over the data read from the local file""" + + # the first value HAS to be high because you were still awake + data[0] = 0.001 + + # remove outliers: + for x in range(len(data)): + if data[x] > 0.75*max(data): + data[x] = 0.75 * max(data) + + # smoothen several times + for j in range(2): + for i in range(1, len(data)-2): + data[i] += data[i-1] + data[i+1] + data[i] /= 3 + del i, j + gc.collect() + + # center and scale and clip between -1 and 1 + mean = sum(data) / len(data) + #mean = m(data) + std = ((sum([x**2 for x in data]) / len(data)) - mean**2)**0.5 + for i in range(len(data)): + data[i] = min(1, max(-1, (data[i] - mean) / std)) + del mean, std, i + gc.collect() + + # smoothen several times + for j in range(2): + for i in range(1, len(data)-2): + data[i] += data[i-1] + data[i+1] + data[i] /= 3 + del i, j + gc.collect() + + # for each sleep cycle, do a least square regression on each + # possible offset value of a sinusoidal wave with the same + # frequency as the sleep cycle + bof_p_cycl = array("H", [0] * len(_SLEEP_CYCL_TRY)) # best offset found per cycle + bfi_p_cycl = array("f", [0] * len(_SLEEP_CYCL_TRY)) # fit value for best offset + for cnt_cycle, cycle in enumerate(_SLEEP_CYCL_TRY): + omega = (_PIPI / _CONV) / (cycle * 2) # 2 * pi * frequency + fits = array("f") + # least square regression: + for cnt_offs, offset in enumerate(_OFFSETS): + fits.append(sum( + [(sin(omega * (i*_STORE_FREQ + offset)) - data[i])**4 + for i in range(len(data))] + )) + if fits[-1] == min(fits): + bof_p_cycl[cnt_cycle] = _OFFSETS[cnt_offs] + bfi_p_cycl[cnt_cycle] = min(fits) + #del fits, offset, cnt_cycle, cnt_offs, data, cycle, omega + gc.collect() + + # find sleep cycle and offset with the least fit: + for i, fit in enumerate(bfi_p_cycl): + if fit == min(bfi_p_cycl): + best_offset = bof_p_cycl[i] + best_omega = (_PIPI / _CONV) / (_SLEEP_CYCL_TRY[i] * 2) + break + + return (best_omega, best_offset) + + def _smart_alarm_compute(self): """computes best wake up time from sleep data""" self._is_computing = True @@ -362,61 +428,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del f, char, buff gc.collect() - # the first value HAS to be high because you were still awake - if data[0] < 0.75*max(data): - data[0] = 0.75*max(data) - - # smoothen several times - for j in range(5): - for i in range(1, len(data)-2): - data[i] += data[i-1] + data[i+1] - data[i] /= 3 - del i, j - gc.collect() - - # center and scale and clip between -1 and 1 - mean = sum(data) / len(data) - std = ((sum([x**2 for x in data]) / len(data)) - mean**2)**0.5 - for i in range(len(data)): - data[i] = min(1, max(-1, (data[i] - mean) / std)) - del mean, std, i - gc.collect() - - # smoothen several times - for j in range(5): - for i in range(1, len(data)-2): - data[i] += data[i-1] + data[i+1] - data[i] /= 3 - del i, j - gc.collect() - - # for each sleep cycle, do a least square regression on each - # possible offset value of a sinusoidal wave with the same - # frequency as the sleep cycle - bof_p_cycl = array("H", [0] * len(_SLEEP_CYCL_TRY)) # best offset found per cycle - bfi_p_cycl = array("f", [0] * len(_SLEEP_CYCL_TRY)) # fit value for best offset - for cnt_cycle, cycle in enumerate(_SLEEP_CYCL_TRY): - omega = (_PIPI / _CONV) / (cycle * 2) # 2 * pi * frequency - fits = array("f") - # least square regression: - for cnt_offs, offset in enumerate(_OFFSETS): - fits.append(sum( - [(sin(omega * (i*_STORE_FREQ + offset)) - data[i])**4 - for i in range(len(data))] - )) - if fits[-1] == min(fits): - bof_p_cycl[cnt_cycle] = _OFFSETS[cnt_offs] - bfi_p_cycl[cnt_cycle] = min(fits) - del fits, offset, cnt_cycle, cnt_offs, data, cycle, omega - gc.collect() - - # find sleep cycle and offset with the least fit: - for i, fit in enumerate(bfi_p_cycl): - if fit == min(bfi_p_cycl): - best_offset = bof_p_cycl[i] - best_omega = (_PIPI / _CONV) / (_SLEEP_CYCL_TRY[i] * 2) - break - del bof_p_cycl, bfi_p_cycl, i, fit + best_omega, best_offset = self._signal_processing(data) gc.collect() # finding how early to wake up: From 9edb46a50cf00a7fb91a71287450ef4b6439220c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 18:20:33 +0100 Subject: [PATCH 207/485] new: avoid setting first value of data to high value --- SleepTk.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 2d3a724..e862d7f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -324,9 +324,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _signal_processing(self, data): """signal processing over the data read from the local file""" - # the first value HAS to be high because you were still awake - data[0] = 0.001 - # remove outliers: for x in range(len(data)): if data[x] > 0.75*max(data): From 6769f97cfff334ac5ee17937e206224cf0c7abf6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 18:20:54 +0100 Subject: [PATCH 208/485] fix: don't delete timer just after initiating it --- SleepTk.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index e862d7f..9f37a09 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -393,7 +393,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) "title": "Starting smart alarm computation", "body": "Starting computation for the smart alarm at ".format(t[3], t[4]) }) - del t try: timer = machine.Timer(id=1, period=8000000) timer.start() From 6ff41a143fbeb6b93ebd021ce7f15ed20fc5ef47 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 19:41:57 +0100 Subject: [PATCH 209/485] better signal processing --- SleepTk.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 9f37a09..e6c090b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -325,9 +325,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """signal processing over the data read from the local file""" # remove outliers: + ma = 0.75*max(data) for x in range(len(data)): - if data[x] > 0.75*max(data): - data[x] = 0.75 * max(data) + if data[x] > ma: + data[x] = ma + del ma, x + gc.collect() # smoothen several times for j in range(2): @@ -339,14 +342,13 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # center and scale and clip between -1 and 1 mean = sum(data) / len(data) - #mean = m(data) std = ((sum([x**2 for x in data]) / len(data)) - mean**2)**0.5 for i in range(len(data)): data[i] = min(1, max(-1, (data[i] - mean) / std)) del mean, std, i gc.collect() - # smoothen several times + # smoothen for j in range(2): for i in range(1, len(data)-2): data[i] += data[i-1] + data[i+1] @@ -357,6 +359,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # for each sleep cycle, do a least square regression on each # possible offset value of a sinusoidal wave with the same # frequency as the sleep cycle + def compute_sin(amplitude, angular_freq, time, index, last_index): + return amplitude * (index/last_index) * sin(omega * time) + amp = max(data) bof_p_cycl = array("H", [0] * len(_SLEEP_CYCL_TRY)) # best offset found per cycle bfi_p_cycl = array("f", [0] * len(_SLEEP_CYCL_TRY)) # fit value for best offset for cnt_cycle, cycle in enumerate(_SLEEP_CYCL_TRY): @@ -365,13 +370,13 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # least square regression: for cnt_offs, offset in enumerate(_OFFSETS): fits.append(sum( - [(sin(omega * (i*_STORE_FREQ + offset)) - data[i])**4 + [(compute_sin(amp, omega, offset + i * _STORE_FREQ, i, len(data)) - data[i])**2 for i in range(len(data))] )) if fits[-1] == min(fits): bof_p_cycl[cnt_cycle] = _OFFSETS[cnt_offs] bfi_p_cycl[cnt_cycle] = min(fits) - #del fits, offset, cnt_cycle, cnt_offs, data, cycle, omega + del fits, offset, cnt_cycle, cnt_offs, data, cycle, omega gc.collect() # find sleep cycle and offset with the least fit: @@ -381,6 +386,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) best_omega = (_PIPI / _CONV) / (_SLEEP_CYCL_TRY[i] * 2) break + # to plot the best fitting function : + # plt.plot(data) ; plt.plot( [compute_sin(amp, best_omega, i*_STORE_FREQ + best_offset, i, len(data)) for i in range(len(data))] ) return (best_omega, best_offset) From b96032aaf9467a235d4e0ba92652aa4fb41c0369 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 19:43:06 +0100 Subject: [PATCH 210/485] todo --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ee830a..6a61037 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,8 @@ ## TODO **misc** * retake outdated UI screenshot + data sample with the right time -* add a small factor that increases omega over the night. Because sleep cycle tend to be shorter over the night. That would really help the fitting -* test clipping all values above `3 x [mean value]`, to avoid too high peaks * pressing the back button should return to home menu +* give up on sinus fitting, use instead a simple peak finder then take the mean / median distance between peaks as the best sleep cycle * turn off the Bluetooth connection when beginning tracking **sleep tracking** From f56dd97742fcf311fc9705fa3aaaa28fb4e59b5a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 20:00:52 +0100 Subject: [PATCH 211/485] write when no alarm set --- SleepTk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index e6c090b..b6f5a5a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -287,6 +287,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) word = "Alarm before " ti = watch.time.localtime(self._WU_t) draw.string("{}{:02d}:{:02d}".format(word, ti[3], ti[4]), 0, 130) + else: + draw.string("No alarm set", 0, 130) self.btn_off = Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == _START: From 21bc2bc23272c2d07ffeb3ebc354c56a7b60cc07 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Feb 2022 20:27:52 +0100 Subject: [PATCH 212/485] fix: don't allow checking hidden boxes in the settings --- SleepTk.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index b6f5a5a..4c74dc9 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -175,22 +175,23 @@ class SleepTkApp(): self._check_smart = None self._draw() - elif self.check_smart.touch(event): - if self._wakeup_smart_enabled == 1: - self._wakeup_smart_enabled = 0 - self.check_smart.state = self._wakeup_smart_enabled - self._check_smart = None - elif self._wakeup_enabled == 1: - self._wakeup_smart_enabled = 1 - self.check_smart.state = self._wakeup_smart_enabled - self.check_smart.update() - self.check_smart.draw() - elif self._spin_H.touch(event): - self._spinval_H = self._spin_H.value - self._spin_H.update() - elif self._spin_M.touch(event): - self._spinval_M = self._spin_M.value - self._spin_M.update() + if self.check_al.state: + if self.check_smart.touch(event): + if self._wakeup_smart_enabled == 1: + self._wakeup_smart_enabled = 0 + self.check_smart.state = self._wakeup_smart_enabled + self._check_smart = None + elif self._wakeup_enabled == 1: + self._wakeup_smart_enabled = 1 + self.check_smart.state = self._wakeup_smart_enabled + self.check_smart.update() + self.check_smart.draw() + elif self._spin_H.touch(event): + self._spinval_H = self._spin_H.value + self._spin_H.update() + elif self._spin_M.touch(event): + self._spinval_M = self._spin_M.value + self._spin_M.update() if no_full_draw is False: self._draw() From 10444e7580b673e3c424ad214bdd1dcd0a2c64ec Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 11:16:21 +0100 Subject: [PATCH 213/485] default increment value for spinner --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4c74dc9..49359cc 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -313,7 +313,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._spin_H = Spinner(30, 120, 0, 23, 2) self._spin_H.value = self._spinval_H self._spin_H.draw() - self._spin_M = Spinner(150, 120, 0, 59, 2) + self._spin_M = Spinner(150, 120, 0, 59, 2, 5) self._spin_M.value = self._spinval_M self._spin_M.draw() self.check_smart = Checkbox(x=0, y=80, label="Smart alarm") From 735791904f23d46015c718f2bfc33afc067ab57f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 12:23:23 +0100 Subject: [PATCH 214/485] untested: findind local maximas instead of using sinus fitting and least regression --- SleepTk.py | 157 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 106 insertions(+), 51 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 49359cc..20f9268 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -359,39 +359,97 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del i, j gc.collect() - # for each sleep cycle, do a least square regression on each - # possible offset value of a sinusoidal wave with the same - # frequency as the sleep cycle - def compute_sin(amplitude, angular_freq, time, index, last_index): - return amplitude * (index/last_index) * sin(omega * time) - amp = max(data) - bof_p_cycl = array("H", [0] * len(_SLEEP_CYCL_TRY)) # best offset found per cycle - bfi_p_cycl = array("f", [0] * len(_SLEEP_CYCL_TRY)) # fit value for best offset - for cnt_cycle, cycle in enumerate(_SLEEP_CYCL_TRY): - omega = (_PIPI / _CONV) / (cycle * 2) # 2 * pi * frequency - fits = array("f") - # least square regression: - for cnt_offs, offset in enumerate(_OFFSETS): - fits.append(sum( - [(compute_sin(amp, omega, offset + i * _STORE_FREQ, i, len(data)) - data[i])**2 - for i in range(len(data))] - )) - if fits[-1] == min(fits): - bof_p_cycl[cnt_cycle] = _OFFSETS[cnt_offs] - bfi_p_cycl[cnt_cycle] = min(fits) - del fits, offset, cnt_cycle, cnt_offs, data, cycle, omega + # find local maximas + x_maximas = array("f") + y_maximas = array("f") + window = int(60*60/_STORE_FREQ) # over 60 minutes + for start_w in range(len(data)) - window: + m = max(data[start_w:start_w+window] + for i in range(start_w, start_w + window): + if data[i] == m: + if i+start_w not in x_maximas: + x_maximas.append(i + start_w) + y_maximas.append(m) + + del window, start_w, i, m gc.collect() - # find sleep cycle and offset with the least fit: - for i, fit in enumerate(bfi_p_cycl): - if fit == min(bfi_p_cycl): - best_offset = bof_p_cycl[i] - best_omega = (_PIPI / _CONV) / (_SLEEP_CYCL_TRY[i] * 2) - break + # remove all peaks found in the first 60 minutes: + for i, x in enumerate(x_maximas): + if x*_STORE_FREQ < 3600: + y_maximas.remove(y_maximas[i]) + x_maximas.remove(x) + + # merge the smallest peaks while there are more than N peaks + N = 3 + while len(x_maximas) > N: + y_min = min(y_maximas) # find minimum + for i, y in y_maximas: # find location of minimum + if y == y_min: + x_min_idx = i + if x_min_idx == len(x_maxinas): # min is last, merging it with penultimate + closest = x_min_idx-1 + elif x_min_idx == 0: # min is first, merging it with 2nd + closest = x_min_idx+1 + else: # merge with closest + if x_maximas[x_min_idx-1] - x_maximas[x_min_idxn] < x_maximas[x_min_idx+1] - x_maximas[x_min_idxn]: + closest = x_min_idx-1 + else: + closest = x_min_idx+1 + y_maximas[closest] += y_maximas[x_min_idx] # adding peak values + x_maximas[closest] += x_maximas[x_min_idx] # averaging the x coordinate + x_maximas[closest] /= 2 + y_maximas.remove(y_maximas[x_min_idx]) + x_maximas.remove(x_maximas[x_min_idx]) + + # sleep cycle period is the time average distance between those N peaks + period = (x_maximas[-1] - x_maximas[0]) / N + + # if wake up time is in more time than last period but less than what + # SleepTk is allowed to anticipate: add new alarm at best time + last_peak_time = self._offset + x_maximas[-1] * _STORE_FREQ + WU_t = self._WU_t + allowed_time = WU_t - _OFFSETS[-1] + if last_peak_time + period < WU_t and last_peak_time + period > allowed_time: + earlier = WU_t - (last_peak_time + period) + else: + earlier = 0 + return (earlier, period) + + +# # for each sleep cycle, do a least square regression on each +# # possible offset value of a sinusoidal wave with the same +# # frequency as the sleep cycle +# def compute_sin(amplitude, angular_freq, time, index, last_index): +# return amplitude * (index/last_index) * sin(omega * time) +# amp = max(data) +# bof_p_cycl = array("H", [0] * len(_SLEEP_CYCL_TRY)) # best offset found per cycle +# bfi_p_cycl = array("f", [0] * len(_SLEEP_CYCL_TRY)) # fit value for best offset +# for cnt_cycle, cycle in enumerate(_SLEEP_CYCL_TRY): +# omega = (_PIPI / _CONV) / (cycle * 2) # 2 * pi * frequency +# fits = array("f") +# # least square regression: +# for cnt_offs, offset in enumerate(_OFFSETS): +# fits.append(sum( +# [(compute_sin(amp, omega, offset + i * _STORE_FREQ, i, len(data)) - data[i])**2 +# for i in range(len(data))] +# )) +# if fits[-1] == min(fits): +# bof_p_cycl[cnt_cycle] = _OFFSETS[cnt_offs] +# bfi_p_cycl[cnt_cycle] = min(fits) +# del fits, offset, cnt_cycle, cnt_offs, data, cycle, omega +# gc.collect() +# +# # find sleep cycle and offset with the least fit: +# for i, fit in enumerate(bfi_p_cycl): +# if fit == min(bfi_p_cycl): +# best_offset = bof_p_cycl[i] +# best_omega = (_PIPI / _CONV) / (_SLEEP_CYCL_TRY[i] * 2) +# break # to plot the best fitting function : # plt.plot(data) ; plt.plot( [compute_sin(amp, best_omega, i*_STORE_FREQ + best_offset, i, len(data)) for i in range(len(data))] ) - return (best_omega, best_offset) +# return (best_omega, best_offset) def _smart_alarm_compute(self): @@ -404,8 +462,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) "body": "Starting computation for the smart alarm at ".format(t[3], t[4]) }) try: - timer = machine.Timer(id=1, period=8000000) - timer.start() + start_time = rtc.time() # stop tracking to save memory, keep the alarm just in case self._disable_tracking(keep_main_alarm=True) @@ -434,31 +491,30 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del f, char, buff gc.collect() - best_omega, best_offset = self._signal_processing(data) - gc.collect() - - # finding how early to wake up: - max_sin = -1 + #best_omega, best_offset = self._signal_processing(data) + earlier, period = self._signal_processing(data) WU_t = self._WU_t - # counting backwards from original wake up time - for t in range(WU_t, WU_t - _OFFSETS[-1], -_STORE_FREQ): - s = sin(best_omega * (t + best_offset)) - if s > max_sin: - max_sin = s - earlier = t # number of seconds earlier than wake up time - del max_sin, s, t gc.collect() - system.set_alarm(min(WU_t - 5, # not after original wake up time - max(WU_t - earlier, # not before right now - int(rtc.time()) + 3) - ), self._listen_to_ticks) +# # finding how early to wake up: +# max_sin = -1 +# WU_t = self._WU_t +# # counting backwards from original wake up time +# for t in range(WU_t, WU_t - _OFFSETS[-1], -_STORE_FREQ): +# s = sin(best_omega * (t + best_offset)) +# if s > max_sin: +# max_sin = s +# earlier = t # number of seconds earlier than wake up time +# del max_sin, s, t +# gc.collect() + self._earlier = earlier + system.set_alarm(max(WU_t - earlier, int(rtc.time()) + 3), # not before right now, to make sure it rings + self._listen_to_ticks) self._page = _TRACKING2 - elapsed = timer.time() - timer.stop() - msg = "Finished computing best wake up time in {}. Best fitting \ - offset: {}. Best sleep cycle duration: {:.2f}h".format(elapsed, best_offset, (_PIPI / _CONV / best_omega / 2 / 60 / 60)) + msg = "Finished computing best wake up time in {:2f}.Best sleep cycle duration: {:.2f}h".format(rtc.time() - start_time, period) +# msg = "Finished computing best wake up time in {}. Best fitting \ +# offset: {}. Best sleep cycle duration: {:.2f}h".format(elapsed, best_offset, (_PIPI / _CONV / best_omega / 2 / 60 / 60)) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Finished smart alarm computation", "body": msg}) @@ -473,7 +529,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f.write(msg.encode()) f.close() finally: - t.stop() self._is_computing = False gc.collect() From b6d7983577f1184bf2f2336b568286dd75fd059d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 12:26:43 +0100 Subject: [PATCH 215/485] testing: remove code related to sinus fitting --- README.md | 1 - SleepTk.py | 62 ++++-------------------------------------------------- 2 files changed, 4 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 6a61037..1fd6788 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ **misc** * retake outdated UI screenshot + data sample with the right time * pressing the back button should return to home menu -* give up on sinus fitting, use instead a simple peak finder then take the mean / median distance between peaks as the best sleep cycle * turn off the Bluetooth connection when beginning tracking **sleep tracking** diff --git a/SleepTk.py b/SleepTk.py index 20f9268..4cd7183 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -25,8 +25,6 @@ from array import array from micropython import const # HARDCODED VARIABLES: -_PIPI = const(628318) # result of 2*pi*100_000 -_CONV = const(100000) # to get 2*pi _START = const(0) # page values: _TRACKING = const(1) _TRACKING2 = const(2) @@ -36,11 +34,10 @@ _FONT = sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(30) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(300) # process data and store to file every X seconds -_SLEEP_CYCL_TRY = array("H", [4800, 5400, 6000]) # sleep cycle length to try (80, 90 and 100 minutes) _BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: -_OFFSETS = array("H", [0, 600, 1200, 1800, 2400]) # possible offsets of sinus to try to fit to data (40 minutes, by increment of 10 minutes) +_ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set class SleepTkApp(): @@ -134,7 +131,7 @@ class SleepTkApp(): # wake up SleepTk 2min before earliest possible wake up if self._wakeup_smart_enabled: - self._WU_a = self._WU_t - _OFFSETS[-1] - 120 + self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 system.set_alarm(self._WU_a, self._smart_alarm_compute) self._page = _TRACKING @@ -381,7 +378,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) x_maximas.remove(x) # merge the smallest peaks while there are more than N peaks - N = 3 + N = 4 while len(x_maximas) > N: y_min = min(y_maximas) # find minimum for i, y in y_maximas: # find location of minimum @@ -409,49 +406,13 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # SleepTk is allowed to anticipate: add new alarm at best time last_peak_time = self._offset + x_maximas[-1] * _STORE_FREQ WU_t = self._WU_t - allowed_time = WU_t - _OFFSETS[-1] + allowed_time = WU_t - _ANTICIPATE_ALLOWED if last_peak_time + period < WU_t and last_peak_time + period > allowed_time: earlier = WU_t - (last_peak_time + period) else: earlier = 0 return (earlier, period) - -# # for each sleep cycle, do a least square regression on each -# # possible offset value of a sinusoidal wave with the same -# # frequency as the sleep cycle -# def compute_sin(amplitude, angular_freq, time, index, last_index): -# return amplitude * (index/last_index) * sin(omega * time) -# amp = max(data) -# bof_p_cycl = array("H", [0] * len(_SLEEP_CYCL_TRY)) # best offset found per cycle -# bfi_p_cycl = array("f", [0] * len(_SLEEP_CYCL_TRY)) # fit value for best offset -# for cnt_cycle, cycle in enumerate(_SLEEP_CYCL_TRY): -# omega = (_PIPI / _CONV) / (cycle * 2) # 2 * pi * frequency -# fits = array("f") -# # least square regression: -# for cnt_offs, offset in enumerate(_OFFSETS): -# fits.append(sum( -# [(compute_sin(amp, omega, offset + i * _STORE_FREQ, i, len(data)) - data[i])**2 -# for i in range(len(data))] -# )) -# if fits[-1] == min(fits): -# bof_p_cycl[cnt_cycle] = _OFFSETS[cnt_offs] -# bfi_p_cycl[cnt_cycle] = min(fits) -# del fits, offset, cnt_cycle, cnt_offs, data, cycle, omega -# gc.collect() -# -# # find sleep cycle and offset with the least fit: -# for i, fit in enumerate(bfi_p_cycl): -# if fit == min(bfi_p_cycl): -# best_offset = bof_p_cycl[i] -# best_omega = (_PIPI / _CONV) / (_SLEEP_CYCL_TRY[i] * 2) -# break - - # to plot the best fitting function : - # plt.plot(data) ; plt.plot( [compute_sin(amp, best_omega, i*_STORE_FREQ + best_offset, i, len(data)) for i in range(len(data))] ) -# return (best_omega, best_offset) - - def _smart_alarm_compute(self): """computes best wake up time from sleep data""" self._is_computing = True @@ -491,30 +452,15 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del f, char, buff gc.collect() - #best_omega, best_offset = self._signal_processing(data) earlier, period = self._signal_processing(data) WU_t = self._WU_t gc.collect() -# # finding how early to wake up: -# max_sin = -1 -# WU_t = self._WU_t -# # counting backwards from original wake up time -# for t in range(WU_t, WU_t - _OFFSETS[-1], -_STORE_FREQ): -# s = sin(best_omega * (t + best_offset)) -# if s > max_sin: -# max_sin = s -# earlier = t # number of seconds earlier than wake up time -# del max_sin, s, t -# gc.collect() - self._earlier = earlier system.set_alarm(max(WU_t - earlier, int(rtc.time()) + 3), # not before right now, to make sure it rings self._listen_to_ticks) self._page = _TRACKING2 msg = "Finished computing best wake up time in {:2f}.Best sleep cycle duration: {:.2f}h".format(rtc.time() - start_time, period) -# msg = "Finished computing best wake up time in {}. Best fitting \ -# offset: {}. Best sleep cycle duration: {:.2f}h".format(elapsed, best_offset, (_PIPI / _CONV / best_omega / 2 / 60 / 60)) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Finished smart alarm computation", "body": msg}) From a326527691449039fa8aab3e4106c9297e561fea Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 12:28:51 +0100 Subject: [PATCH 216/485] fix: forgot parenthesis --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4cd7183..44e8ae9 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -361,7 +361,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) y_maximas = array("f") window = int(60*60/_STORE_FREQ) # over 60 minutes for start_w in range(len(data)) - window: - m = max(data[start_w:start_w+window] + m = max(data[start_w:start_w+window]) for i in range(start_w, start_w + window): if data[i] == m: if i+start_w not in x_maximas: From 7e6639954520bfc9baba988f3dfac36a61b23217 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 12:52:21 +0100 Subject: [PATCH 217/485] minor --- SleepTk.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 44e8ae9..a1b6e37 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -367,7 +367,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) if i+start_w not in x_maximas: x_maximas.append(i + start_w) y_maximas.append(m) - del window, start_w, i, m gc.collect() @@ -376,6 +375,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) if x*_STORE_FREQ < 3600: y_maximas.remove(y_maximas[i]) x_maximas.remove(x) + del i, x + gc.collect() # merge the smallest peaks while there are more than N peaks N = 4 @@ -384,12 +385,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) for i, y in y_maximas: # find location of minimum if y == y_min: x_min_idx = i - if x_min_idx == len(x_maxinas): # min is last, merging it with penultimate + if x_min_idx == len(x_maximas): # min is last, merging it with penultimate closest = x_min_idx-1 elif x_min_idx == 0: # min is first, merging it with 2nd closest = x_min_idx+1 else: # merge with closest - if x_maximas[x_min_idx-1] - x_maximas[x_min_idxn] < x_maximas[x_min_idx+1] - x_maximas[x_min_idxn]: + if x_maximas[x_min_idx-1] - x_maximas[x_min_idx] < x_maximas[x_min_idx+1] - x_maximas[x_min_idx]: closest = x_min_idx-1 else: closest = x_min_idx+1 @@ -398,6 +399,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) x_maximas[closest] /= 2 y_maximas.remove(y_maximas[x_min_idx]) x_maximas.remove(x_maximas[x_min_idx]) + del closest, y_min, x_min_idx, i, y + gc.collect() # sleep cycle period is the time average distance between those N peaks period = (x_maximas[-1] - x_maximas[0]) / N @@ -410,7 +413,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) if last_peak_time + period < WU_t and last_peak_time + period > allowed_time: earlier = WU_t - (last_peak_time + period) else: - earlier = 0 + earlier = 0 # don't anticipate return (earlier, period) def _smart_alarm_compute(self): From a5282cb19a445fc743f970066648125ac50b5f62 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 14:17:31 +0100 Subject: [PATCH 218/485] new: pressing the button goes back to home menu except if in ringing mode --- SleepTk.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index a1b6e37..2cef5b9 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -13,7 +13,7 @@ alarm you set up manually. """ import time -from wasp import watch, system, EventMask, gc, machine +from wasp import watch, system, EventMask, EventType, gc, machine from watch import rtc, battery, accel from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView @@ -89,6 +89,8 @@ class SleepTkApp(): if self._page == _RINGING: self._disable_tracking() self._page = _START + else: + system.navigate(EventType.HOME) def swipe(self, event): "switches between start page and settings page" From b2d9899f24e3734822a227bd4c736d2e109dad3c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 15:44:55 +0100 Subject: [PATCH 219/485] new: after chatting with danielt, I should not implement sleep --- SleepTk.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 2cef5b9..7726e9a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -50,7 +50,6 @@ class SleepTkApp(): self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._conf_view = None - self._is_computing = False self._is_tracking = False self._earlier = 0 self._page = _START @@ -74,12 +73,6 @@ class SleepTkApp(): EventMask.SWIPE_UPDOWN | EventMask.BUTTON) - def sleep(self): - """stop sleeping when calculating smart alarm time""" - gc.collect() - if self._is_computing: - return False - def background(self): gc.collect() @@ -420,7 +413,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _smart_alarm_compute(self): """computes best wake up time from sleep data""" - self._is_computing = True gc.collect() t = watch.time.localtime(time.time()) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", @@ -479,8 +471,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f = open("smart_alarm_error_{}.txt".format(int(time.time())), "wb") f.write(msg.encode()) f.close() - finally: - self._is_computing = False gc.collect() From 9911ba5eb0888ec68f689508f7219ad027da9c3c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 15:45:42 +0100 Subject: [PATCH 220/485] new: wake screen but turn it off when computing best wake up time --- SleepTk.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 7726e9a..b199cef 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -414,6 +414,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _smart_alarm_compute(self): """computes best wake up time from sleep data""" gc.collect() + mute = watch.display.mute + mute(True) + system.wake() + system.switch(self) + mute(True) t = watch.time.localtime(time.time()) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Starting smart alarm computation", From 90dfcd773fd55a4100232ff877aaace42009fc5a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 15:45:59 +0100 Subject: [PATCH 221/485] minor --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index b199cef..af5b61a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -417,8 +417,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) mute = watch.display.mute mute(True) system.wake() - system.switch(self) mute(True) + system.switch(self) t = watch.time.localtime(time.time()) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Starting smart alarm computation", From 6baba87b50fd985965b9235f3463a2616daa6788 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 17:09:17 +0100 Subject: [PATCH 222/485] new: sends a bunch of keep_awake commands to keep computing the smart alarm --- SleepTk.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index af5b61a..e6ff355 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -326,6 +326,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) data[x] = ma del ma, x gc.collect() + system.keep_awake() # smoothen several times for j in range(2): @@ -334,6 +335,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) data[i] /= 3 del i, j gc.collect() + system.keep_awake() # center and scale and clip between -1 and 1 mean = sum(data) / len(data) @@ -342,6 +344,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) data[i] = min(1, max(-1, (data[i] - mean) / std)) del mean, std, i gc.collect() + system.keep_awake() # smoothen for j in range(2): @@ -350,6 +353,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) data[i] /= 3 del i, j gc.collect() + system.keep_awake() # find local maximas x_maximas = array("f") @@ -364,6 +368,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) y_maximas.append(m) del window, start_w, i, m gc.collect() + system.keep_awake() # remove all peaks found in the first 60 minutes: for i, x in enumerate(x_maximas): @@ -372,6 +377,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) x_maximas.remove(x) del i, x gc.collect() + system.keep_awake() # merge the smallest peaks while there are more than N peaks N = 4 @@ -396,6 +402,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) x_maximas.remove(x_maximas[x_min_idx]) del closest, y_min, x_min_idx, i, y gc.collect() + system.keep_awake() # sleep cycle period is the time average distance between those N peaks period = (x_maximas[-1] - x_maximas[0]) / N @@ -409,6 +416,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) earlier = WU_t - (last_peak_time + period) else: earlier = 0 # don't anticipate + system.keep_awake() return (earlier, period) def _smart_alarm_compute(self): @@ -453,6 +461,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f.close() del f, char, buff gc.collect() + system.keep_awake() earlier, period = self._signal_processing(data) WU_t = self._WU_t From 5d6e6148872b3bc2e4d0a4c5fac2173aba324f4e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 17:09:33 +0100 Subject: [PATCH 223/485] fix: wrong notification text --- SleepTk.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index e6ff355..4b5e20e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -428,10 +428,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) mute(True) system.switch(self) t = watch.time.localtime(time.time()) - system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", - "title": "Starting smart alarm computation", - "body": "Starting computation for the smart alarm at ".format(t[3], t[4]) - }) + system.notify(watch.rtc.get_uptime_ms(), + {"src": "SleepTk", + "title": "Starting smart alarm computation", + "body": "Starting computation for the smart alarm at {:02d}h{:02d}m".format(t[3], t[4])} + ) try: start_time = rtc.time() # stop tracking to save memory, keep the alarm just in case @@ -471,14 +472,14 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) system.set_alarm(max(WU_t - earlier, int(rtc.time()) + 3), # not before right now, to make sure it rings self._listen_to_ticks) self._page = _TRACKING2 - msg = "Finished computing best wake up time in {:2f}.Best sleep cycle duration: {:.2f}h".format(rtc.time() - start_time, period) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Finished smart alarm computation", - "body": msg}) + "body": "Finished computing best wake up time in {:2f}s. Best sleep cycle duration: {:.2f}h".format(rtc.time() - start_time, period) + }) except Exception as e: gc.collect() t = watch.time.localtime(time.time()) - msg = "Exception occured at {}h{}m: '{}'%".format(t[3], t[4], str(e)) + msg = "Exception occured at {:02d}h{:02d}m: '{}'%".format(t[3], t[4], str(e)) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Smart alarm error", "body": msg}) From 53d5ce3128f65bde7fb2b97367b856da1a6d3ebf Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 17:48:33 +0100 Subject: [PATCH 224/485] todo --- README.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1fd6788..bf3717e 100644 --- a/README.md +++ b/README.md @@ -32,18 +32,14 @@ ## TODO **misc** +* make sure smart alarm works with the new local maximum method * retake outdated UI screenshot + data sample with the right time -* pressing the back button should return to home menu -* turn off the Bluetooth connection when beginning tracking +* add a power nap mode that wakes you as soon as there has been no movement for 5 minutes +* when smart alarm is on but could not find any better wake up time, vibrates just a tiny bit every 5 minutes before waking up to make you gradually come back from sleep. -**sleep tracking** -* infer light and deep sleep directly on the device - -**Features that I'm note sure yet** -* log smart alarm data to file -* log heart rate data every 10 minutes -* should the watch ask you after waking up to rate your freshness at wake? -* ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation. +* log heart rate data every X minutes? +* log smart alarm data to file? log user rating of how well he/she felt fresh at wake? +* ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation? ## Related links: * article with detailed implementation : https://www.nature.com/articles/s41598-018-31266-z From 2bd77dd5dfe2209ec75938aef9ebb5103a59f959 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 18:35:15 +0100 Subject: [PATCH 225/485] fix: called time.time instead of rtc.time --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 4b5e20e..65fe7f6 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -219,7 +219,7 @@ class SleepTkApp(): # strop tracking if battery low self._disable_tracking(keep_main_alarm=True) self._wakeup_smart_enabled = 0 - h, m = watch.time.localtime(time.time())[3:5] + h, m = watch.time.localtime(rtc.time())[3:5] system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Bat <20%", "body": "Stopped \ @@ -427,7 +427,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) system.wake() mute(True) system.switch(self) - t = watch.time.localtime(time.time()) + t = watch.time.localtime(rtc.time()) system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Starting smart alarm computation", From 729a68de929aa73f39fc040453620ffc3a9020d5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 18:53:44 +0100 Subject: [PATCH 226/485] feat: implement gradual wakeup --- README.md | 1 - SleepTk.py | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bf3717e..f2cc6df 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ * make sure smart alarm works with the new local maximum method * retake outdated UI screenshot + data sample with the right time * add a power nap mode that wakes you as soon as there has been no movement for 5 minutes -* when smart alarm is on but could not find any better wake up time, vibrates just a tiny bit every 5 minutes before waking up to make you gradually come back from sleep. * log heart rate data every X minutes? * log smart alarm data to file? log user rating of how well he/she felt fresh at wake? diff --git a/SleepTk.py b/SleepTk.py index 65fe7f6..59f5c2e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -38,6 +38,7 @@ _BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only ke # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set +_GRADUAL_WAKE = array("H", [1, 2, 3, 5, 8, 13, 20]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up class SleepTkApp(): @@ -124,6 +125,11 @@ class SleepTkApp(): self._WU_t = time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) system.set_alarm(self._WU_t, self._listen_to_ticks) + # also set alarm to vibrate a tiny bit before wake up time + # to wake up gradually + for t in _GRADUAL_WAKE: + system.set_alarm(self._WU_t - t*60, self._tiny_vibration) + # wake up SleepTk 2min before earliest possible wake up if self._wakeup_smart_enabled: self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 @@ -503,3 +509,14 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """vibrate to wake you up""" if self._page == _RINGING: watch.vibrator.pulse(duty=50, ms=500) + + def _tiny_vibration(self): + """vibrate just a tiny bit before waking up, to gradually return + to consciousness""" + gc.collect() + mute = watch.display.mute + mute(True) + system.wake() + system.keep_awake() + system.switch(self) + watch.vibrator.pulse(duty=60, ms=100) From d6dc8d0babb179ad39d5b07f70e5383d19f66d9a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 18:54:51 +0100 Subject: [PATCH 227/485] avoid calling mute(True) too many times and call keep_awake once more --- SleepTk.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 59f5c2e..ff0659c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -431,7 +431,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) mute = watch.display.mute mute(True) system.wake() - mute(True) + system.keep_awake() system.switch(self) t = watch.time.localtime(rtc.time()) system.notify(watch.rtc.get_uptime_ms(), @@ -499,6 +499,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """listen to ticks every second, telling the watch to vibrate""" gc.collect() self._page = _RINGING + mute = watch.display.mute + mute(True) system.wake() system.keep_awake() system.switch(self) From cfed213aa117da64c9ece5d92fbbda7374547ce8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 18:57:03 +0100 Subject: [PATCH 228/485] remove import of sin --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index ff0659c..a08bfbc 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -20,7 +20,7 @@ from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView from shell import mkdir, cd from fonts import sans18 -from math import sin, atan +from math import atan from array import array from micropython import const From 469ce6181f66246540719d54d0b6ce49300a2db7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 19:11:43 +0100 Subject: [PATCH 229/485] new: import package without creating variables --- SleepTk.py | 227 ++++++++++++++++++++++++++--------------------------- 1 file changed, 112 insertions(+), 115 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index a08bfbc..4e62494 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -13,39 +13,36 @@ alarm you set up manually. """ import time -from wasp import watch, system, EventMask, EventType, gc, machine -from watch import rtc, battery, accel - -from widgets import Button, Spinner, Checkbox, StatusBar, ConfirmationView -from shell import mkdir, cd -from fonts import sans18 - -from math import atan -from array import array -from micropython import const +import wasp +import widgets +import shell +import fonts +import math +import array +import micropython # HARDCODED VARIABLES: -_START = const(0) # page values: -_TRACKING = const(1) -_TRACKING2 = const(2) -_SETTINGS = const(3) -_RINGING = const(4) -_FONT = sans18 -_TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = const(30) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_STORE_FREQ = const(300) # process data and store to file every X seconds -_BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only keep the alarm +_START = micropython.const(0) # page values: +_TRACKING = micropython.const(1) +_TRACKING2 = micropython.const(2) +_SETTINGS = micropython.const(3) +_RINGING = micropython.const(4) +_FONT = fonts.sans18 +_TIMESTAMP = micropython.const(946684800) # unix time and time used by wasp os don't have the same reference date +_FREQ = micropython.const(30) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_STORE_FREQ = micropython.const(300) # process data and store to file every X seconds +_BATTERY_THRESHOLD = micropython.const(20) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: -_ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set -_GRADUAL_WAKE = array("H", [1, 2, 3, 5, 8, 13, 20]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up +_ANTICIPATE_ALLOWED = micropython.const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set +_GRADUAL_WAKE = array.array("H", [1, 2, 3, 5, 8, 13, 20]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up class SleepTkApp(): NAME = 'SleepTk' def __init__(self): - gc.collect() + wasp.gc.collect() self._wakeup_enabled = 1 self._wakeup_smart_enabled = 0 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._spinval_H = 7 # default wake up time @@ -56,26 +53,26 @@ class SleepTkApp(): self._page = _START try: - mkdir("logs/") + shell.mkdir("logs/") except: # folder already exists pass cd("logs") try: - mkdir("sleep") + shell.mkdir("logs/sleep") except: # folder already exists pass cd("..") def foreground(self): self._conf_view = None - gc.collect() + wasp.gc.collect() self._draw() - system.request_event(EventMask.TOUCH | - EventMask.SWIPE_UPDOWN | - EventMask.BUTTON) + wasp.system.request_event(wasp.EventMask.TOUCH | + wasp.EventMask.SWIPE_UPDOWN | + wasp.EventMask.BUTTON) def background(self): - gc.collect() + wasp.gc.collect() def press(self, button, state): "stop ringing alarm if pressed physical button" @@ -84,7 +81,7 @@ class SleepTkApp(): self._disable_tracking() self._page = _START else: - system.navigate(EventType.HOME) + wasp.system.navigate(wasp.EventType.HOME) def swipe(self, event): "switches between start page and settings page" @@ -97,16 +94,16 @@ class SleepTkApp(): def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" - gc.collect() + wasp.gc.collect() no_full_draw = False if self._page == _START: if self.btn_on.touch(event): self._is_tracking = True # accel data not yet written to disk: - self._buff = array("f") + self._buff = array.array("f") self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file - self._offset = int(rtc.time()) # makes output more compact + self._offset = int(wasp.watch.rtc.time()) # makes output more compact # create one file per recording session: self.filep = "logs/sleep/{}.csv".format(str(self._offset + _TIMESTAMP)) @@ -114,7 +111,7 @@ class SleepTkApp(): # setting up alarm if self._wakeup_enabled: - now = rtc.get_localtime() + now = wasp.watch.rtc.get_localtime() yyyy = now[0] mm = now[1] dd = now[2] @@ -123,23 +120,23 @@ class SleepTkApp(): if HH < now[3] or (HH == now[3] and MM <= now[4]): dd += 1 self._WU_t = time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) - system.set_alarm(self._WU_t, self._listen_to_ticks) + wasp.system.set_alarm(self._WU_t, self._listen_to_ticks) # also set alarm to vibrate a tiny bit before wake up time # to wake up gradually for t in _GRADUAL_WAKE: - system.set_alarm(self._WU_t - t*60, self._tiny_vibration) + wasp.system.set_alarm(self._WU_t - t*60, self._tiny_vibration) # wake up SleepTk 2min before earliest possible wake up if self._wakeup_smart_enabled: self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 - system.set_alarm(self._WU_a, self._smart_alarm_compute) + wasp.system.set_alarm(self._WU_a, self._smart_alarm_compute) self._page = _TRACKING elif self._page == _TRACKING or self._page == _TRACKING2: if self._conf_view is None: if self.btn_off.touch(event): - self._conf_view = ConfirmationView() + self._conf_view = widgets.ConfirmationView() self._conf_view.draw("Stop tracking?") no_full_draw = True else: @@ -198,41 +195,41 @@ class SleepTkApp(): """called by touching "STOP TRACKING" or when computing best alarm time to wake up you disables tracking features and alarms""" self._is_tracking = False - system.cancel_alarm(self.next_al, self._trackOnce) + wasp.system.cancel_alarm(self.next_al, self._trackOnce) if self._wakeup_enabled: if keep_main_alarm is False: # to keep the alarm when stopping because of low battery - system.cancel_alarm(self._WU_t, self._listen_to_ticks) + wasp.system.cancel_alarm(self._WU_t, self._listen_to_ticks) if self._wakeup_smart_enabled: - system.cancel_alarm(self._WU_a, self._smart_alarm_compute) + wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_compute) self._periodicSave() - gc.collect() + wasp.gc.collect() def _add_accel_alar(self): """set an alarm, due in _FREQ minutes, to log the accelerometer data once""" - self.next_al = watch.rtc.time() + _FREQ - system.set_alarm(self.next_al, self._trackOnce) + self.next_al = wasp.watch.rtc.time() + _FREQ + wasp.system.set_alarm(self.next_al, self._trackOnce) def _trackOnce(self): """get one data point of accelerometer every _FREQ seconds and they are then averaged and stored every _STORE_FREQ seconds""" if self._is_tracking: - [self._buff.append(x) for x in accel.read_xyz()] + [self._buff.append(x) for x in wasp.watch.accel.read_xyz()] self._data_point_nb += 1 self._add_accel_alar() self._periodicSave() - if battery.level() <= _BATTERY_THRESHOLD: + if wasp.watch.battery.level() <= _BATTERY_THRESHOLD: # strop tracking if battery low self._disable_tracking(keep_main_alarm=True) self._wakeup_smart_enabled = 0 - h, m = watch.time.localtime(rtc.time())[3:5] - system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", + h, m = watch.time.localtime(wasp.watch.rtc.time())[3:5] + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Bat <20%", "body": "Stopped \ tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ on.".format(h, m, _BATTERY_THRESHOLD)}) - gc.collect() + wasp.gc.collect() def _periodicSave(self): """save data after averageing over a window to file""" @@ -241,16 +238,16 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) x_avg = sum([buff[i] for i in range(0, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) y_avg = sum([buff[i] for i in range(1, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) z_avg = sum([buff[i] for i in range(2, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) - buff = array("f") # reseting array - buff.append(abs(atan(z_avg / (x_avg**2 + y_avg**2)))) + buff = array.array("f") # reseting array + buff.append(abs(math.atan(z_avg / (x_avg**2 + y_avg**2)))) # formula from https://www.nature.com/articles/s41598-018-31266-z # note: math.atan() is faster than using a taylor serie - buff.append(int(rtc.time() - self._offset)) + buff.append(int(wasp.watch.rtc.time() - self._offset)) buff.append(x_avg) buff.append(y_avg) buff.append(z_avg) del x_avg, y_avg, z_avg - buff.append(int(battery.voltage_mv())) # currently more accurate than percent + buff.append(int(wasp.watch.battery.voltage_mv())) # currently more accurate than percent f = open(self.filep, "ab") for x in buff[:-1]: @@ -261,11 +258,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f.close() self._last_checkpoint = self._data_point_nb - self._buff = array("f") + self._buff = array.array("f") def _draw(self): """GUI""" - draw = watch.drawable + draw = wasp.watch.drawable draw.fill(0) draw.set_font(_FONT) if self._page == _RINGING: @@ -274,24 +271,24 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) else: msg = "WAKE UP" draw.string(msg, 0, 70) - self.btn_al = Button(x=0, y=70, w=240, h=140, label="WAKE UP") + self.btn_al = widgets.Button(x=0, y=70, w=240, h=140, label="WAKE UP") self.btn_al.draw() elif self._page == _TRACKING or self._page == _TRACKING2: - ti = watch.time.localtime(self._offset) + ti = wasp.watch.time.localtime(self._offset) draw.string('Started at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 70) draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) if self._wakeup_enabled: word = "Alarm at " if self._wakeup_smart_enabled: word = "Alarm before " - ti = watch.time.localtime(self._WU_t) + ti = wasp.watch.time.localtime(self._WU_t) draw.string("{}{:02d}:{:02d}".format(word, ti[3], ti[4]), 0, 130) else: draw.string("No alarm set", 0, 130) - self.btn_off = Button(x=0, y=200, w=240, h=40, label="Stop tracking") + self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() elif self._page == _START: - self.btn_on = Button(x=0, y=200, w=240, h=40, label="Start tracking") + self.btn_on = widgets.Button(x=0, y=200, w=240, h=40, label="Start tracking") self.btn_on.draw() draw.set_font(_FONT) draw.string('Sleep tracker with' , 0, 60) @@ -304,21 +301,21 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) draw.string('earlier.' , 0, 140) draw.string('PRE RELEASE.' , 0, 160) elif self._page == _SETTINGS: - self.check_al = Checkbox(x=0, y=40, label="Alarm") + self.check_al = widgets.Checkbox(x=0, y=40, label="Alarm") self.check_al.state = self._wakeup_enabled self.check_al.draw() if self._wakeup_enabled: - self._spin_H = Spinner(30, 120, 0, 23, 2) + self._spin_H = widgets.Spinner(30, 120, 0, 23, 2) self._spin_H.value = self._spinval_H self._spin_H.draw() - self._spin_M = Spinner(150, 120, 0, 59, 2, 5) + self._spin_M = widgets.Spinner(150, 120, 0, 59, 2, 5) self._spin_M.value = self._spinval_M self._spin_M.draw() - self.check_smart = Checkbox(x=0, y=80, label="Smart alarm") + self.check_smart = widgets.Checkbox(x=0, y=80, label="Smart alarm") self.check_smart.state = self._wakeup_smart_enabled self.check_smart.draw() - self.stat_bar = StatusBar() + self.stat_bar = widgets.StatusBar() self.stat_bar.clock = True self.stat_bar.draw() @@ -331,8 +328,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) if data[x] > ma: data[x] = ma del ma, x - gc.collect() - system.keep_awake() + wasp.gc.collect() + wasp.system.keep_awake() # smoothen several times for j in range(2): @@ -340,8 +337,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) data[i] += data[i-1] + data[i+1] data[i] /= 3 del i, j - gc.collect() - system.keep_awake() + wasp.gc.collect() + wasp.system.keep_awake() # center and scale and clip between -1 and 1 mean = sum(data) / len(data) @@ -349,8 +346,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) for i in range(len(data)): data[i] = min(1, max(-1, (data[i] - mean) / std)) del mean, std, i - gc.collect() - system.keep_awake() + wasp.gc.collect() + wasp.system.keep_awake() # smoothen for j in range(2): @@ -358,12 +355,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) data[i] += data[i-1] + data[i+1] data[i] /= 3 del i, j - gc.collect() - system.keep_awake() + wasp.gc.collect() + wasp.system.keep_awake() # find local maximas - x_maximas = array("f") - y_maximas = array("f") + x_maximas = array.array("f") + y_maximas = array.array("f") window = int(60*60/_STORE_FREQ) # over 60 minutes for start_w in range(len(data)) - window: m = max(data[start_w:start_w+window]) @@ -373,8 +370,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) x_maximas.append(i + start_w) y_maximas.append(m) del window, start_w, i, m - gc.collect() - system.keep_awake() + wasp.gc.collect() + wasp.system.keep_awake() # remove all peaks found in the first 60 minutes: for i, x in enumerate(x_maximas): @@ -382,8 +379,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) y_maximas.remove(y_maximas[i]) x_maximas.remove(x) del i, x - gc.collect() - system.keep_awake() + wasp.gc.collect() + wasp.system.keep_awake() # merge the smallest peaks while there are more than N peaks N = 4 @@ -407,8 +404,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) y_maximas.remove(y_maximas[x_min_idx]) x_maximas.remove(x_maximas[x_min_idx]) del closest, y_min, x_min_idx, i, y - gc.collect() - system.keep_awake() + wasp.gc.collect() + wasp.system.keep_awake() # sleep cycle period is the time average distance between those N peaks period = (x_maximas[-1] - x_maximas[0]) / N @@ -422,31 +419,31 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) earlier = WU_t - (last_peak_time + period) else: earlier = 0 # don't anticipate - system.keep_awake() + wasp.system.keep_awake() return (earlier, period) def _smart_alarm_compute(self): """computes best wake up time from sleep data""" - gc.collect() - mute = watch.display.mute + wasp.gc.collect() + mute = wasp.watch.display.mute mute(True) - system.wake() - system.keep_awake() - system.switch(self) - t = watch.time.localtime(rtc.time()) - system.notify(watch.rtc.get_uptime_ms(), + wasp.system.wake() + wasp.system.keep_awake() + wasp.system.switch(self) + t = wasp.watch.time.localtime(wasp.watch.rtc.time()) + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Starting smart alarm computation", "body": "Starting computation for the smart alarm at {:02d}h{:02d}m".format(t[3], t[4])} ) try: - start_time = rtc.time() + start_time = wasp.watch.rtc.time() # stop tracking to save memory, keep the alarm just in case self._disable_tracking(keep_main_alarm=True) # read file one character at a time, to get only the 1st # value of each row, which is the arm angle - data = array("f") + data = array.array("f") buff = b"" f = open(self.filep, "rb") skip = False @@ -467,58 +464,58 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f.close() del f, char, buff - gc.collect() - system.keep_awake() + wasp.gc.collect() + wasp.system.keep_awake() earlier, period = self._signal_processing(data) WU_t = self._WU_t - gc.collect() + wasp.gc.collect() self._earlier = earlier - system.set_alarm(max(WU_t - earlier, int(rtc.time()) + 3), # not before right now, to make sure it rings + wasp.system.set_alarm(max(WU_t - earlier, int(wasp.watch.rtc.time()) + 3), # not before right now, to make sure it rings self._listen_to_ticks) self._page = _TRACKING2 - system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Finished smart alarm computation", - "body": "Finished computing best wake up time in {:2f}s. Best sleep cycle duration: {:.2f}h".format(rtc.time() - start_time, period) + "body": "Finished computing best wake up time in {:2f}s. Best sleep cycle duration: {:.2f}h".format(wasp.watch.rtc.time() - start_time, period) }) except Exception as e: - gc.collect() - t = watch.time.localtime(time.time()) + wasp.gc.collect() + t = wasp.watch.time.localtime(time.time()) msg = "Exception occured at {:02d}h{:02d}m: '{}'%".format(t[3], t[4], str(e)) - system.notify(watch.rtc.get_uptime_ms(), {"src": "SleepTk", + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Smart alarm error", "body": msg}) f = open("smart_alarm_error_{}.txt".format(int(time.time())), "wb") f.write(msg.encode()) f.close() - gc.collect() + wasp.gc.collect() def _listen_to_ticks(self): """listen to ticks every second, telling the watch to vibrate""" - gc.collect() + wasp.gc.collect() self._page = _RINGING - mute = watch.display.mute + mute = wasp.watch.display.mute mute(True) - system.wake() - system.keep_awake() - system.switch(self) + wasp.system.wake() + wasp.system.keep_awake() + wasp.system.switch(self) self._draw() - system.request_tick(period_ms=1000) + wasp.system.request_tick(period_ms=1000) def tick(self, ticks): """vibrate to wake you up""" if self._page == _RINGING: - watch.vibrator.pulse(duty=50, ms=500) + wasp.watch.vibrator.pulse(duty=50, ms=500) def _tiny_vibration(self): """vibrate just a tiny bit before waking up, to gradually return to consciousness""" - gc.collect() - mute = watch.display.mute + wasp.gc.collect() + mute = wasp.watch.display.mute mute(True) - system.wake() - system.keep_awake() - system.switch(self) - watch.vibrator.pulse(duty=60, ms=100) + wasp.system.wake() + wasp.system.keep_awake() + wasp.system.switch(self) + wasp.watch.vibrator.pulse(duty=60, ms=100) From 0a4ca3be7cf13ae7bbe072afd669b568a634c0f6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 19:11:56 +0100 Subject: [PATCH 230/485] useless to use cd to use mkdir --- SleepTk.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 4e62494..9c70167 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -56,12 +56,10 @@ class SleepTkApp(): shell.mkdir("logs/") except: # folder already exists pass - cd("logs") try: shell.mkdir("logs/sleep") except: # folder already exists pass - cd("..") def foreground(self): self._conf_view = None From 10b51739c15d01aaec883697660222bc62539ee1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 21:56:51 +0100 Subject: [PATCH 231/485] fix: wrong import statement for time --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 9c70167..e475860 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -479,12 +479,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) }) except Exception as e: wasp.gc.collect() - t = wasp.watch.time.localtime(time.time()) + t = wasp.watch.time.localtime(wasp.watch.rtc.time()) msg = "Exception occured at {:02d}h{:02d}m: '{}'%".format(t[3], t[4], str(e)) wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Smart alarm error", "body": msg}) - f = open("smart_alarm_error_{}.txt".format(int(time.time())), "wb") + f = open("smart_alarm_error_{}.txt".format(int(wasp.watch.rtc.time())), "wb") f.write(msg.encode()) f.close() wasp.gc.collect() From 12c95cb7d43da91fa1a3453ad77db069162629d6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 21:57:24 +0100 Subject: [PATCH 232/485] add gradual wake timestep --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index e475860..883d8ce 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -35,7 +35,7 @@ _BATTERY_THRESHOLD = micropython.const(20) # under X% of battery, stop tracking # user might want to edit this: _ANTICIPATE_ALLOWED = micropython.const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set -_GRADUAL_WAKE = array.array("H", [1, 2, 3, 5, 8, 13, 20]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up +_GRADUAL_WAKE = array.array("H", [1, 2, 3, 4, 5, 8, 13, 20]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up class SleepTkApp(): From 3af2bd3c0d701ab136b51420ff9c42f78d6d103e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 22:05:09 +0100 Subject: [PATCH 233/485] feat: make notifications silent dring the night --- SleepTk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 883d8ce..cb09704 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -51,6 +51,7 @@ class SleepTkApp(): self._is_tracking = False self._earlier = 0 self._page = _START + self._old_notification_level = wasp.system.notify_level try: shell.mkdir("logs/") @@ -129,6 +130,7 @@ class SleepTkApp(): if self._wakeup_smart_enabled: self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 wasp.system.set_alarm(self._WU_a, self._smart_alarm_compute) + wasp.system.notify_level = 1 # silent notifications self._page = _TRACKING elif self._page == _TRACKING or self._page == _TRACKING2: @@ -493,6 +495,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _listen_to_ticks(self): """listen to ticks every second, telling the watch to vibrate""" wasp.gc.collect() + wasp.system.notify_level = self._old_notification_level # restore notification level self._page = _RINGING mute = wasp.watch.display.mute mute(True) From 50805b3d974847a7284cba5b80f0c0eed4f751bd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 28 Feb 2022 22:06:01 +0100 Subject: [PATCH 234/485] docs: mention that notifications are cut at night --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f2cc6df..6d5d561 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) * Status as of end of February 2022: *UI (**done**), regular alarm (**done**), smart alarm (**mostly done but untested**)* * you can download your sleep data file using the file `pull_latest_sleep_data`. A suggested workflow to load it into [pandas](https://pypi.org/project/pandas/) can be found at the bottom of the page. +* the notifications are set to "silent" during the tracking session and are restored to the previously used level when the alarm is ringing # Screenshots: ![start](./screenshots/start_page.png) From f4baf4f34b2af4abe3b0effed41b269fc083f670 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 1 Mar 2022 20:01:29 +0100 Subject: [PATCH 235/485] docs + minor style --- README.md | 4 +++- SleepTk.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6d5d561..b196a41 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,10 @@ ## TODO **misc** -* make sure smart alarm works with the new local maximum method * retake outdated UI screenshot + data sample with the right time +* show settings panel after clicking start, instead of a swipe menu +* recommend best wake up time +* make sure smart alarm works with the new local maximum method * add a power nap mode that wakes you as soon as there has been no movement for 5 minutes * log heart rate data every X minutes? diff --git a/SleepTk.py b/SleepTk.py index cb09704..5e1807f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -67,8 +67,8 @@ class SleepTkApp(): wasp.gc.collect() self._draw() wasp.system.request_event(wasp.EventMask.TOUCH | - wasp.EventMask.SWIPE_UPDOWN | - wasp.EventMask.BUTTON) + wasp.EventMask.SWIPE_UPDOWN | + wasp.EventMask.BUTTON) def background(self): wasp.gc.collect() From 3675e49a315362054283a4e2aa523732273f06d3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 13:30:22 +0100 Subject: [PATCH 236/485] better outlier removal --- SleepTk.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 5e1807f..53c26aa 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -323,10 +323,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """signal processing over the data read from the local file""" # remove outliers: - ma = 0.75*max(data) for x in range(len(data)): - if data[x] > ma: - data[x] = ma + data[x] = min(0.0008, data[x]) del ma, x wasp.gc.collect() wasp.system.keep_awake() From e418f8304df10cd0c79db12e7ef0b42c6daa2369 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 13:31:25 +0100 Subject: [PATCH 237/485] better smoother function --- SleepTk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 53c26aa..c327b87 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -330,15 +330,15 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.keep_awake() # smoothen several times - for j in range(2): - for i in range(1, len(data)-2): - data[i] += data[i-1] + data[i+1] - data[i] /= 3 + for j in range(5): + for i in range(1, len(data)-1): + data[i] += data[i-1] + data[i] /= 2 del i, j wasp.gc.collect() wasp.system.keep_awake() - # center and scale and clip between -1 and 1 + # center and scale mean = sum(data) / len(data) std = ((sum([x**2 for x in data]) / len(data)) - mean**2)**0.5 for i in range(len(data)): From e00551a0542f0a751fe62c1f2da178aab77be34d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 13:32:19 +0100 Subject: [PATCH 238/485] better docs with workflow --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b196a41..b3aafae 100644 --- a/README.md +++ b/README.md @@ -59,14 +59,55 @@ Commands the author uses to take a look a the data using pandas: fname = "./logs/sleep/YOUR_TIME.csv" import pandas as pd -from array import array -from matplotlib import pyplot as plt +import plotly.express as plt -df = pd.read_csv(fname, names=["angl_avg", "time", "x_avg", "y_avg", "z_avg", "battery"]) -offset = int(fname.split("/")[-1].split(".csv")[0]) -df["human_time"] = pd.to_datetime(df["time"]+offset, unit='s') -df["hours"] = df["human_time"].dt.time -df = df.set_index("hours") -data = array("f", df["angl_avg"].values[:-4]) -df["angl_avg"].plot() +df = pd.read_csv(fname, names=["motion", "elapsed", "x_avg", "y_avg", "z_avg", "battery"]) +start_time = int(fname.split("/")[-1].split(".csv")[0]) + +df["time"] = pd.to_datetime(df["elapsed"]+start_time, unit='s') +df["human_time"] = df["time"].dt.time + +month = df.iloc[0]["time"].month_name() +dayname = str(df.iloc[0]["time"].day_name()) +daynumber = str(df.iloc[0]["time"].day) +if daynumber == 1: + daynumber = str(daynumber) + "st" +elif daynumber.endswith("2"): + daynumber = str(daynumber) + "nd" +elif daynumber.endswith("3"): + daynumber = str(daynumber) + "rd" +else: + daynumber = str(daynumber) + "th" +date = f"{month} {daynumber} ({dayname})" + +fig = px.line(df, + x="time", + y="motion", + labels={"motion": "Body motion"}, + title=f"Night starting on {date}") +fig.update_xaxes(type="date", + tickformat="%H:%M" + ) +fig.show() +``` + +Now, to investigate signal processing: +``` + +# reproduce signal processing of the watch here: +import array +data = array.array("f", df["motion"]) + + +### PUT SIGNAL PROCESSING CODE HERE + + +from matplotlib import pyplot as plt +plt.plot(data) +for i in x_maximas: + plt.axvline(x=i, + color="red", + linestyle="--" + ) +plt.show() ``` From 2dc73e609aca80b5f01c833366b956d3927bd8d9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 13:32:28 +0100 Subject: [PATCH 239/485] todo --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3aafae..5a336a2 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,10 @@ ## TODO **misc** -* retake outdated UI screenshot + data sample with the right time +* retake outdated UI screenshot + data sample with the right timestamp +* if error encountered when pulling data, erase file OR check file size * show settings panel after clicking start, instead of a swipe menu -* recommend best wake up time +* recommend best wake up time when setting up alarm * make sure smart alarm works with the new local maximum method * add a power nap mode that wakes you as soon as there has been no movement for 5 minutes From 87beaf3c8c1d0fbdf01d708175f37d0048ad8edf Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 13:34:04 +0100 Subject: [PATCH 240/485] it's bas to clip data --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index c327b87..35d61ea 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -325,7 +325,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # remove outliers: for x in range(len(data)): data[x] = min(0.0008, data[x]) - del ma, x + del x wasp.gc.collect() wasp.system.keep_awake() @@ -342,7 +342,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) mean = sum(data) / len(data) std = ((sum([x**2 for x in data]) / len(data)) - mean**2)**0.5 for i in range(len(data)): - data[i] = min(1, max(-1, (data[i] - mean) / std)) + data[i] = (data[i] - mean) / std del mean, std, i wasp.gc.collect() wasp.system.keep_awake() From b513ef5b86e57ff887f8315c73594d1698139007 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 13:34:18 +0100 Subject: [PATCH 241/485] remove second smoothing as it's useless --- SleepTk.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 35d61ea..118968b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -347,15 +347,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() wasp.system.keep_awake() - # smoothen - for j in range(2): - for i in range(1, len(data)-2): - data[i] += data[i-1] + data[i+1] - data[i] /= 3 - del i, j - wasp.gc.collect() - wasp.system.keep_awake() - # find local maximas x_maximas = array.array("f") y_maximas = array.array("f") From 878de55355ac6641a3c4ba83be68f0a2033fd33c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 14:26:14 +0100 Subject: [PATCH 242/485] center data but DONT scale it --- SleepTk.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 118968b..241aeac 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -338,12 +338,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() wasp.system.keep_awake() - # center and scale + # center data mean = sum(data) / len(data) - std = ((sum([x**2 for x in data]) / len(data)) - mean**2)**0.5 for i in range(len(data)): - data[i] = (data[i] - mean) / std - del mean, std, i + data[i] = data[i] - mean) + del mean, i wasp.gc.collect() wasp.system.keep_awake() From 7f3e6ef2f6474689e71f87f63e13fa244d04ed5e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:02:01 +0100 Subject: [PATCH 243/485] better docs with workflow 2 --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5a336a2..af440f5 100644 --- a/README.md +++ b/README.md @@ -92,15 +92,17 @@ fig.update_xaxes(type="date", fig.show() ``` -Now, to investigate signal processing: +Now, to play around with the signal processing function: ``` - -# reproduce signal processing of the watch here: import array data = array.array("f", df["motion"]) +data = data[:15] # remove the last few data points as the signal +# processor does not yet have access to them when finding best wake up time -### PUT SIGNAL PROCESSING CODE HERE +############################################## +### PUT LATEST SIGNAL PROCESSING CODE HERE ### +############################################## from matplotlib import pyplot as plt From 2ee7baca2fb91081bf3e9e4a88f0fcdc777f4dfd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:02:22 +0100 Subject: [PATCH 244/485] smooth 3 times only --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 241aeac..e538e96 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -330,7 +330,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.keep_awake() # smoothen several times - for j in range(5): + for j in range(3): for i in range(1, len(data)-1): data[i] += data[i-1] data[i] /= 2 From 9b390f963811930afef73bd14d972ec4dfed9118 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:02:32 +0100 Subject: [PATCH 245/485] fix: forgot parenthesis --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index e538e96..40c8eaf 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -341,7 +341,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # center data mean = sum(data) / len(data) for i in range(len(data)): - data[i] = data[i] - mean) + data[i] = data[i] - mean del mean, i wasp.gc.collect() wasp.system.keep_awake() From 2d22548e267f54a6272a6e62bc7151a93b996f71 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:03:17 +0100 Subject: [PATCH 246/485] feat: new way better algo to find local maximas --- SleepTk.py | 69 +++++++++++++++++++++++------------------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 40c8eaf..02a0ac9 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -347,51 +347,40 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.keep_awake() # find local maximas - x_maximas = array.array("f") - y_maximas = array.array("f") - window = int(60*60/_STORE_FREQ) # over 60 minutes - for start_w in range(len(data)) - window: - m = max(data[start_w:start_w+window]) + x_maximas = array.array("H", [0]) + y_maximas = array.array("f", [0]) + window = int(60*60/_STORE_FREQ) + skip = 1800 // _STORE_FREQ # skip first 60 minutes of data + for start_w in range(skip, len(data) - window + 1): + m = max(data[start_w:start_w + window]) for i in range(start_w, start_w + window): - if data[i] == m: - if i+start_w not in x_maximas: - x_maximas.append(i + start_w) - y_maximas.append(m) - del window, start_w, i, m + if data[i] == m and m > 0: + if i not in x_maximas: + if i - x_maximas[-1] <= 2: + # too close to last maximum, keep highest + if y_maximas[-1] < data[i]: + x_maximas[-1] = i + y_maximas[-1] = data[i] + else: + x_maximas.append(i) + y_maximas.append(m) + del window, skip, start_w, i, m, x_maximas[0], y_maximas[0], data wasp.gc.collect() wasp.system.keep_awake() - # remove all peaks found in the first 60 minutes: - for i, x in enumerate(x_maximas): - if x*_STORE_FREQ < 3600: - y_maximas.remove(y_maximas[i]) - x_maximas.remove(x) - del i, x - wasp.gc.collect() - wasp.system.keep_awake() - - # merge the smallest peaks while there are more than N peaks - N = 4 + # merge the closest peaks while there are more than N peaks + N = 3 while len(x_maximas) > N: - y_min = min(y_maximas) # find minimum - for i, y in y_maximas: # find location of minimum - if y == y_min: - x_min_idx = i - if x_min_idx == len(x_maximas): # min is last, merging it with penultimate - closest = x_min_idx-1 - elif x_min_idx == 0: # min is first, merging it with 2nd - closest = x_min_idx+1 - else: # merge with closest - if x_maximas[x_min_idx-1] - x_maximas[x_min_idx] < x_maximas[x_min_idx+1] - x_maximas[x_min_idx]: - closest = x_min_idx-1 - else: - closest = x_min_idx+1 - y_maximas[closest] += y_maximas[x_min_idx] # adding peak values - x_maximas[closest] += x_maximas[x_min_idx] # averaging the x coordinate - x_maximas[closest] /= 2 - y_maximas.remove(y_maximas[x_min_idx]) - x_maximas.remove(x_maximas[x_min_idx]) - del closest, y_min, x_min_idx, i, y + diffs = array.array("f", [x_maximas[int(x)+1] - x_maximas[int(x)] for x in range(len(x_maximas)-1)]) + ex = False + for d_min_idx, d in enumerate(diffs): + if ex: + break + if d == min(diffs): + y_maximas.remove(y_maximas[d_min_idx+1]) + x_maximas.remove(x_maximas[d_min_idx+1]) + ex = True + del diffs, ex, d_min_idx wasp.gc.collect() wasp.system.keep_awake() From d04b3d36f028579a33370787720f5f55275401f9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:03:33 +0100 Subject: [PATCH 247/485] misc --- SleepTk.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 02a0ac9..2f8752e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -384,20 +384,23 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() wasp.system.keep_awake() - # sleep cycle period is the time average distance between those N peaks - period = (x_maximas[-1] - x_maximas[0]) / N + # sleep cycle duration is the average time distance between those N peaks + cycle = sum([x_maximas[i+1] - x_maximas[i] for i in range(len(x_maximas) -1)]) / N * _STORE_FREQ - # if wake up time is in more time than last period but less than what - # SleepTk is allowed to anticipate: add new alarm at best time - last_peak_time = self._offset + x_maximas[-1] * _STORE_FREQ + last_peak = self._offset + x_maximas[-1] * _STORE_FREQ WU_t = self._WU_t - allowed_time = WU_t - _ANTICIPATE_ALLOWED - if last_peak_time + period < WU_t and last_peak_time + period > allowed_time: - earlier = WU_t - (last_peak_time + period) - else: - earlier = 0 # don't anticipate + + # check if too late, already woken up: + if last_peak + cycle > WU_t: + raise Exception("Took too long to compute!") + + # if smart alarm wants to wake you up too early, limit how early + if last_peak + cycle < WU_t - _ANTICIPATE_ALLOWED: + earlier = _ANTICIPATE_ALLOWED + else: # will wake you up at computed time + earlier = last_peak - self._offset + cycle wasp.system.keep_awake() - return (earlier, period) + return (earlier, cycle) def _smart_alarm_compute(self): """computes best wake up time from sleep data""" @@ -444,17 +447,17 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() wasp.system.keep_awake() - earlier, period = self._signal_processing(data) + earlier, cycle = self._signal_processing(data) WU_t = self._WU_t wasp.gc.collect() self._earlier = earlier wasp.system.set_alarm(max(WU_t - earlier, int(wasp.watch.rtc.time()) + 3), # not before right now, to make sure it rings - self._listen_to_ticks) + self._listen_to_ticks) self._page = _TRACKING2 wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Finished smart alarm computation", - "body": "Finished computing best wake up time in {:2f}s. Best sleep cycle duration: {:.2f}h".format(wasp.watch.rtc.time() - start_time, period) + "body": "Finished computing best wake up time in {:2f}s. Sleep cycle: {:.2f}h".format(wasp.watch.rtc.time() - start_time, cycle) }) except Exception as e: wasp.gc.collect() From 254ccc24db009b4b85ecdfaf9d76c6d30b94440f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:07:28 +0100 Subject: [PATCH 248/485] remove useless _TRACKING2 page --- SleepTk.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 2f8752e..d2e3f8a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -24,9 +24,8 @@ import micropython # HARDCODED VARIABLES: _START = micropython.const(0) # page values: _TRACKING = micropython.const(1) -_TRACKING2 = micropython.const(2) -_SETTINGS = micropython.const(3) -_RINGING = micropython.const(4) +_SETTINGS = micropython.const(2) +_RINGING = micropython.const(3) _FONT = fonts.sans18 _TIMESTAMP = micropython.const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = micropython.const(30) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds @@ -133,7 +132,7 @@ class SleepTkApp(): wasp.system.notify_level = 1 # silent notifications self._page = _TRACKING - elif self._page == _TRACKING or self._page == _TRACKING2: + elif self._page == _TRACKING: if self._conf_view is None: if self.btn_off.touch(event): self._conf_view = widgets.ConfirmationView() @@ -273,7 +272,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) draw.string(msg, 0, 70) self.btn_al = widgets.Button(x=0, y=70, w=240, h=140, label="WAKE UP") self.btn_al.draw() - elif self._page == _TRACKING or self._page == _TRACKING2: + elif self._page == _TRACKING: ti = wasp.watch.time.localtime(self._offset) draw.string('Started at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 70) draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) @@ -454,7 +453,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._earlier = earlier wasp.system.set_alarm(max(WU_t - earlier, int(wasp.watch.rtc.time()) + 3), # not before right now, to make sure it rings self._listen_to_ticks) - self._page = _TRACKING2 + self._page = _TRACKING wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Finished smart alarm computation", "body": "Finished computing best wake up time in {:2f}s. Sleep cycle: {:.2f}h".format(wasp.watch.rtc.time() - start_time, cycle) From fe1b7bb55d2a12f62ecc6f65171056ec7d744d6f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:09:13 +0100 Subject: [PATCH 249/485] fix: calling wasp instead of watch.wasp --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index d2e3f8a..082f2dd 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -221,7 +221,7 @@ class SleepTkApp(): # strop tracking if battery low self._disable_tracking(keep_main_alarm=True) self._wakeup_smart_enabled = 0 - h, m = watch.time.localtime(wasp.watch.rtc.time())[3:5] + h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Bat <20%", "body": "Stopped \ From 89b34397e8157a9a715eed205247b04b1c166f86 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:09:28 +0100 Subject: [PATCH 250/485] testing, don't stop tracking when computing best wake up time --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 082f2dd..1119115 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -418,7 +418,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) try: start_time = wasp.watch.rtc.time() # stop tracking to save memory, keep the alarm just in case - self._disable_tracking(keep_main_alarm=True) + #self._disable_tracking(keep_main_alarm=True) # read file one character at a time, to get only the 1st # value of each row, which is the arm angle From 8516651687f16cc05a4524c078cb1c3897641555 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:12:05 +0100 Subject: [PATCH 251/485] update screenshot --- README.md | 2 +- screenshots/example_night2.png | Bin 0 -> 52254 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 screenshots/example_night2.png diff --git a/README.md b/README.md index af440f5..53299c8 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ![start](./screenshots/start_page.png) ![settings](./screenshots/settings_page.png) ![tracking](./screenshots/tracking_page.png) -![night example](./screenshots/example_night.png) +![night example](./screenshots/example_night2.png) ## TODO **misc** diff --git a/screenshots/example_night2.png b/screenshots/example_night2.png new file mode 100644 index 0000000000000000000000000000000000000000..f9353c34cee4401250a9d94397be28b06148914b GIT binary patch literal 52254 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 U+nZ-H Date: Wed, 2 Mar 2022 15:19:13 +0100 Subject: [PATCH 252/485] update example night screenshot --- screenshots/example_night.png | Bin 30019 -> 66621 bytes screenshots/example_night2.png | Bin 52254 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 screenshots/example_night2.png diff --git a/screenshots/example_night.png b/screenshots/example_night.png index 104ae354e1e848fc38eb6fa3107a0f8013298ac9..350bdb17178ef56d8e8ae5c31a4dc1a63601f913 100644 GIT binary patch literal 66621 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%0DqvZOy+7zD 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

dC~{2| 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 literal 30019 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 z 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 diff --git a/screenshots/example_night2.png b/screenshots/example_night2.png deleted file mode 100644 index f9353c34cee4401250a9d94397be28b06148914b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52254 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 U+nZ-H Date: Wed, 2 Mar 2022 15:19:21 +0100 Subject: [PATCH 253/485] style --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 53299c8..d12669e 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ date = f"{month} {daynumber} ({dayname})" fig = px.line(df, x="time", y="motion", - labels={"motion": "Body motion"}, + labels={"motion": "Body motion", "time":"Time"}, title=f"Night starting on {date}") fig.update_xaxes(type="date", tickformat="%H:%M" From 52d903d5f452cc3e025c565c9a4f6d164d5ee151 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:20:07 +0100 Subject: [PATCH 254/485] fix wrong link to screenshot --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d12669e..f3785cf 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ![start](./screenshots/start_page.png) ![settings](./screenshots/settings_page.png) ![tracking](./screenshots/tracking_page.png) -![night example](./screenshots/example_night2.png) +![night example](./screenshots/example_night.png) ## TODO **misc** From 9e8b7a23b83894b484e778032dea25c1fb46b22c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:20:58 +0100 Subject: [PATCH 255/485] todo --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f3785cf..13e6f26 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ ## TODO **misc** -* retake outdated UI screenshot + data sample with the right timestamp * if error encountered when pulling data, erase file OR check file size * show settings panel after clicking start, instead of a swipe menu * recommend best wake up time when setting up alarm From bfe38680d87a075027fbe64956fdc8e0e0320676 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:45:32 +0100 Subject: [PATCH 256/485] todo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 13e6f26..059743b 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ **misc** * if error encountered when pulling data, erase file OR check file size * show settings panel after clicking start, instead of a swipe menu -* recommend best wake up time when setting up alarm -* make sure smart alarm works with the new local maximum method + * add a Button instead of checkbox to manage alarm / gentle alarm / smart alarm / both + * recommend best wake up time when setting up alarm * add a power nap mode that wakes you as soon as there has been no movement for 5 minutes * log heart rate data every X minutes? From 53dcb60c684cc0c5722c59c31fa589286cd9e09f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:46:30 +0100 Subject: [PATCH 257/485] docs: mention gentle alarm clock --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 059743b..eabaae2 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ * **sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file * **Flexible**: does not make too many assumption regarding time to fall asleep, sleep cycle duration etc. SleepTk tries various data to see what fits best for your profile. If you still want to customize things, all the hardcoded and commented settings are easily accessible at the top of the file. * alarm clock: wakes you up at a specific time +* **gentle alarm clock**: vibrates the watch a tiny bit regularly before wake up time to lift you gently back to consciousness * **smart alarm clock**: can wake you up to 40 minutes before the set time to make sure you wake up feeling refreshed. * **privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download if if needed) * open source From 81352baf20e307fe6c2a4fa8ffbcceffe44c7e2c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 15:47:11 +0100 Subject: [PATCH 258/485] remove gentle vibration a T-20 minutes --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 1119115..5e816cd 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -34,7 +34,7 @@ _BATTERY_THRESHOLD = micropython.const(20) # under X% of battery, stop tracking # user might want to edit this: _ANTICIPATE_ALLOWED = micropython.const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set -_GRADUAL_WAKE = array.array("H", [1, 2, 3, 4, 5, 8, 13, 20]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up +_GRADUAL_WAKE = array.array("H", [1, 2, 3, 4, 5, 8, 13]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up class SleepTkApp(): From 66d91a9f5520c857626bec32b444299d2d4b5de2 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 2 Mar 2022 21:28:34 +0100 Subject: [PATCH 259/485] new: when using smart alarm, remove previous gentle alarm and add new one --- SleepTk.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 5e816cd..b190e83 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -450,9 +450,17 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) WU_t = self._WU_t wasp.gc.collect() - self._earlier = earlier + # add new alarm wasp.system.set_alarm(max(WU_t - earlier, int(wasp.watch.rtc.time()) + 3), # not before right now, to make sure it rings self._listen_to_ticks) + + # replace old gentle alarm by another one + for t in _GRADUAL_WAKE: + wasp.system.cancel_alarm(WU_t - t*60, self._tiny_vibration) + if earlier + t*60 < _ANTICIPATE_ALLOWED: + wasp.system.set_alarm(WU_t - earlier - t*60, self._tiny_vibration) + + self._earlier = earlier self._page = _TRACKING wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Finished smart alarm computation", From 668c94856fc1ebabb231e6b7804f41f81f2bddf6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 09:45:57 +0100 Subject: [PATCH 260/485] new: smoothen the data only once --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index b190e83..6c065ef 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -329,7 +329,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.keep_awake() # smoothen several times - for j in range(3): + for j in range(1): for i in range(1, len(data)-1): data[i] += data[i-1] data[i] /= 2 From a79033e6d07d5c8525117f13df0338f025139cd6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 09:46:24 +0100 Subject: [PATCH 261/485] feat: remove partially downloaded sleep data if exception is caught --- pull_latest_sleep_data.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pull_latest_sleep_data.py b/pull_latest_sleep_data.py index deff0e8..1f48cd5 100644 --- a/pull_latest_sleep_data.py +++ b/pull_latest_sleep_data.py @@ -34,6 +34,10 @@ for fi in to_dl: else: print(f"Downloading file '{fi}'") pull_cmd = f'./tools/wasptool --verbose --pull logs/sleep/{fi}' - os.system(pull_cmd) - print(f"Succesfully downloaded to './logs/sleep/{fi}'") + try: + out = subprocess.check_output(shlex.split(pull_cmd)) + print(f"Succesfully downloaded to './logs/sleep/{fi}'") + except Exception as e: + print(f"Error happenned when donloading {fi}, deleting file") + os.system(f"rm ./logs/sleep/{fi}") print("\n\n") From c1ff8bc0c3fb72b4ebcce52ad2f45b29651c3980 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 12:04:11 +0100 Subject: [PATCH 262/485] new: don't delete buff array but just empty it --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 6c065ef..1c21fb0 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -51,6 +51,7 @@ class SleepTkApp(): self._earlier = 0 self._page = _START self._old_notification_level = wasp.system.notify_level + self._buff = array.array("f") try: shell.mkdir("logs/") @@ -98,7 +99,6 @@ class SleepTkApp(): if self.btn_on.touch(event): self._is_tracking = True # accel data not yet written to disk: - self._buff = array.array("f") self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file self._offset = int(wasp.watch.rtc.time()) # makes output more compact @@ -257,7 +257,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f.close() self._last_checkpoint = self._data_point_nb - self._buff = array.array("f") + del self._buff[:] def _draw(self): """GUI""" From 24d4a04cfa832edee5370956f15a888dba8057e6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 12:04:23 +0100 Subject: [PATCH 263/485] style: changed default values order --- SleepTk.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 1c21fb0..36adb02 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -42,14 +42,15 @@ class SleepTkApp(): def __init__(self): wasp.gc.collect() + # default values: self._wakeup_enabled = 1 self._wakeup_smart_enabled = 0 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._spinval_H = 7 # default wake up time self._spinval_M = 30 - self._conf_view = None - self._is_tracking = False - self._earlier = 0 self._page = _START + self._is_tracking = False + self._conf_view = None # confirmation view + self._earlier = 0 # number of seconds between the alarm you set manually and the smart alarm time self._old_notification_level = wasp.system.notify_level self._buff = array.array("f") From c5edca99011f7d12b0a0d50096a773edf42a606c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 12:09:52 +0100 Subject: [PATCH 264/485] todo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eabaae2..6a22ea3 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ ## TODO **misc** -* if error encountered when pulling data, erase file OR check file size * show settings panel after clicking start, instead of a swipe menu * add a Button instead of checkbox to manage alarm / gentle alarm / smart alarm / both * recommend best wake up time when setting up alarm +* add a "nap tracking" mode that records sleep tracking with more precision * add a power nap mode that wakes you as soon as there has been no movement for 5 minutes * log heart rate data every X minutes? From ab478441d9148d139a64fa022fa96f04386a433d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 12:10:00 +0100 Subject: [PATCH 265/485] increase data logging frequency --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 36adb02..d7c78ea 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -28,8 +28,8 @@ _SETTINGS = micropython.const(2) _RINGING = micropython.const(3) _FONT = fonts.sans18 _TIMESTAMP = micropython.const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = micropython.const(30) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_STORE_FREQ = micropython.const(300) # process data and store to file every X seconds +_FREQ = micropython.const(10) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_STORE_FREQ = micropython.const(60) # process data and store to file every X seconds _BATTERY_THRESHOLD = micropython.const(20) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: From 0ece1faa174a2accbc09aa689774ba78722dc0fa Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 12:28:36 +0100 Subject: [PATCH 266/485] new: stop storing battery data --- SleepTk.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index d7c78ea..76b914c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -232,7 +232,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() def _periodicSave(self): - """save data after averageing over a window to file""" + """save data after averaging over a window to file + row order in the csv: + 1. arm angle + 2. elapsed times + 3/4/5. x/y/z average value over _STORE_FREQ seconds""" buff = self._buff if self._data_point_nb - self._last_checkpoint >= _STORE_FREQ / _FREQ: x_avg = sum([buff[i] for i in range(0, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) @@ -247,7 +251,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff.append(y_avg) buff.append(z_avg) del x_avg, y_avg, z_avg - buff.append(int(wasp.watch.battery.voltage_mv())) # currently more accurate than percent f = open(self.filep, "ab") for x in buff[:-1]: From 77b9a6006e83656491174905558b5f665121517f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 12:59:25 +0100 Subject: [PATCH 267/485] new: stop storing absolute value of arm angle + way better way to store data --- SleepTk.py | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 76b914c..a6c5977 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -52,7 +52,7 @@ class SleepTkApp(): self._conf_view = None # confirmation view self._earlier = 0 # number of seconds between the alarm you set manually and the smart alarm time self._old_notification_level = wasp.system.notify_level - self._buff = array.array("f") + self._buff = array.array("f", [0, 0, 0]) try: shell.mkdir("logs/") @@ -214,7 +214,11 @@ class SleepTkApp(): """get one data point of accelerometer every _FREQ seconds and they are then averaged and stored every _STORE_FREQ seconds""" if self._is_tracking: - [self._buff.append(x) for x in wasp.watch.accel.read_xyz()] + buff = self._buff + xyz = wasp.watch.accel.read_xyz() + buff[0] += xyz[0] + buff[1] += xyz[1] + buff[2] += xyz[2] self._data_point_nb += 1 self._add_accel_alar() self._periodicSave() @@ -236,32 +240,31 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) row order in the csv: 1. arm angle 2. elapsed times - 3/4/5. x/y/z average value over _STORE_FREQ seconds""" + 3/4/5. x/y/z average value over _STORE_FREQ seconds + arm angle formula from https://www.nature.com/articles/s41598-018-31266-z + note: math.atan() is faster than using a taylor serie + """ buff = self._buff - if self._data_point_nb - self._last_checkpoint >= _STORE_FREQ / _FREQ: - x_avg = sum([buff[i] for i in range(0, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) - y_avg = sum([buff[i] for i in range(1, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) - z_avg = sum([buff[i] for i in range(2, len(buff), 3)]) / (self._data_point_nb - self._last_checkpoint) - buff = array.array("f") # reseting array - buff.append(abs(math.atan(z_avg / (x_avg**2 + y_avg**2)))) - # formula from https://www.nature.com/articles/s41598-018-31266-z - # note: math.atan() is faster than using a taylor serie - buff.append(int(wasp.watch.rtc.time() - self._offset)) - buff.append(x_avg) - buff.append(y_avg) - buff.append(z_avg) - del x_avg, y_avg, z_avg + n = self._data_point_nb - self._last_checkpoint + if n >= _STORE_FREQ / _FREQ: + buff[0] /= n # averages x, y, z + buff[1] /= n + buff[2] /= n f = open(self.filep, "ab") - for x in buff[:-1]: - f.write("{:7f}".format(x).encode()) - f.write(b",") - f.write("{:7f}".format(buff[-1]).encode()) - f.write(b"\n") + f.write("{:7f},{},{:7f},{:7f},{:7f}\n".format( + math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # average arm angle + int(wasp.watch.rtc.time() - self._offset), + buff[0], buff[1], buff[2] + ).encode()) f.close() - + del f + buff[0] = 0 # resets x/y/z to 0 + buff[1] = 0 + buff[2] = 0 self._last_checkpoint = self._data_point_nb - del self._buff[:] + wasp.gc.collect() + def _draw(self): """GUI""" From 50751bf53c451e2d171be2f8075b9021cc299707 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 12:59:41 +0100 Subject: [PATCH 268/485] fix: was not reading last value from file when computing smart alarm --- SleepTk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index a6c5977..d8dc239 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -444,8 +444,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff = b"" continue if char == b"": # end of file + data.append(float(buff)) break - elif not skip: # digit of arm angle value + if not skip: # digit of arm angle value buff += char f.close() From 85cec0e6966426943f19102b1d09b0bd0d4b987d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 4 Mar 2022 13:01:34 +0100 Subject: [PATCH 269/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6a22ea3..00949ca 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ * show settings panel after clicking start, instead of a swipe menu * add a Button instead of checkbox to manage alarm / gentle alarm / smart alarm / both * recommend best wake up time when setting up alarm +* investigate if downsampling is necessary * add a "nap tracking" mode that records sleep tracking with more precision * add a power nap mode that wakes you as soon as there has been no movement for 5 minutes From 95eae6d6abcc328c2d30542cc5fd26ee353eeba3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 7 Mar 2022 10:01:10 +0100 Subject: [PATCH 270/485] todo --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 00949ca..cb8fc96 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,13 @@ ## TODO **misc** +* ask to re download file if size differ +* use the absolute rate of change to compute best wake up time * show settings panel after clicking start, instead of a swipe menu * add a Button instead of checkbox to manage alarm / gentle alarm / smart alarm / both * recommend best wake up time when setting up alarm * investigate if downsampling is necessary +* if self.foreground is called, record the time. Use it to cancel smart alarm if you woke up too many times (more than 2 times in more than 20 minutes apart). * add a "nap tracking" mode that records sleep tracking with more precision * add a power nap mode that wakes you as soon as there has been no movement for 5 minutes From 2048d848496496b4c2a03bd5c6e96f4ef5941d65 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 7 Mar 2022 11:01:24 +0100 Subject: [PATCH 271/485] header warns that product is not finished --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index d8dc239..4702432 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -6,9 +6,9 @@ # https://github.com/thiswillbeyourgithub/sleep_tracker_pinetime_wasp-os -SleepTk is designed to track accelerometer data throughout the night. It can -also compute the best time to wake you up, up to 40 minutes before the -alarm you set up manually. +SleepTk is designed to track accelerometer data throughout the night. When +finished, it will also compute the best time to wake you up, up to 40 minutes +before the alarm you set up manually. """ From b036712818451f96504cff3dd969fb3713887ff1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 7 Mar 2022 11:01:59 +0100 Subject: [PATCH 272/485] set 0 and 1 to _OFF and _ON + import only const from micropython --- SleepTk.py | 56 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 4702432..d38d900 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -19,21 +19,23 @@ import shell import fonts import math import array -import micropython +from micropython import const # HARDCODED VARIABLES: -_START = micropython.const(0) # page values: -_TRACKING = micropython.const(1) -_SETTINGS = micropython.const(2) -_RINGING = micropython.const(3) +_ON = const(1) +_OFF = const(0) +_START = const(0) # page values: +_TRACKING = const(1) +_SETTINGS = const(2) +_RINGING = const(3) _FONT = fonts.sans18 -_TIMESTAMP = micropython.const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = micropython.const(10) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_STORE_FREQ = micropython.const(60) # process data and store to file every X seconds -_BATTERY_THRESHOLD = micropython.const(20) # under X% of battery, stop tracking and only keep the alarm +_TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date +_FREQ = const(10) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_STORE_FREQ = const(60) # process data and store to file every X seconds +_BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: -_ANTICIPATE_ALLOWED = micropython.const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set +_ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set _GRADUAL_WAKE = array.array("H", [1, 2, 3, 4, 5, 8, 13]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up @@ -43,16 +45,16 @@ class SleepTkApp(): def __init__(self): wasp.gc.collect() # default values: - self._wakeup_enabled = 1 - self._wakeup_smart_enabled = 0 # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART + self._wakeup_enabled = _ON + self._wakeup_smart_enabled = _OFF # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._page = _START - self._is_tracking = False - self._conf_view = None # confirmation view + self._is_tracking = _OFF + self._conf_view = _OFF # confirmation view self._earlier = 0 # number of seconds between the alarm you set manually and the smart alarm time self._old_notification_level = wasp.system.notify_level - self._buff = array.array("f", [0, 0, 0]) + self._buff = array.array("f", [_OFF, _OFF, _OFF]) try: shell.mkdir("logs/") @@ -64,7 +66,7 @@ class SleepTkApp(): pass def foreground(self): - self._conf_view = None + self._conf_view = _OFF wasp.gc.collect() self._draw() wasp.system.request_event(wasp.EventMask.TOUCH | @@ -134,7 +136,7 @@ class SleepTkApp(): self._page = _TRACKING elif self._page == _TRACKING: - if self._conf_view is None: + if self._conf_view is _OFF: if self.btn_off.touch(event): self._conf_view = widgets.ConfirmationView() self._conf_view.draw("Stop tracking?") @@ -144,7 +146,7 @@ class SleepTkApp(): if self._conf_view.value: self._disable_tracking() self._page = _START - self._conf_view = None + self._conf_view = _OFF elif self._page == _RINGING: if self.btn_al.touch(event): @@ -155,29 +157,29 @@ class SleepTkApp(): no_full_draw = True disable_all = False if self.check_al.touch(event): - if self._wakeup_enabled == 1: - self._wakeup_enabled = 0 + if self._wakeup_enabled == _ON: + self._wakeup_enabled = _OFF disable_all = True else: - self._wakeup_enabled = 1 + self._wakeup_enabled = _ON no_full_draw = False self.check_al.state = self._wakeup_enabled self.check_al.update() if disable_all: - self._wakeup_smart_enabled = 0 + self._wakeup_smart_enabled = _OFF self.check_smart.state = self._wakeup_smart_enabled self._check_smart = None self._draw() if self.check_al.state: if self.check_smart.touch(event): - if self._wakeup_smart_enabled == 1: - self._wakeup_smart_enabled = 0 + if self._wakeup_smart_enabled == _ON: + self._wakeup_smart_enabled = _OFF self.check_smart.state = self._wakeup_smart_enabled self._check_smart = None - elif self._wakeup_enabled == 1: - self._wakeup_smart_enabled = 1 + elif self._wakeup_enabled == _ON: + self._wakeup_smart_enabled = _ON self.check_smart.state = self._wakeup_smart_enabled self.check_smart.update() self.check_smart.draw() @@ -225,7 +227,7 @@ class SleepTkApp(): if wasp.watch.battery.level() <= _BATTERY_THRESHOLD: # strop tracking if battery low self._disable_tracking(keep_main_alarm=True) - self._wakeup_smart_enabled = 0 + self._wakeup_smart_enabled = _OFF h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Bat <20%", From 39033fffe05e499c528520fea32335bf6c334567 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 7 Mar 2022 11:02:36 +0100 Subject: [PATCH 273/485] only import array from array --- SleepTk.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index d38d900..61a4a99 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -18,7 +18,7 @@ import widgets import shell import fonts import math -import array +from array import array from micropython import const # HARDCODED VARIABLES: @@ -36,7 +36,7 @@ _BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only ke # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set -_GRADUAL_WAKE = array.array("H", [1, 2, 3, 4, 5, 8, 13]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up +_GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 8, 13]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up class SleepTkApp(): @@ -54,7 +54,7 @@ class SleepTkApp(): self._conf_view = _OFF # confirmation view self._earlier = 0 # number of seconds between the alarm you set manually and the smart alarm time self._old_notification_level = wasp.system.notify_level - self._buff = array.array("f", [_OFF, _OFF, _OFF]) + self._buff = array("f", [_OFF, _OFF, _OFF]) try: shell.mkdir("logs/") @@ -355,8 +355,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.keep_awake() # find local maximas - x_maximas = array.array("H", [0]) - y_maximas = array.array("f", [0]) + x_maximas = array("H", [0]) + y_maximas = array("f", [0]) window = int(60*60/_STORE_FREQ) skip = 1800 // _STORE_FREQ # skip first 60 minutes of data for start_w in range(skip, len(data) - window + 1): @@ -379,7 +379,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # merge the closest peaks while there are more than N peaks N = 3 while len(x_maximas) > N: - diffs = array.array("f", [x_maximas[int(x)+1] - x_maximas[int(x)] for x in range(len(x_maximas)-1)]) + diffs = array("f", [x_maximas[int(x)+1] - x_maximas[int(x)] for x in range(len(x_maximas)-1)]) ex = False for d_min_idx, d in enumerate(diffs): if ex: @@ -431,7 +431,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # read file one character at a time, to get only the 1st # value of each row, which is the arm angle - data = array.array("f") + data = array("f") buff = b"" f = open(self.filep, "rb") skip = False From 0d7fd8994bdb8b56d7a8ecc5223f794e006e11a5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 7 Mar 2022 11:06:47 +0100 Subject: [PATCH 274/485] no need to import time --- SleepTk.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 61a4a99..f58c762 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -12,7 +12,6 @@ before the alarm you set up manually. """ -import time import wasp import widgets import shell @@ -120,7 +119,7 @@ class SleepTkApp(): MM = self._spinval_M if HH < now[3] or (HH == now[3] and MM <= now[4]): dd += 1 - self._WU_t = time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) + self._WU_t = wasp.watch.time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) wasp.system.set_alarm(self._WU_t, self._listen_to_ticks) # also set alarm to vibrate a tiny bit before wake up time From db4a723d8f53ed1152e6ff44cca96e19a6bad057 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 8 Mar 2022 12:56:10 +0100 Subject: [PATCH 275/485] mention other related project for the banglejs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cb8fc96..d59f7ac 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ * maybe coding a 1D convolution is a good way to extract peaks * list of ways to find local maxima in python : https://blog.finxter.com/how-to-find-local-minima-in-1d-and-2d-numpy-arrays/ + https://pythonawesome.com/overview-of-the-peaks-dectection-algorithms-available-in-python/ +### Related project: +* another hackable smartwatch has a similar software: [sleepphasealarm](https://banglejs.com/apps/#sleepphasealarm) and [steelball](https://github.com/jabituyaben/SteelBall) for the [Banglejs](https://banglejs.com/) From 66d662e6026110539f1a1e9a47a0fcdbcc589a38 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 8 Mar 2022 13:19:08 +0100 Subject: [PATCH 276/485] new: dont average x/y/z, instead keep only the max value --- SleepTk.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index f58c762..5787b3b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -212,14 +212,15 @@ class SleepTkApp(): wasp.system.set_alarm(self.next_al, self._trackOnce) def _trackOnce(self): - """get one data point of accelerometer every _FREQ seconds and - they are then averaged and stored every _STORE_FREQ seconds""" + """get one data point of accelerometer every _FREQ seconds, keep + the maximum over each axis then store in a file every + _STORE_FREQ seconds""" if self._is_tracking: buff = self._buff xyz = wasp.watch.accel.read_xyz() - buff[0] += xyz[0] - buff[1] += xyz[1] - buff[2] += xyz[2] + buff[0] = max(buff[0], xyz[0]) + buff[1] = max(buff[1], xyz[1]) + buff[2] = max(buff[2], xyz[2]) self._data_point_nb += 1 self._add_accel_alar() self._periodicSave() @@ -237,24 +238,20 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() def _periodicSave(self): - """save data after averaging over a window to file + """save data after maxpooling over a window to file row order in the csv: 1. arm angle 2. elapsed times - 3/4/5. x/y/z average value over _STORE_FREQ seconds + 3/4/5. x/y/z max value over _STORE_FREQ seconds arm angle formula from https://www.nature.com/articles/s41598-018-31266-z note: math.atan() is faster than using a taylor serie """ buff = self._buff n = self._data_point_nb - self._last_checkpoint if n >= _STORE_FREQ / _FREQ: - buff[0] /= n # averages x, y, z - buff[1] /= n - buff[2] /= n - f = open(self.filep, "ab") f.write("{:7f},{},{:7f},{:7f},{:7f}\n".format( - math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # average arm angle + math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # estimated arm angle int(wasp.watch.rtc.time() - self._offset), buff[0], buff[1], buff[2] ).encode()) From 541af818491d68c9876bddf1abdc6de52da7f25e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 8 Mar 2022 13:21:10 +0100 Subject: [PATCH 277/485] take more measurements --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 5787b3b..39f1f41 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -29,7 +29,7 @@ _SETTINGS = const(2) _RINGING = const(3) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = const(10) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_FREQ = const(2) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(60) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only keep the alarm From 325d1d08a6f74e4a4898ad2640aad22c6d75506d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 8 Mar 2022 13:26:12 +0100 Subject: [PATCH 278/485] link to another study --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d59f7ac..4a76537 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ * very interesting research paper on the topic : https://academic.oup.com/sleep/article/42/12/zsz180/5549536 * maybe coding a 1D convolution is a good way to extract peaks * list of ways to find local maxima in python : https://blog.finxter.com/how-to-find-local-minima-in-1d-and-2d-numpy-arrays/ + https://pythonawesome.com/overview-of-the-peaks-dectection-algorithms-available-in-python/ +* interesting study : https://ieeexplore.ieee.org/document/7052479 ### Related project: * another hackable smartwatch has a similar software: [sleepphasealarm](https://banglejs.com/apps/#sleepphasealarm) and [steelball](https://github.com/jabituyaben/SteelBall) for the [Banglejs](https://banglejs.com/) From d79640707eb1bd87910e578ccf6eca995ba0c352 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 8 Mar 2022 13:26:19 +0100 Subject: [PATCH 279/485] todo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a76537..3b70743 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ * investigate if downsampling is necessary * if self.foreground is called, record the time. Use it to cancel smart alarm if you woke up too many times (more than 2 times in more than 20 minutes apart). * add a "nap tracking" mode that records sleep tracking with more precision -* add a power nap mode that wakes you as soon as there has been no movement for 5 minutes +* add a power nap mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops * log heart rate data every X minutes? * log smart alarm data to file? log user rating of how well he/she felt fresh at wake? From 3581a3fee19791f706ff0e7667efa550a2df68e7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 8 Mar 2022 13:27:16 +0100 Subject: [PATCH 280/485] fix: catch error when pulling files --- pull_latest_sleep_data.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pull_latest_sleep_data.py b/pull_latest_sleep_data.py index 1f48cd5..f115277 100644 --- a/pull_latest_sleep_data.py +++ b/pull_latest_sleep_data.py @@ -10,12 +10,11 @@ mode = "all" # download "all" files or only "latest" print("\n\nRunning gc.collect()...") mem_cmd = './tools/wasptool --verbose --eval \'wasp.gc.collect()\'' -os.system(mem_cmd) +subprocess.check_output(shlex.split(mem_cmd)) print("\n\nListing remote files...") ls_cmd = './tools/wasptool --verbose --eval \'from shell import ls ; ls(\"/flash/logs/sleep/\")\'' -ls_cmd = shlex.split(ls_cmd) # properly split args -out = subprocess.check_output(ls_cmd).decode() +out = subprocess.check_output(shlex.split(ls_cmd)).decode() files = re.findall(r"\d*\.csv", out) print(f"Found files {', '.join(files)}") @@ -35,7 +34,7 @@ for fi in to_dl: print(f"Downloading file '{fi}'") pull_cmd = f'./tools/wasptool --verbose --pull logs/sleep/{fi}' try: - out = subprocess.check_output(shlex.split(pull_cmd)) + subprocess.check_output(shlex.split(pull_cmd)) print(f"Succesfully downloaded to './logs/sleep/{fi}'") except Exception as e: print(f"Error happenned when donloading {fi}, deleting file") From 92668fca48583b31da469ad67106bcbe40e27773 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 8 Mar 2022 15:14:05 +0100 Subject: [PATCH 281/485] take derivative of value + less strong outlier limitation + more smoothing --- SleepTk.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 39f1f41..9711404 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -326,15 +326,21 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _signal_processing(self, data): """signal processing over the data read from the local file""" + # take absolute rate of change of data + for i in range(len(data)-1): + mem = data[i+1] + data[i] = abs(mem-data[i]) + del i + # remove outliers: for x in range(len(data)): - data[x] = min(0.0008, data[x]) + data[x] = min(0.005, data[x]) del x wasp.gc.collect() wasp.system.keep_awake() # smoothen several times - for j in range(1): + for j in range(5): for i in range(1, len(data)-1): data[i] += data[i-1] data[i] /= 2 From 314b2bef60fd790d093343bf15afbf38767a6cce Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 9 Mar 2022 19:20:50 +0100 Subject: [PATCH 282/485] todo --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b70743..7e5109b 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,7 @@ ## TODO **misc** -* ask to re download file if size differ -* use the absolute rate of change to compute best wake up time +* log heart rate data every X minutes * show settings panel after clicking start, instead of a swipe menu * add a Button instead of checkbox to manage alarm / gentle alarm / smart alarm / both * recommend best wake up time when setting up alarm @@ -44,7 +43,6 @@ * add a "nap tracking" mode that records sleep tracking with more precision * add a power nap mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops -* log heart rate data every X minutes? * log smart alarm data to file? log user rating of how well he/she felt fresh at wake? * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation? From 253b33d4704fc1662e330c4a840caf00c772b9b0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 9 Mar 2022 19:21:21 +0100 Subject: [PATCH 283/485] replace function _add_accel_alar by two direct calls --- SleepTk.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 9711404..4b9928b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -107,7 +107,10 @@ class SleepTkApp(): # create one file per recording session: self.filep = "logs/sleep/{}.csv".format(str(self._offset + _TIMESTAMP)) - self._add_accel_alar() + + # add alarm to log accel data in _FREQ seconds + self.next_al = wasp.watch.rtc.time() + _FREQ + wasp.system.set_alarm(self.next_al, self._trackOnce) # setting up alarm if self._wakeup_enabled: @@ -205,12 +208,6 @@ class SleepTkApp(): self._periodicSave() wasp.gc.collect() - def _add_accel_alar(self): - """set an alarm, due in _FREQ minutes, to log the accelerometer data - once""" - self.next_al = wasp.watch.rtc.time() + _FREQ - wasp.system.set_alarm(self.next_al, self._trackOnce) - def _trackOnce(self): """get one data point of accelerometer every _FREQ seconds, keep the maximum over each axis then store in a file every @@ -222,7 +219,11 @@ class SleepTkApp(): buff[1] = max(buff[1], xyz[1]) buff[2] = max(buff[2], xyz[2]) self._data_point_nb += 1 - self._add_accel_alar() + + # add alarm to log accel data in _FREQ seconds + self.next_al = wasp.watch.rtc.time() + _FREQ + wasp.system.set_alarm(self.next_al, self._trackOnce) + self._periodicSave() if wasp.watch.battery.level() <= _BATTERY_THRESHOLD: # strop tracking if battery low From 212b5bd2b8b08ec74c33bcbfd02bd6bb824d6e7e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 9 Mar 2022 19:21:51 +0100 Subject: [PATCH 284/485] reduce _FREQ to 5 seconds --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4b9928b..833ea4d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -29,7 +29,7 @@ _SETTINGS = const(2) _RINGING = const(3) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = const(2) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(60) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only keep the alarm From e0c024a229939658efe2c78f09989d6dd9cc84ce Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 14 Mar 2022 17:52:07 +0100 Subject: [PATCH 285/485] auto init file by writing nothing to it --- SleepTk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 833ea4d..45f8cae 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -107,6 +107,9 @@ class SleepTkApp(): # create one file per recording session: self.filep = "logs/sleep/{}.csv".format(str(self._offset + _TIMESTAMP)) + f = open(self.filep, "wb") + f.write(b"") + f.close() # add alarm to log accel data in _FREQ seconds self.next_al = wasp.watch.rtc.time() + _FREQ From ce635863c6eba5352ebaf572b3523f12eb30e5fa Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 14 Mar 2022 20:17:08 +0100 Subject: [PATCH 286/485] set battery threshold to 10% --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 45f8cae..c0e2d9b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -31,7 +31,7 @@ _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(60) # process data and store to file every X seconds -_BATTERY_THRESHOLD = const(20) # under X% of battery, stop tracking and only keep the alarm +_BATTERY_THRESHOLD = const(10) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set From 0b16a34a9027b0c330d806181881aa5de3b5bbc4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 14 Mar 2022 20:19:45 +0100 Subject: [PATCH 287/485] better code to turn screen off when waking up --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index c0e2d9b..192308b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -520,6 +520,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) mute = wasp.watch.display.mute mute(True) wasp.system.wake() - wasp.system.keep_awake() + mute(True) wasp.system.switch(self) wasp.watch.vibrator.pulse(duty=60, ms=100) From be7f4d72bebc6dbc8bb1ae82a0ee34867a867ffe Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 14 Mar 2022 20:20:01 +0100 Subject: [PATCH 288/485] gradual wake starts 15 minutes before alarm --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 192308b..076b5d1 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -35,7 +35,7 @@ _BATTERY_THRESHOLD = const(10) # under X% of battery, stop tracking and only ke # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set -_GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 8, 13]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up +_GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 8, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up class SleepTkApp(): From 55574efd6500655126a0ba0c043752adba442650 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 10:02:22 +0100 Subject: [PATCH 289/485] fix: buff has to be initialized at -16000 instead of 0 because you only keep the max --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 076b5d1..054b89e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -53,7 +53,7 @@ class SleepTkApp(): self._conf_view = _OFF # confirmation view self._earlier = 0 # number of seconds between the alarm you set manually and the smart alarm time self._old_notification_level = wasp.system.notify_level - self._buff = array("f", [_OFF, _OFF, _OFF]) + self._buff = array("f", [-16000, -16000, -16000]) try: shell.mkdir("logs/") From 61a7d0aa1a5b34d7e0f2943e2b9385a91c52e450 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 10:04:24 +0100 Subject: [PATCH 290/485] minor --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 054b89e..7c3a985 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -162,7 +162,7 @@ class SleepTkApp(): no_full_draw = True disable_all = False if self.check_al.touch(event): - if self._wakeup_enabled == _ON: + if self._wakeup_enabled: self._wakeup_enabled = _OFF disable_all = True else: @@ -179,11 +179,11 @@ class SleepTkApp(): if self.check_al.state: if self.check_smart.touch(event): - if self._wakeup_smart_enabled == _ON: + if self._wakeup_smart_enabled: self._wakeup_smart_enabled = _OFF self.check_smart.state = self._wakeup_smart_enabled self._check_smart = None - elif self._wakeup_enabled == _ON: + elif self._wakeup_enabled: self._wakeup_smart_enabled = _ON self.check_smart.state = self._wakeup_smart_enabled self.check_smart.update() From 6a32d3f47721294042983804b719ceea47ef18cf Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 10:04:35 +0100 Subject: [PATCH 291/485] less frequent data logging --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 7c3a985..d662abb 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -30,7 +30,7 @@ _RINGING = const(3) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_STORE_FREQ = const(60) # process data and store to file every X seconds +_STORE_FREQ = const(300) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(10) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: From ac0013b9651448f7bc89de530be828252cc75cd9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 10:21:54 +0100 Subject: [PATCH 292/485] preliminary work to refactor UI --- SleepTk.py | 111 ++++++++++++++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index d662abb..2272d8d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -23,10 +23,10 @@ from micropython import const # HARDCODED VARIABLES: _ON = const(1) _OFF = const(0) -_START = const(0) # page values: -_TRACKING = const(1) -_SETTINGS = const(2) -_RINGING = const(3) +_TRACKING = const(0) +_RINGING = const(1) +_START = const(2) # page values: +_SETTINGS1 = const(3) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds @@ -69,7 +69,7 @@ class SleepTkApp(): wasp.gc.collect() self._draw() wasp.system.request_event(wasp.EventMask.TOUCH | - wasp.EventMask.SWIPE_UPDOWN | + wasp.EventMask.SWIPE_LEFTRIGHT | wasp.EventMask.BUTTON) def background(self): @@ -85,60 +85,65 @@ class SleepTkApp(): wasp.system.navigate(wasp.EventType.HOME) def swipe(self, event): - "switches between start page and settings page" - if self._page == _START: - self._page = _SETTINGS - self._draw() - elif self._page == _SETTINGS: - self._page = _START + "navigate between start and various settings page" + if self._page >= 3: + if event[0] == wasp.EventType.RIGHT: + self._page -= 1 + else: + self._page += 1 + self._page = max(self._page, _START) + self._page = min(self._page, _SETTINGS2) self._draw() + def _start_tracking(self): + self._is_tracking = True + # accel data not yet written to disk: + self._data_point_nb = 0 # total number of data points so far + self._last_checkpoint = 0 # to know when to save to file + self._offset = int(wasp.watch.rtc.time()) # makes output more compact + + # create one file per recording session: + self.filep = "logs/sleep/{}.csv".format(str(self._offset + _TIMESTAMP)) + f = open(self.filep, "wb") + f.write(b"") + f.close() + + # add alarm to log accel data in _FREQ seconds + self.next_al = wasp.watch.rtc.time() + _FREQ + wasp.system.set_alarm(self.next_al, self._trackOnce) + + # setting up alarm + if self._wakeup_enabled: + now = wasp.watch.rtc.get_localtime() + yyyy = now[0] + mm = now[1] + dd = now[2] + HH = self._spinval_H + MM = self._spinval_M + if HH < now[3] or (HH == now[3] and MM <= now[4]): + dd += 1 + self._WU_t = wasp.watch.time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) + wasp.system.set_alarm(self._WU_t, self._listen_to_ticks) + + # also set alarm to vibrate a tiny bit before wake up time + # to wake up gradually + for t in _GRADUAL_WAKE: + wasp.system.set_alarm(self._WU_t - t*60, self._tiny_vibration) + + # wake up SleepTk 2min before earliest possible wake up + if self._wakeup_smart_enabled: + self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 + wasp.system.set_alarm(self._WU_a, self._smart_alarm_compute) + wasp.system.notify_level = 1 # silent notifications + def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" wasp.gc.collect() no_full_draw = False if self._page == _START: if self.btn_on.touch(event): - self._is_tracking = True - # accel data not yet written to disk: - self._data_point_nb = 0 # total number of data points so far - self._last_checkpoint = 0 # to know when to save to file - self._offset = int(wasp.watch.rtc.time()) # makes output more compact - - # create one file per recording session: - self.filep = "logs/sleep/{}.csv".format(str(self._offset + _TIMESTAMP)) - f = open(self.filep, "wb") - f.write(b"") - f.close() - - # add alarm to log accel data in _FREQ seconds - self.next_al = wasp.watch.rtc.time() + _FREQ - wasp.system.set_alarm(self.next_al, self._trackOnce) - - # setting up alarm - if self._wakeup_enabled: - now = wasp.watch.rtc.get_localtime() - yyyy = now[0] - mm = now[1] - dd = now[2] - HH = self._spinval_H - MM = self._spinval_M - if HH < now[3] or (HH == now[3] and MM <= now[4]): - dd += 1 - self._WU_t = wasp.watch.time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) - wasp.system.set_alarm(self._WU_t, self._listen_to_ticks) - - # also set alarm to vibrate a tiny bit before wake up time - # to wake up gradually - for t in _GRADUAL_WAKE: - wasp.system.set_alarm(self._WU_t - t*60, self._tiny_vibration) - - # wake up SleepTk 2min before earliest possible wake up - if self._wakeup_smart_enabled: - self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 - wasp.system.set_alarm(self._WU_a, self._smart_alarm_compute) - wasp.system.notify_level = 1 # silent notifications - self._page = _TRACKING + #self._start_tracking() + self._page = _SETTINGS1 elif self._page == _TRACKING: if self._conf_view is _OFF: @@ -158,7 +163,7 @@ class SleepTkApp(): self._disable_tracking() self._page = _START - elif self._page == _SETTINGS: + elif self._page == _SETTINGS1: no_full_draw = True disable_all = False if self.check_al.touch(event): @@ -308,7 +313,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) draw.string('Wake you up to 40m' , 0, 120) draw.string('earlier.' , 0, 140) draw.string('PRE RELEASE.' , 0, 160) - elif self._page == _SETTINGS: + elif self._page == _SETTINGS1: self.check_al = widgets.Checkbox(x=0, y=40, label="Alarm") self.check_al.state = self._wakeup_enabled self.check_al.draw() From 3d19ca1153d02f69d889b22b7c33c4e7410f88c1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 12:45:26 +0100 Subject: [PATCH 293/485] major: refactored UI + major changes --- README.md | 12 +- SleepTk.py | 321 ++++++++++++++++++--------------- screenshots/settings_page.png | Bin 6847 -> 7014 bytes screenshots/settings_page2.png | Bin 0 -> 6927 bytes screenshots/start_page.png | Bin 7733 -> 7516 bytes screenshots/tracking_page.png | Bin 6680 -> 7297 bytes 6 files changed, 178 insertions(+), 155 deletions(-) create mode 100644 screenshots/settings_page2.png diff --git a/README.md b/README.md index 7e5109b..78f059f 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ ## Features: * **sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file * **Flexible**: does not make too many assumption regarding time to fall asleep, sleep cycle duration etc. SleepTk tries various data to see what fits best for your profile. If you still want to customize things, all the hardcoded and commented settings are easily accessible at the top of the file. -* alarm clock: wakes you up at a specific time -* **gentle alarm clock**: vibrates the watch a tiny bit regularly before wake up time to lift you gently back to consciousness -* **smart alarm clock**: can wake you up to 40 minutes before the set time to make sure you wake up feeling refreshed. +* **suggested alarm clock**: suggests wake up time according to average sleep cycles length +* **gradual alarm clock**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness +* **smart alarm clock (alpha)**: adaptative alarm that wakes you up to 40 minutes before the set time to make sure you wake up feeling refreshed. * **privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download if if needed) * open source @@ -29,19 +29,17 @@ # Screenshots: ![start](./screenshots/start_page.png) ![settings](./screenshots/settings_page.png) +![settings2](./screenshots/settings_page2.png) ![tracking](./screenshots/tracking_page.png) ![night example](./screenshots/example_night.png) ## TODO **misc** * log heart rate data every X minutes -* show settings panel after clicking start, instead of a swipe menu - * add a Button instead of checkbox to manage alarm / gentle alarm / smart alarm / both - * recommend best wake up time when setting up alarm * investigate if downsampling is necessary * if self.foreground is called, record the time. Use it to cancel smart alarm if you woke up too many times (more than 2 times in more than 20 minutes apart). * add a "nap tracking" mode that records sleep tracking with more precision -* add a power nap mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops + * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops * log smart alarm data to file? log user rating of how well he/she felt fresh at wake? * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation? diff --git a/SleepTk.py b/SleepTk.py index 2272d8d..332b814 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -27,6 +27,7 @@ _TRACKING = const(0) _RINGING = const(1) _START = const(2) # page values: _SETTINGS1 = const(3) +_SETTINGS2 = const(4) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds @@ -36,6 +37,8 @@ _BATTERY_THRESHOLD = const(10) # under X% of battery, stop tracking and only ke # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 8, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up +_TIME_TO_FALL_ASLEEP = const(14) # in minutes, according to https://sleepyti.me/ +_CYCLE_LENGTH = const(90) # in minutes, according to https://sleepyti.me/ class SleepTkApp(): @@ -44,8 +47,9 @@ class SleepTkApp(): def __init__(self): wasp.gc.collect() # default values: - self._wakeup_enabled = _ON - self._wakeup_smart_enabled = _OFF # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART + self._alarm_state = _ON + self._grad_alarm_state = _ON + self._smart_alarm_state = _OFF # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._page = _START @@ -86,7 +90,7 @@ class SleepTkApp(): def swipe(self, event): "navigate between start and various settings page" - if self._page >= 3: + if self._page >= 2: if event[0] == wasp.EventType.RIGHT: self._page -= 1 else: @@ -95,6 +99,142 @@ class SleepTkApp(): self._page = min(self._page, _SETTINGS2) self._draw() + def touch(self, event): + """either start trackign or disable it, draw the screen in all cases""" + wasp.gc.collect() + if self._page == _TRACKING: + if self._conf_view is _OFF: + if self.btn_off.touch(event): + self._conf_view = widgets.ConfirmationView() + self._conf_view.draw("Stop tracking?") + return + else: + if self._conf_view.touch(event): + if self._conf_view.value: + self._disable_tracking() + self._page = _START + self._conf_view = _OFF + + elif self._page == _RINGING: + if self.btn_al.touch(event): + self._disable_tracking() + self._page = _START + + elif self._page == _SETTINGS1: + if self._alarm_state and (self._spin_H.touch(event) or self._spin_M.touch(event)): + self._spinval_H = self._spin_H.value + self._spin_H.update() + self._spinval_M = self._spin_M.value + self._spin_M.update() + if self._alarm_state: + draw = wasp.watch.drawable + draw.set_font(_FONT) + + duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP) // 60 + duration = max(duration, 0) # if alarm too close + draw.string("Total sleep {:02d}h{:02d}m".format( + int(duration // 60), + int(duration % 60),), 0, 180) + + cycl = (duration) / _CYCLE_LENGTH + draw.string("{} cycles ".format(str(cycl)[0:5]), 0, 200) + + cycl_modulo = cycl-int(cycl) + if cycl_modulo > 0.10 and cycl_modulo < 0.90: + draw.string("Not rested!", 0, 220) + else: + draw.string("Well rested", 0, 220) + + return + + elif self.check_al.touch(event): + self._alarm_state = self.check_al.state + self.check_al.update() + + elif self._page == _SETTINGS2: + if self._alarm_state: + if self.check_smart.touch(event): + self._smart_alarm_state = self.check_smart.state + self.check_smart.draw() + return + elif self.check_grad.touch(event): + self._grad_alarm_state = self.check_grad.state + self.check_grad.draw() + return + if self.btn_sta.touch(event): + self._start_tracking() + + self._draw() + + def _draw(self): + """GUI""" + draw = wasp.watch.drawable + draw.fill(0) + draw.set_font(_FONT) + if self._page == _RINGING: + if self._earlier != 0: + msg = "WAKE UP ({}m early)".format(str(self._earlier/60)[0:2]) + else: + msg = "WAKE UP" + draw.string(msg, 0, 70) + self.btn_al = widgets.Button(x=0, y=70, w=240, h=140, label="WAKE UP") + self.btn_al.draw() + elif self._page == _TRACKING: + ti = wasp.watch.time.localtime(self._offset) + draw.string('Began at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 70) + if self._alarm_state: + word = "Alarm at " + if self._smart_alarm_state: + word = "Alarm BEFORE " + ti = wasp.watch.time.localtime(self._WU_t) + draw.string("{}{:02d}:{:02d}".format(word, ti[3], ti[4]), 0, 90) + draw.string("Gradual wake: {}".format(True if self._grad_alarm_state else False), 0, 110) + else: + draw.string("No alarm set", 0, 90) + draw.string("data points: {} / {}".format(str(self._data_point_nb), str(self._data_point_nb * _FREQ // _STORE_FREQ)), 0, 130) + self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") + self.btn_off.draw() + elif self._page == _START: + draw.set_font(_FONT) + label = 'Sleep tracker with optional wake up alarm, smart alarm up to 40min before, gradual wake up to 15m. Swipe to navigate.' + chunks = draw.wrap(label, 240) + for i in range(len(chunks)-1): + sub = label[chunks[i]:chunks[i+1]].rstrip() + draw.string(sub, 0, 60 + 20 * i) + elif self._page == _SETTINGS1: + self.check_al = widgets.Checkbox(x=0, y=40, label="Wake me up") + self.check_al.state = self._alarm_state + self.check_al.draw() + if self._alarm_state: + self._spin_H = widgets.Spinner(30, 70, 0, 23, 2) + self._spin_H.value = self._spinval_H + self._spin_H.draw() + self._spin_M = widgets.Spinner(150, 70, 0, 59, 2, 5) + self._spin_M.value = self._spinval_M + self._spin_M.draw() + + elif self._page == _SETTINGS2: + if self._alarm_state: + self.check_grad = widgets.Checkbox(0, 40, "Gradual wake") + self.check_grad.state = self._grad_alarm_state + self.check_grad.draw() + self.check_smart = widgets.Checkbox(x=0, y=80, label="Smart alarm (alpha)") + self.check_smart.state = self._smart_alarm_state + self.check_smart.draw() + else: + draw.set_font(_FONT) + label = 'Skipping smart and gradual alarm because no regular alarm is set' + chunks = draw.wrap(label, 240) + for i in range(len(chunks)-1): + sub = label[chunks[i]:chunks[i+1]].rstrip() + draw.string(sub, 0, 50 + 24 * i) + self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start tracking") + self.btn_sta.draw() + + self.stat_bar = widgets.StatusBar() + self.stat_bar.clock = True + self.stat_bar.draw() + def _start_tracking(self): self._is_tracking = True # accel data not yet written to disk: @@ -112,106 +252,49 @@ class SleepTkApp(): self.next_al = wasp.watch.rtc.time() + _FREQ wasp.system.set_alarm(self.next_al, self._trackOnce) + if self._grad_alarm_state and not self._alarm_state: + # fix incompatible settings + self._grad_alarm_state = _OFF + # setting up alarm - if self._wakeup_enabled: - now = wasp.watch.rtc.get_localtime() - yyyy = now[0] - mm = now[1] - dd = now[2] - HH = self._spinval_H - MM = self._spinval_M - if HH < now[3] or (HH == now[3] and MM <= now[4]): - dd += 1 - self._WU_t = wasp.watch.time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) + if self._alarm_state: + self._WU_t = self._read_time(self._spinval_H, self._spinval_M) wasp.system.set_alarm(self._WU_t, self._listen_to_ticks) # also set alarm to vibrate a tiny bit before wake up time # to wake up gradually - for t in _GRADUAL_WAKE: - wasp.system.set_alarm(self._WU_t - t*60, self._tiny_vibration) + if self._grad_alarm_state: + for t in _GRADUAL_WAKE: + wasp.system.set_alarm(self._WU_t - t*60, self._tiny_vibration) # wake up SleepTk 2min before earliest possible wake up - if self._wakeup_smart_enabled: + if self._smart_alarm_state: self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 wasp.system.set_alarm(self._WU_a, self._smart_alarm_compute) wasp.system.notify_level = 1 # silent notifications + self._page = _TRACKING - def touch(self, event): - """either start trackign or disable it, draw the screen in all cases""" - wasp.gc.collect() - no_full_draw = False - if self._page == _START: - if self.btn_on.touch(event): - #self._start_tracking() - self._page = _SETTINGS1 - - elif self._page == _TRACKING: - if self._conf_view is _OFF: - if self.btn_off.touch(event): - self._conf_view = widgets.ConfirmationView() - self._conf_view.draw("Stop tracking?") - no_full_draw = True - else: - if self._conf_view.touch(event): - if self._conf_view.value: - self._disable_tracking() - self._page = _START - self._conf_view = _OFF - - elif self._page == _RINGING: - if self.btn_al.touch(event): - self._disable_tracking() - self._page = _START - - elif self._page == _SETTINGS1: - no_full_draw = True - disable_all = False - if self.check_al.touch(event): - if self._wakeup_enabled: - self._wakeup_enabled = _OFF - disable_all = True - else: - self._wakeup_enabled = _ON - no_full_draw = False - self.check_al.state = self._wakeup_enabled - self.check_al.update() - - if disable_all: - self._wakeup_smart_enabled = _OFF - self.check_smart.state = self._wakeup_smart_enabled - self._check_smart = None - self._draw() - - if self.check_al.state: - if self.check_smart.touch(event): - if self._wakeup_smart_enabled: - self._wakeup_smart_enabled = _OFF - self.check_smart.state = self._wakeup_smart_enabled - self._check_smart = None - elif self._wakeup_enabled: - self._wakeup_smart_enabled = _ON - self.check_smart.state = self._wakeup_smart_enabled - self.check_smart.update() - self.check_smart.draw() - elif self._spin_H.touch(event): - self._spinval_H = self._spin_H.value - self._spin_H.update() - elif self._spin_M.touch(event): - self._spinval_M = self._spin_M.value - self._spin_M.update() - - if no_full_draw is False: - self._draw() + def _read_time(self, HH, MM): + "convert time from spinners to seconds" + now = wasp.watch.rtc.get_localtime() + yyyy = now[0] + mm = now[1] + dd = now[2] + HH = self._spinval_H + MM = self._spinval_M + if HH < now[3] or (HH == now[3] and MM <= now[4]): + dd += 1 + return wasp.watch.time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) def _disable_tracking(self, keep_main_alarm=False): """called by touching "STOP TRACKING" or when computing best alarm time to wake up you disables tracking features and alarms""" self._is_tracking = False wasp.system.cancel_alarm(self.next_al, self._trackOnce) - if self._wakeup_enabled: + if self._alarm_state: if keep_main_alarm is False: # to keep the alarm when stopping because of low battery wasp.system.cancel_alarm(self._WU_t, self._listen_to_ticks) - if self._wakeup_smart_enabled: + if self._smart_alarm_state: wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_compute) self._periodicSave() wasp.gc.collect() @@ -236,7 +319,7 @@ class SleepTkApp(): if wasp.watch.battery.level() <= _BATTERY_THRESHOLD: # strop tracking if battery low self._disable_tracking(keep_main_alarm=True) - self._wakeup_smart_enabled = _OFF + self._smart_alarm_state = _OFF h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Bat <20%", @@ -273,65 +356,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() - def _draw(self): - """GUI""" - draw = wasp.watch.drawable - draw.fill(0) - draw.set_font(_FONT) - if self._page == _RINGING: - if self._earlier != 0: - msg = "WAKE UP ({}m early)".format(str(self._earlier/60)[0:2]) - else: - msg = "WAKE UP" - draw.string(msg, 0, 70) - self.btn_al = widgets.Button(x=0, y=70, w=240, h=140, label="WAKE UP") - self.btn_al.draw() - elif self._page == _TRACKING: - ti = wasp.watch.time.localtime(self._offset) - draw.string('Started at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 70) - draw.string("data points: {}".format(str(self._data_point_nb)), 0, 90) - if self._wakeup_enabled: - word = "Alarm at " - if self._wakeup_smart_enabled: - word = "Alarm before " - ti = wasp.watch.time.localtime(self._WU_t) - draw.string("{}{:02d}:{:02d}".format(word, ti[3], ti[4]), 0, 130) - else: - draw.string("No alarm set", 0, 130) - self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") - self.btn_off.draw() - elif self._page == _START: - self.btn_on = widgets.Button(x=0, y=200, w=240, h=40, label="Start tracking") - self.btn_on.draw() - draw.set_font(_FONT) - draw.string('Sleep tracker with' , 0, 60) - draw.string('alarm and smart alarm.' , 0, 80) - if not self._wakeup_smart_enabled: - # no need to remind it after the first time - draw.string('Swipe down for settings' , 0, 100) - else: - draw.string('Wake you up to 40m' , 0, 120) - draw.string('earlier.' , 0, 140) - draw.string('PRE RELEASE.' , 0, 160) - elif self._page == _SETTINGS1: - self.check_al = widgets.Checkbox(x=0, y=40, label="Alarm") - self.check_al.state = self._wakeup_enabled - self.check_al.draw() - if self._wakeup_enabled: - self._spin_H = widgets.Spinner(30, 120, 0, 23, 2) - self._spin_H.value = self._spinval_H - self._spin_H.draw() - self._spin_M = widgets.Spinner(150, 120, 0, 59, 2, 5) - self._spin_M.value = self._spinval_M - self._spin_M.draw() - self.check_smart = widgets.Checkbox(x=0, y=80, label="Smart alarm") - self.check_smart.state = self._wakeup_smart_enabled - self.check_smart.draw() - - self.stat_bar = widgets.StatusBar() - self.stat_bar.clock = True - self.stat_bar.draw() - def _signal_processing(self, data): """signal processing over the data read from the local file""" @@ -476,10 +500,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._listen_to_ticks) # replace old gentle alarm by another one - for t in _GRADUAL_WAKE: - wasp.system.cancel_alarm(WU_t - t*60, self._tiny_vibration) - if earlier + t*60 < _ANTICIPATE_ALLOWED: - wasp.system.set_alarm(WU_t - earlier - t*60, self._tiny_vibration) + if self._grad_alarm_state: + for t in _GRADUAL_WAKE: + wasp.system.cancel_alarm(WU_t - t*60, self._tiny_vibration) + if earlier + t*60 < _ANTICIPATE_ALLOWED: + wasp.system.set_alarm(WU_t - earlier - t*60, self._tiny_vibration) self._earlier = earlier self._page = _TRACKING diff --git a/screenshots/settings_page.png b/screenshots/settings_page.png index 5f25e99239922a267f235b3ca9d97a03444b77de..0fb922f916fa0f970dd07c712b50683d3edff9b3 100644 GIT binary patch literal 7014 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

V0*qb;`u7 z=pCkMh>rEfrMOO3rrQ2up<>WXMVX)WnVHI}q_Z48nwfpA8a(mw=*YZ0H5r7uJn)2% zW=!E^YQFtM^lt`DzV72Rz&WNpblcmlZ%nzdla}XnN27PYH68vmP^7yMe_(+8E5 ziLTz&Nt8LVQ+0w_O(@$%Xqx|eYj`OjYpChX*kie6{Z;O&h!xJae)GjWme0~_kTR}_ zybKKbYEM~;*kT-=N;-F8WTLHN9!_pIiz7635P}-w>~gYgew^(y4d+qm3r>Bygn>SG z5#qdFb|S9(S)M^E_ormjE)|yzhe}^SD%nSg|0E~;`zsLU*t>q_cTz7f3Woc<{Vbfa zKI91t^gw3dm~X_jDlv@m^uAd|d}3~qvxwExdiK2RN?5d(qfimDve0|!!=UTFhoe~D zjS6a!(aYhZS;Y#74~m}c7W}EU%gukLB9^&umOtE>BX7)_MysFt8Hjwiq4+!H-6*@t z;FZ2`&I5<0zU0wuip7A&vHeBGgt_WvC>yQ8!|CdSWUL1_{8p_1u4P9covNZw#vCEK zVe&pxHK8%D3w@wX9qG(pPCwG9vM64x;uW05a$Wwl9u3>X?$mvi+c&7ph=PKTYvQ2i z;DDvq{?_OG>8vT;(eJci>}R!N@?%M`nK7GqR&Wez#S84bCqRsdKz@0N`~z!0sV7L& zD`kX+4rj1?FVhMO=1EtW!g`zqXAfUuc4<&AubI6RbIP$=^@sl$hnrPWsE!j zlo#bwn7Y%`dMERU@wDE;K`3j`7%8i3{7eK6?~1ZVUIvrtMTptCy7pszKT5|ry36uY zn!;MGnp=2wUg(pBBzl2EUa?S|->6uD?7b?~$Svr*aMmU8epvU1O5Thw$c`5gdz}VS zM&3{5PM4(b!7j8eCfMH4pJF~`oZ=0oZE^Bn3(4>IB`JwnzwQ}3H=nup3=C&pmsoDm z2%N5l$Rj2a-$}@Y1dMJS8FI6_`?@vblEN-im^gg-?q24#;?$#vn=FzWdDb%!&Yr|4*voIq7a{v|pHv$+Qe-Bn=ws9x|qL8$E^q?HjHx`?RW*`xV zjYt_g6sk&z1CtxilL1F zr7uuNdXGPrPWZUI+}-s!^bZJl+$aD#EgvuMiaTkCtCrJkV>>Jxr5@V{|6xY6K zOwY~DVX;TN?bRFG-YSLJ7VO}jBt8zwZRNp(gT_E~YwP2RaS;)b!UPZvHy>ZkxC_z1 z)|upYbtO@Ci&;`SF8+$yS0IcSaB&({b$``zIy91t{<45*Wc@Q8NfbIYo6XDUd zgZ=JZA1nZKhJu&n-Z%9N*aZg@=xmN7@a8H%Z36>mpo=|6M@Q}3oo9dGf-zTn@fkI+4IbaYyY zS4WVWV`F38-4Uc3cf~A#=3%c-8kL!drAjcwQ$qk7SKvy36)f^imjO`jA)ohE$Iwu? zdg#t4>Y`cP{kW*6rY3*~S65dDLhs`SyziqfPozATlMA!LI z4UY%s<_NZ&Mc#DNe$Ib=FqPf`|R-!pfiAgLY{BfV3WSu z?gBXVuNkso9wKXuA2wiRfq2wn|G9t#o>OX8A&uUNX#3PW?ItB5A>>LM<_@f%m^2^> z>7>!g{>lz};&1&HLhvX1E8|HCu*fmc=`ewSM%sUt8&dOH$@UX@E;-I0Bj-36yrR1B z5UR+P(AwH6&p&7#UZsdh=$l z#3u~|umD9x!X&vQwLOfnfqLqmCC3jKuvCCKB|(YDtzIhx0})f$*|&;>Lx8s<4KFwEqjS=_K^+o>Ip)~eu@0Tv;ZjY>bd$O5x`m-Co8!IH|yy%0{!MZ zdbUIHstr8Bz;0%rnlI0lus|Z>0n8tKIR2>J>+Er>l^JUQD5P}6sdVkAQjCq(hVYAn z512kRum49_Ni7SwZ*+9@LS#RJ2mBsTTA-ghdOmS{>>t7H7FInyHD!oLax2NpdoN*7 z?OSadIlvtU;1Gp!ptz|7`mU;~D&QA7y1Ng+CT^V{`~dDs6tNOd`ave44;5lGg8%>k literal 6847 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 literal 0 HcmV?d00001 diff --git a/screenshots/start_page.png b/screenshots/start_page.png index 01e3d127f465cd588906007be464f2610aaacb83..40d636e45a779985fa38047591cc80e7e92ce533 100644 GIT binary patch literal 7516 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 VC(cd>plb?xrmCY-rDXl)-vI6GI!*up literal 7733 zcmchcc|6qL+y93e3Z+7WgzUmhSwhxCmSl^B8AGyU-*-lsBs<|_CwoFA%V12}>|1tY z2H8z^V;al2-`$VzU-$3($Nl~NasM%odA!egKdy7$=UmryuJe4w8X0JxW8!84fk5YU zbu>*tAUXu_D?URHw1B*gV?m&+N4lDKOapV*2}mcN@uH6HEW+qUcPhD@HY}Tgz<-QB zb*?(?FqyG&p@@14Yjn3MS=~eYTe#CayT*MxCo%M8?)kA~+1Dd-vgqhN*5`-GF`xWC zvx@sa(-q&lHmY`|OP3AZlqwlSb`0?2TOxLO`a3T5X33!Xe^a7(A64baI0h^qE%o+h z35JA(;HCQ-+sM_(lY?=Kar!NCdTr?9@5wrgaSenxi3!ws)tfHjB^<;r00F)11cLb##4xay!FgiL~S}MGb#vN|7O!b_W4W_kdRN_%_zhYugx+wZKS zLniTfd~Dyzq%t->US2)8&Y9h}gl2}E=TRuEz6E;CbK7^NG}GF--qOqR(|s&% zD+zTpztEM2qU{zaojk?+SDIc=*UPX!$HMY0xCKO;nVQPBw^;XYA-D&)VF2qV-(lfi zdn-f#NSO4Z;~=nn)v#bYyRU~&D^0n%xu;qJJbG`_C4XpYYHDqjl%1o|Xb@!Rb`|p2 zIX6{sS)+vzzSAX&+OI1JDv(Uq(@x}xeaH&xc5rZ*T5q6amFEYqiK5O#N71A*RQd5Xb-4gF zhla3sr>G=0c`tTD+P2URb*o2~m8R$URNuW<1?lGGT-0oM}%vD3F{`@s}jHXhl#o9@?xX&$*cXjEaErSs){?((-XEV(}O?ss!7`UqM ziczn3Sy@>wV^mbs@bGZJv{#HPSOd8|&v!C$4!kLzP~qU{SPr~VR@Ttaa0bk;-a;Tw zO?glC{QmvUThToVfk1?9V3rpkEUPd{DXH}t9LmpclxkL8H>kKmuP)SmmyrPd+S%E8 zbad38r(~bOG>c!CHal{3W!K~c{$ps!t{E%7!W%cUns*ZhdPIaRqBlGRVrl8%We3g*gyC8gn zsECLVvzsh8Gcz;M@$KkZ4Webbh7qI-EDK-Q-q6xg8Kvv^kkXSR;+F#2o+#zRkuaNw*dCd@j&Jk`3qyEYfDmr3uD|h(14@EvgTkz{Zmk|zaz?Mf z9n2Q6u{t#qi}^>NB&!}(jgrU<4wzHH-8(zpSnJwR=M5R+l8Ej{W5z{|5g8sC8JUfF z%j!qfj|8R8^PARNk{cvFiqo!94+`OdC&!eNO|47j=PpAagtUu`-dRSVOTXYfKD>?i z_3z)m4{A;OumQOd_w>=lky(NaTDq<+@We3s$n!pkNtN1V*qmff(~84K3mcvsE@_m+ z)(~3WO7;L@YH4AS0>2OuY1QZ+1e3RJ@t68Tc84m+biCN1I0` z+#2h9Hopkt>tD>hAb!o6o16Osrca*3PTmPuh+;X1nN@L~^%YFcE-Jc4`9MTXy}bsR z&ez3Sk6*C_(M!T$Enm6zh#%kWHk6i}bU;COG)@Mj+@XPn(bBfc$`m7hjo*_!fAyz7F(oTALSJZT z?^AlkQGpNLw-n7`J5g43O(^I>3NuJ^fgW@b1Nz@T8rO&-?INGCCtW=7o_oMK&?(Km9$oNfl;6nvd&DlYK@>&lR>*$LKyz&JiO;#Nt2bKK*JdZ$ zg{B^R_My|Qab7Avl^!(jaCf=dEN#@lgEnD-_|_R0rY$*kb%XwkWwwZej>ka*;H+C7 zE8@t6)ZZ*-^R~|{o!}ld?lWLm{c&~3sw`ea&+(dh zZzovd-6X|TY@wWOBY~rn`_UT`JrFEi%8@%%`9Fk&RMc#ft#yGk-dM=yeV#i z@rviOPj{)=d0%VVrkKhuY{lm#pQn@SwOt(QiidAqDIc5{EZl9yopU=0s!?X?M9;S0 z$6h`cv=JTiNPHkY#+*2y)E)06OUlqg2q`Tv2{N15_%4RrAs#Zj3$i+`$G>3M>kdJ+ zGr6oq(--E^VJ;Lh-n~<%lxIe<>cQMgyt?!GH{6q~BPMjmT&Z{2wBP7qG@PG3b?Fl| zqKv3id?_(}se;V+vzDeGDX05-8)NfGEZU$Rs+?AZtqGkUFsmou_WGqcW~LK0`L}N` zl3rB9@TLOf40=SNTBQl6F2EjJiLr4KD;W+HsMex=SA3^@>4}GjN5Jvdr6q^zM?Son zJ&W7h+cmJ9>}+pd78aJ*dfD2#WC{fag|11lbq)fVygqZ%x!!%IZCyD(H~05J_PP9%3ee9>jTtv3-Iq+g*ZZ+JjY>QWszk4jyiWB#eIWAsuiR zO|{prABbkvck-BkYgN{)67^QByKt&|^QqLWi)mcP;4GE=LejmZajWxz55|}4T~peN z2Fc(Zj`OOaMu9!;(U&FO8T6nT0=$0lgBkW9JgJBB&wHT~9#>5f+G2Ek21KIt&X0In zWMM@J3&R$_X#6UyB1Jd3wN(G8=feh9)*r?*&bd3q`A^itIR}wOS4I|8evz%@etGK-k!EK2w<9HG2V%&xIt5s4}TiN}YTGi^^9OpOW7W@r)$@3bQ zfKQFiZ)AOJw3Lf`N@Y4#^~QZtx@zU!`vo*M;?jt76=I1aq9>lFk$bP=1D%9xld+Xe zuf)rpQx_s<;w;$`WRLF9(uNe?(x(EJKssE&1`F|Of5NXf$`eaO! zkFCG|{t87 z{KmyOO9#BPa0SL-cfhRisYicI2=lU+Gd$o2`&^ceJhIcYcvmi5NnW?8PEj)qFzk$Y za_}SK75JT4IN?aoi1vf|GqBg5j3qy`1dhd*!X8SKQZaqvCUTK2?Zz{9M}}spn3=$v zoWsbMGcf~tk48NMhJqbsb=KGaIZS{=&|z#g_EUo7ZVynu$1+?h3gAp;P8^(nDitSm zZP(d1FHhLvS)%4lB4C5=AFuHCp6NW@T<5?{vo_u$2qA`ErVMAZ@#A70cV78f6Dz+< z_HPrAFD(OaGbw+>N6;dVd|!B7){U%ju1GIF{vF)gSnP>532)C0ok!c=LtWdmVhs~n z8j0$5lML?@?n$u-Im-S_<RI|dhN zbKg+W-7;wMNn%$X*4?U+{Is>#g)+);^pHI9>Pl87i*+3)1_!nMsOVjR1%^ZrJ;pbw z{q{i}f} z*goDun%gTmJ%0t($+P0G0+}_}8L1DMk9E6Bb<{_-*YfdJqIjnRwv-soE8I$);aar} zTtWY=Af;Qp_Yr!Tt+Ux#OCLUP!%6t+-$7=&qe9MgR}1frggMRmjau*uN>$uyd+cx& zDTKSGpIRFY{jmob5rb>4G`H+tu`IWjAGHWv(P>j)luJdKT!~#!r!gtk0O~oZq)x2z{bnQR?Z%yb7wg@9d1_QYy{3n*$Nym zix<^_q6U#V85g~5G=)<{09Vj-*n`OCH_DLqLku1Zuwh4$vy%M$Ux5g){vH0@26zVX ze|=BWSec~5U8EsAn3y;R_--i0Ev$o#J)nd2I_fG}YR>D_o&n}$N8pK z^(Q5GKS+EHNx}F$w_qJg*F8o@ zoINwI-_H3=^-Dx}%a^yOh{nA@1je#QdvrO_wzcO+f~e(?p-u#_Na z8^;Y_Xe&Cz5k)!({oKM#Q(wTH31$>0tta28bmcFo}phP9|Z`5FD=E9#$#_2UBUMAreYY8(~~D<&LYA_*Id%~MN_Ms&?0NJ%8y%c{cQq zzUCHwz;Uz7DM&4`b6xJ}AC8B-Z#lP~Hu#1;^o8#5Zyuv{$}8*j+biq`ZF4!qaV8$m zjbns5PRd@h-G~u?*4#>gfJ&17g`)KS-p+TO!omm*>j3|7l{n4sQwkhwJsU6y35mG4 zxITwE>)NEmM2E*xAg=4jE81!Y0LR!2dpqP5`e!a?cX!uf^sIV=UU6|TKxg<23se!+ zkLqezBH&^c8D>?Z;RnM7{r&ynhxi%VUb$MHUpnaN?|ITiJ3G7O{=Am(;{&`0E^Pnz zB%7$g)4yY^NfWmxKG=06Ud2yOzZ-R4H9w=i#EJV4E=d1z(f7Z*iKV}jxbQ1s{6v&= zy)c58Ho%tUYI#DQK$YW6z25M}dnJ`i~jC4>gw8rMARcU6JADZs{n5rPuSn)*62I?D*A}%u@Aq1|eG-+|% zfbitaWXv=&cdk@Qy>-G|xG;zAB`Zt(U?k>PnGSj1$(R3)O+v%0Xca$O=rH#Q0W-}D zuh`!%H{L^cA4&;eRvF)6Nz39RioS@v0`=#ITS0>@82(}x?VEvd`Zy1+&^Wn3B{UVm&gQYxg>Ty*Wy6e1OF zbkA}5&2Ah?QY{UhJo)C;w}2y!ddmlOyv>1)yvy46pXC`cyw0UD2%+|_z^VOOLahEz zUPL0#B6jvwwoAo=MZP#Kp37QniloM%wr@{Qc1Y zwvx<{K-3a4r79HJ2w<&DX&f?;*68`4I!K||2m`MMv4`wx5V)~Y0bR?tBY8}T#Af(E zXjhI@6V+T@Hp&C^jo@O#Pdk&={OWHy(Vg+`35H-G8KU|P{U#mJaS3YM2 z^FpW%40c#&{Uu?Enrk~8JHFLdgJL2kwzOHh4IBE1qEsz#JX6a6Fj2DhNpq2zN9L zapTJ`d%~+-XtTm!5>KPwF_@o3>b${$P9@We7ytW!$@0qEncPJ7H#3Qe+^Vke2FSTA zH{h@YlI7~^>Z%|01pOXgz71@PIe8}X)#H?C7 zsFLgIRl@YNFE8o1)_}Wvhj$L(eBWRfBH{sdPT6h?JCx0McMYBwk9S{=(1H;(Z*fjEPt!C3%fyrA@U%pCdltFGtOaD@}hQnW5$b-PZ07Q``x8U$; z5#<{<`0HL3!vBnvt1c=hV<@(pM65fcbky04{*q27?&R2`Q8z1 z>P$BppvzeTZ~$L>@p-trH=7%QRz3o}zrKD^xxo#OUwNu?YpoEj5WMG9VrH2kr>MB9 zN}QMg90SPd#>{ks&z?Rl8FC5?Xf&owXV??`#u3sm=5F+SM^Z~tEuuvP^9!JuElD|j zOJBZ39#QXu5M%&mHhFyw33wC0$J2`s#3tO1xaw)(_3YU*$_AmIVA4 zSXhhy`cD2GF#q424}j4WPu#~)vBw#5Dk|%;yO!1dyGwmnmHeZjmqw7BTwItF+F@RY zF{6mSfkCii?8MYmFaRGtX4(+YOzgZMov)m%tSK}TXgF?XQYYsJO+dL8pGN%?(pQv6L`W0%OG``F)0sJ~UanUv zt*BUenTg17_>Ij_=S3vP6!N_!2}>d7A)D` zG-U~QN87-_z~JEEfqa!z<`-qB%63uX@3Ak8>RvxO;S>lcC~QCNTA^wA&f#m9g~oP=LA!mc?M@@ zPP!#(n<(B5mes(@Rd9fl6EPS?dGv$D7-GrEs6$a$Ng%iA4% z=3?4wN0C(o_rDXsdCq<5=)vz#OmGfk=_j54Mb+~|ceY+fX z11MS=Y($LD^56PLn>R?vpzGD&eA3(73%m|RwoQ|3`#-bb8-$k==r43LPmsZ^ zs()tVU~Lt)F9-cu*8L9`Gm(jky=T=!1AtKRKpo;0`X&MOYS#Hy4(^b!3AJtwr0y*G zZX~@0)E93>GzXn?c3+ti-tvuJ=}!pm&-V^K)KQ8y@6d!8kepm>ZRY?9?l{Mzjk=Hb zU=Dapl9QGeKrx<4ZmX}Cqd=do9~>M2+E3_!3Pt&^2&b}_ykJ_BveFB#1w=RsF~q~( zUO09nltx|tZ^iGidS(o7@Jg&`{-CvMy(IzoU?8(CoTS8Omu!U9JgQ=3<<+m)BD4gE zno1Xb2h@xWzysg86eX^v{@X(4|3u3C9|aT*LW%&cj-2%4f8}6ZEd$LO4V%dS0))i} A0ssI2 diff --git a/screenshots/tracking_page.png b/screenshots/tracking_page.png index 519bc747de00b47eba6e8e06bebed8175c61f5c7..cb85955ee3565a4444181d9dc7f9e21ff5de01bb 100644 GIT binary patch literal 7297 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 KR;pC63i~(ZD#KX- literal 6680 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 From 706144b30cb19f2c353df6c7991d9a2f49b523e1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 13:01:10 +0100 Subject: [PATCH 294/485] vibrate a tiny bit when using the spinner --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index 332b814..b6f5580 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -126,6 +126,7 @@ class SleepTkApp(): self._spin_H.update() self._spinval_M = self._spin_M.value self._spin_M.update() + wasp.watch.vibrator.pulse(25, 15) if self._alarm_state: draw = wasp.watch.drawable draw.set_font(_FONT) From 41b219b4f652c1a5268c51b5a20c612e5bdf4403 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 13:14:05 +0100 Subject: [PATCH 295/485] warn user that cycle length currently only used for printing --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index b6f5580..9653c01 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -38,7 +38,7 @@ _BATTERY_THRESHOLD = const(10) # under X% of battery, stop tracking and only ke _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 8, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up _TIME_TO_FALL_ASLEEP = const(14) # in minutes, according to https://sleepyti.me/ -_CYCLE_LENGTH = const(90) # in minutes, according to https://sleepyti.me/ +_CYCLE_LENGTH = const(90) # in minutes, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! class SleepTkApp(): From 0fe8ce9ae1720e05465184a2bd98fbe49239a5ee Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 13:14:16 +0100 Subject: [PATCH 296/485] minor: style --- SleepTk.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 9653c01..1f5b553 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -114,12 +114,10 @@ class SleepTkApp(): self._disable_tracking() self._page = _START self._conf_view = _OFF - elif self._page == _RINGING: if self.btn_al.touch(event): self._disable_tracking() self._page = _START - elif self._page == _SETTINGS1: if self._alarm_state and (self._spin_H.touch(event) or self._spin_M.touch(event)): self._spinval_H = self._spin_H.value @@ -130,28 +128,22 @@ class SleepTkApp(): if self._alarm_state: draw = wasp.watch.drawable draw.set_font(_FONT) - duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP) // 60 duration = max(duration, 0) # if alarm too close draw.string("Total sleep {:02d}h{:02d}m".format( int(duration // 60), int(duration % 60),), 0, 180) - cycl = (duration) / _CYCLE_LENGTH draw.string("{} cycles ".format(str(cycl)[0:5]), 0, 200) - cycl_modulo = cycl-int(cycl) if cycl_modulo > 0.10 and cycl_modulo < 0.90: draw.string("Not rested!", 0, 220) else: draw.string("Well rested", 0, 220) - return - elif self.check_al.touch(event): self._alarm_state = self.check_al.state self.check_al.update() - elif self._page == _SETTINGS2: if self._alarm_state: if self.check_smart.touch(event): @@ -164,7 +156,6 @@ class SleepTkApp(): return if self.btn_sta.touch(event): self._start_tracking() - self._draw() def _draw(self): @@ -213,7 +204,6 @@ class SleepTkApp(): self._spin_M = widgets.Spinner(150, 70, 0, 59, 2, 5) self._spin_M.value = self._spinval_M self._spin_M.draw() - elif self._page == _SETTINGS2: if self._alarm_state: self.check_grad = widgets.Checkbox(0, 40, "Gradual wake") @@ -231,7 +221,6 @@ class SleepTkApp(): draw.string(sub, 0, 50 + 24 * i) self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start tracking") self.btn_sta.draw() - self.stat_bar = widgets.StatusBar() self.stat_bar.clock = True self.stat_bar.draw() From 43204857d13bc83f39ce7425d38055967e5cf48b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 13:15:08 +0100 Subject: [PATCH 297/485] new: notification if smart_alarm_compute is launched by mistake --- SleepTk.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 1f5b553..0f3e012 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -438,6 +438,14 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _smart_alarm_compute(self): """computes best wake up time from sleep data""" wasp.gc.collect() + if not self._smart_alarm_state: + t = wasp.watch.time.localtime(wasp.watch.rtc.time()) + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), + {"src": "SleepTk", + "title": "Smart alarm computation", + "body": "Started computation for the smart alarm \ +BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) + return mute = wasp.watch.display.mute mute(True) wasp.system.wake() From 684ddf574b06db72a448af5949da8f80eabbf1b2 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 13:15:16 +0100 Subject: [PATCH 298/485] minor: up to date top docstring --- SleepTk.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 0f3e012..8fe6f8a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -4,12 +4,9 @@ """Sleep tracker ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# https://github.com/thiswillbeyourgithub/sleep_tracker_pinetime_wasp-os - -SleepTk is designed to track accelerometer data throughout the night. When -finished, it will also compute the best time to wake you up, up to 40 minutes -before the alarm you set up manually. - +SleepTk is an alarm clock app designed to track body movement throughout +the night to optimize sleep. Currently a WIP, more information at +https://github.com/thiswillbeyourgithub/sleep_tracker_pinetime_wasp-os """ import wasp From b15fe4e6eb40a5d367b8ebb942e289d7c90dbb50 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 13:16:01 +0100 Subject: [PATCH 299/485] remove vibration when touching spinner because too slow --- SleepTk.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 8fe6f8a..c085704 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -121,7 +121,6 @@ class SleepTkApp(): self._spin_H.update() self._spinval_M = self._spin_M.value self._spin_M.update() - wasp.watch.vibrator.pulse(25, 15) if self._alarm_state: draw = wasp.watch.drawable draw.set_font(_FONT) From 565150aedc579e6e539691d38bdc150753264b4e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 13:25:31 +0100 Subject: [PATCH 300/485] new: catch exception and issue notification in trackOnce --- SleepTk.py | 53 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index c085704..7b171d5 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -289,31 +289,42 @@ class SleepTkApp(): """get one data point of accelerometer every _FREQ seconds, keep the maximum over each axis then store in a file every _STORE_FREQ seconds""" - if self._is_tracking: - buff = self._buff - xyz = wasp.watch.accel.read_xyz() - buff[0] = max(buff[0], xyz[0]) - buff[1] = max(buff[1], xyz[1]) - buff[2] = max(buff[2], xyz[2]) - self._data_point_nb += 1 + try: + if self._is_tracking: + buff = self._buff + xyz = wasp.watch.accel.read_xyz() + buff[0] = max(buff[0], xyz[0]) + buff[1] = max(buff[1], xyz[1]) + buff[2] = max(buff[2], xyz[2]) + self._data_point_nb += 1 - # add alarm to log accel data in _FREQ seconds - self.next_al = wasp.watch.rtc.time() + _FREQ - wasp.system.set_alarm(self.next_al, self._trackOnce) + # add alarm to log accel data in _FREQ seconds + self.next_al = wasp.watch.rtc.time() + _FREQ + wasp.system.set_alarm(self.next_al, self._trackOnce) - self._periodicSave() - if wasp.watch.battery.level() <= _BATTERY_THRESHOLD: - # strop tracking if battery low - self._disable_tracking(keep_main_alarm=True) - self._smart_alarm_state = _OFF - h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] - wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", - "title": "Bat <20%", - "body": "Stopped \ + self._periodicSave() + if wasp.watch.battery.level() <= _BATTERY_THRESHOLD: + # strop tracking if battery low + self._disable_tracking(keep_main_alarm=True) + self._smart_alarm_state = _OFF + h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", + "title": "Bat <20%", + "body": "Stopped \ tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ on.".format(h, m, _BATTERY_THRESHOLD)}) - - wasp.gc.collect() + except Exception as e: + mute = wasp.watch.display.mute + mute(False) + wasp.system.wake() + mute(False) + h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), + {"src": "SleepTk", + "title": "trackOnce", + "body": "Exception at {}h{}m : {]".format(h, m, e)}) + finally: + wasp.gc.collect() def _periodicSave(self): """save data after maxpooling over a window to file From 447f7e49ae4d32f4968c5267191c793d26ab12b0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 13:25:57 +0100 Subject: [PATCH 301/485] remove useless keep_awake --- SleepTk.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 7b171d5..c8c9b4f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -456,7 +456,6 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) mute = wasp.watch.display.mute mute(True) wasp.system.wake() - wasp.system.keep_awake() wasp.system.switch(self) t = wasp.watch.time.localtime(wasp.watch.rtc.time()) wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), @@ -538,7 +537,6 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) mute = wasp.watch.display.mute mute(True) wasp.system.wake() - wasp.system.keep_awake() wasp.system.switch(self) self._draw() wasp.system.request_tick(period_ms=1000) From 383c410804d353f2252992f0589185300e97a57a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 13:30:09 +0100 Subject: [PATCH 302/485] store battery level once more --- SleepTk.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index c8c9b4f..79e5282 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -332,6 +332,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) 1. arm angle 2. elapsed times 3/4/5. x/y/z max value over _STORE_FREQ seconds + 6. battery level arm angle formula from https://www.nature.com/articles/s41598-018-31266-z note: math.atan() is faster than using a taylor serie """ @@ -339,10 +340,10 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) n = self._data_point_nb - self._last_checkpoint if n >= _STORE_FREQ / _FREQ: f = open(self.filep, "ab") - f.write("{:7f},{},{:7f},{:7f},{:7f}\n".format( + f.write("{:7f},{},{:7f},{:7f},{:7f},{}\n".format( math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # estimated arm angle int(wasp.watch.rtc.time() - self._offset), - buff[0], buff[1], buff[2] + buff[0], buff[1], buff[2], wasp.watch.battery.level() ).encode()) f.close() del f From da47d58cfe145b078488d716b7004b488a108c44 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 20:19:24 +0100 Subject: [PATCH 303/485] fix: forgot to reset buff to -16000 --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 79e5282..3b63266 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -347,9 +347,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) ).encode()) f.close() del f - buff[0] = 0 # resets x/y/z to 0 - buff[1] = 0 - buff[2] = 0 + buff[0] = -16000 # resets x/y/z to 0 + buff[1] = -16000 + buff[2] = -16000 self._last_checkpoint = self._data_point_nb wasp.gc.collect() From 1547a053ba5449a9017074e37a66e2a642d6f885 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 20:20:07 +0100 Subject: [PATCH 304/485] more frequent data storing --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 3b63266..ab60ee6 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -28,7 +28,7 @@ _SETTINGS2 = const(4) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_STORE_FREQ = const(300) # process data and store to file every X seconds +_STORE_FREQ = const(30) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(10) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: From e95b99d89d25771608715912a2fd225a856dc6aa Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 15 Mar 2022 20:30:03 +0100 Subject: [PATCH 305/485] fix: initialize accel buff to minimum value --- SleepTk.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index ab60ee6..7d3eec8 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -29,6 +29,7 @@ _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(30) # process data and store to file every X seconds +_MIN_ACCEL = const(-17000) # minimum value of one accelerator axis _BATTERY_THRESHOLD = const(10) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: @@ -54,7 +55,7 @@ class SleepTkApp(): self._conf_view = _OFF # confirmation view self._earlier = 0 # number of seconds between the alarm you set manually and the smart alarm time self._old_notification_level = wasp.system.notify_level - self._buff = array("f", [-16000, -16000, -16000]) + self._buff = array("f", [_MIN_ACCEL, _MIN_ACCEL, _MIN_ACCEL]) try: shell.mkdir("logs/") @@ -347,9 +348,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) ).encode()) f.close() del f - buff[0] = -16000 # resets x/y/z to 0 - buff[1] = -16000 - buff[2] = -16000 + buff[0] = _MIN_ACCEL # resets x/y/z to 0 + buff[1] = _MIN_ACCEL + buff[2] = _MIN_ACCEL self._last_checkpoint = self._data_point_nb wasp.gc.collect() From e641995d7b2e792cf934d1f414f317415b542758 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 16 Mar 2022 16:34:13 +0100 Subject: [PATCH 306/485] handle exception while pulling sleep data --- pull_latest_sleep_data.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pull_latest_sleep_data.py b/pull_latest_sleep_data.py index f115277..cab5bb6 100644 --- a/pull_latest_sleep_data.py +++ b/pull_latest_sleep_data.py @@ -34,9 +34,11 @@ for fi in to_dl: print(f"Downloading file '{fi}'") pull_cmd = f'./tools/wasptool --verbose --pull logs/sleep/{fi}' try: - subprocess.check_output(shlex.split(pull_cmd)) + out = subprocess.check_output(shlex.split(pull_cmd)) + if b"Watch reported error" in out: + raise Exception("Watch reported error") print(f"Succesfully downloaded to './logs/sleep/{fi}'") except Exception as e: - print(f"Error happenned when donloading {fi}, deleting file") + print(f"Error happened while downloading {fi}, deleting local incomplete file") os.system(f"rm ./logs/sleep/{fi}") print("\n\n") From cf5a42589de371c264048e6e14511a82dd351d1c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 16 Mar 2022 16:55:15 +0100 Subject: [PATCH 307/485] no need to store the battery now --- SleepTk.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 7d3eec8..d032a75 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -328,12 +328,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() def _periodicSave(self): - """save data after maxpooling over a window to file + """save data after averageing over a window to file row order in the csv: 1. arm angle 2. elapsed times - 3/4/5. x/y/z max value over _STORE_FREQ seconds - 6. battery level + 3/4/5. x/y/z average value over _STORE_FREQ seconds arm angle formula from https://www.nature.com/articles/s41598-018-31266-z note: math.atan() is faster than using a taylor serie """ @@ -341,10 +340,10 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) n = self._data_point_nb - self._last_checkpoint if n >= _STORE_FREQ / _FREQ: f = open(self.filep, "ab") - f.write("{:7f},{},{:7f},{:7f},{:7f},{}\n".format( + f.write("{:7f},{},{:7f},{:7f},{:7f}\n".format( math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # estimated arm angle int(wasp.watch.rtc.time() - self._offset), - buff[0], buff[1], buff[2], wasp.watch.battery.level() + buff[0], buff[1], buff[2] ).encode()) f.close() del f From c58199fbd205e9489968b452de6b62d061dee536 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 16 Mar 2022 16:55:33 +0100 Subject: [PATCH 308/485] store average instead of maxpooling --- SleepTk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index d032a75..d0437c8 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -288,15 +288,15 @@ class SleepTkApp(): def _trackOnce(self): """get one data point of accelerometer every _FREQ seconds, keep - the maximum over each axis then store in a file every + the rolling average of each axis then store in a file every _STORE_FREQ seconds""" try: if self._is_tracking: buff = self._buff xyz = wasp.watch.accel.read_xyz() - buff[0] = max(buff[0], xyz[0]) - buff[1] = max(buff[1], xyz[1]) - buff[2] = max(buff[2], xyz[2]) + buff[0] = (buff[0] + xyz[0]) / 2 + buff[1] = (buff[1] + xyz[1]) / 2 + buff[2] = (buff[2] + xyz[2]) / 2 self._data_point_nb += 1 # add alarm to log accel data in _FREQ seconds From 730fcf47e7d6a645d7c598bdd5be59959d031940 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 17 Mar 2022 07:34:33 +0100 Subject: [PATCH 309/485] todo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 78f059f..621ccab 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ ## TODO **misc** * log heart rate data every X minutes -* investigate if downsampling is necessary +* implement downsampling * if self.foreground is called, record the time. Use it to cancel smart alarm if you woke up too many times (more than 2 times in more than 20 minutes apart). * add a "nap tracking" mode that records sleep tracking with more precision * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops From 72d63cd03f415353e559848f76e6133e5df2befd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 18 Mar 2022 09:44:48 +0100 Subject: [PATCH 310/485] new battery threshold : 25% --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index d0437c8..bfa706d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -30,7 +30,7 @@ _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have t _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(30) # process data and store to file every X seconds _MIN_ACCEL = const(-17000) # minimum value of one accelerator axis -_BATTERY_THRESHOLD = const(10) # under X% of battery, stop tracking and only keep the alarm +_BATTERY_THRESHOLD = const(25) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set From 22ce8f28bd2a219881b829577403dbd0e64f9ecd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 18 Mar 2022 10:16:41 +0100 Subject: [PATCH 311/485] store average arm angle instead of max value etc --- SleepTk.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index bfa706d..e348621 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -29,7 +29,6 @@ _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(30) # process data and store to file every X seconds -_MIN_ACCEL = const(-17000) # minimum value of one accelerator axis _BATTERY_THRESHOLD = const(25) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: @@ -55,7 +54,7 @@ class SleepTkApp(): self._conf_view = _OFF # confirmation view self._earlier = 0 # number of seconds between the alarm you set manually and the smart alarm time self._old_notification_level = wasp.system.notify_level - self._buff = array("f", [_MIN_ACCEL, _MIN_ACCEL, _MIN_ACCEL]) + self._buff = array("f", [_OFF, _OFF, _OFF]) try: shell.mkdir("logs/") @@ -288,15 +287,15 @@ class SleepTkApp(): def _trackOnce(self): """get one data point of accelerometer every _FREQ seconds, keep - the rolling average of each axis then store in a file every + the average of each axis then store in a file every _STORE_FREQ seconds""" try: if self._is_tracking: buff = self._buff xyz = wasp.watch.accel.read_xyz() - buff[0] = (buff[0] + xyz[0]) / 2 - buff[1] = (buff[1] + xyz[1]) / 2 - buff[2] = (buff[2] + xyz[2]) / 2 + buff[0] += xyz[0] + buff[1] += xyz[1] + buff[2] += xyz[2] self._data_point_nb += 1 # add alarm to log accel data in _FREQ seconds @@ -328,9 +327,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() def _periodicSave(self): - """save data after averageing over a window to file - row order in the csv: - 1. arm angle + """save data to csv with row order: + 1. average arm angle 2. elapsed times 3/4/5. x/y/z average value over _STORE_FREQ seconds arm angle formula from https://www.nature.com/articles/s41598-018-31266-z @@ -339,6 +337,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff = self._buff n = self._data_point_nb - self._last_checkpoint if n >= _STORE_FREQ / _FREQ: + buff[0] /= n + buff[1] /= n + buff[2] /= n f = open(self.filep, "ab") f.write("{:7f},{},{:7f},{:7f},{:7f}\n".format( math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # estimated arm angle @@ -347,9 +348,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) ).encode()) f.close() del f - buff[0] = _MIN_ACCEL # resets x/y/z to 0 - buff[1] = _MIN_ACCEL - buff[2] = _MIN_ACCEL + buff[0] = 0 # resets x/y/z to 0 + buff[1] = 0 + buff[2] = 0 self._last_checkpoint = self._data_point_nb wasp.gc.collect() From b85f16ea75eb0655ca89ab0adb608117d09d9a16 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 18 Mar 2022 10:16:47 +0100 Subject: [PATCH 312/485] todo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 621ccab..5f8d36f 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ ## TODO **misc** * log heart rate data every X minutes -* implement downsampling +* implement downsampling to 15 minutes precision to compute best wake up time * if self.foreground is called, record the time. Use it to cancel smart alarm if you woke up too many times (more than 2 times in more than 20 minutes apart). * add a "nap tracking" mode that records sleep tracking with more precision * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops From 5bdb4c5ac5a088db45bef178121793676fb3528d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 18 Mar 2022 10:16:54 +0100 Subject: [PATCH 313/485] less frequent data loggin --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index e348621..9bbcb68 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -27,8 +27,8 @@ _SETTINGS1 = const(3) _SETTINGS2 = const(4) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_STORE_FREQ = const(30) # process data and store to file every X seconds +_FREQ = const(60) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_STORE_FREQ = const(300) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(25) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: From 68cb0188f643e545eeaf637335ea8dee17a984e0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 18 Mar 2022 10:23:46 +0100 Subject: [PATCH 314/485] fix: minor --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 9bbcb68..b56212d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -309,7 +309,7 @@ class SleepTkApp(): self._smart_alarm_state = _OFF h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", - "title": "Bat <20%", + "title": "Bat low", "body": "Stopped \ tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ on.".format(h, m, _BATTERY_THRESHOLD)}) From 239fcf3b3ec06c4a11d4028ff5421a7a948ee668 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 20 Mar 2022 16:07:34 +0100 Subject: [PATCH 315/485] stop storing x/y/z values --- SleepTk.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index b56212d..ae11fd4 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -330,7 +330,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """save data to csv with row order: 1. average arm angle 2. elapsed times - 3/4/5. x/y/z average value over _STORE_FREQ seconds arm angle formula from https://www.nature.com/articles/s41598-018-31266-z note: math.atan() is faster than using a taylor serie """ @@ -344,7 +343,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f.write("{:7f},{},{:7f},{:7f},{:7f}\n".format( math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # estimated arm angle int(wasp.watch.rtc.time() - self._offset), - buff[0], buff[1], buff[2] ).encode()) f.close() del f From 302be10fe61b587d40738fadbf500eb46bd0648c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Mar 2022 12:50:36 +0100 Subject: [PATCH 316/485] fix: error when catching exception --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index ae11fd4..e57b3ac 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -322,7 +322,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "trackOnce", - "body": "Exception at {}h{}m : {]".format(h, m, e)}) + "body": "Exception at {}h{}m : {}".format(h, m, str(e))}) finally: wasp.gc.collect() @@ -518,8 +518,8 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) }) except Exception as e: wasp.gc.collect() - t = wasp.watch.time.localtime(wasp.watch.rtc.time()) - msg = "Exception occured at {:02d}h{:02d}m: '{}'%".format(t[3], t[4], str(e)) + h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] + msg = "Exception occured at {:02d}h{:02d}m: '{}'%".format(h, m, str(e)) wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Smart alarm error", "body": msg}) From b5b70829b077754f15091d59ac421d64c150aae5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Mar 2022 13:03:17 +0100 Subject: [PATCH 317/485] better exception code --- SleepTk.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index e57b3ac..cc60e67 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -315,14 +315,17 @@ tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ on.".format(h, m, _BATTERY_THRESHOLD)}) except Exception as e: mute = wasp.watch.display.mute - mute(False) wasp.system.wake() mute(False) h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] + msg = "Exception at {}h{}m : {}".format(h, m, str(e)) + f = open("smart_alarm_error_{}.txt".format(int(wasp.watch.rtc.time())), "wb") + f.write(msg.encode()) + f.close() wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "trackOnce", - "body": "Exception at {}h{}m : {}".format(h, m, str(e))}) + "body": msg}) finally: wasp.gc.collect() @@ -520,12 +523,12 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) wasp.gc.collect() h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] msg = "Exception occured at {:02d}h{:02d}m: '{}'%".format(h, m, str(e)) - wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", - "title": "Smart alarm error", - "body": msg}) f = open("smart_alarm_error_{}.txt".format(int(wasp.watch.rtc.time())), "wb") f.write(msg.encode()) f.close() + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", + "title": "Smart alarm error", + "body": msg}) wasp.gc.collect() @@ -553,6 +556,5 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) mute = wasp.watch.display.mute mute(True) wasp.system.wake() - mute(True) wasp.system.switch(self) wasp.watch.vibrator.pulse(duty=60, ms=100) From 566705f471489c8bdc12cfacfaf0a8f7c36b3b62 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Mar 2022 13:30:39 +0100 Subject: [PATCH 318/485] fix: minor --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index cc60e67..00352cb 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -343,7 +343,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff[1] /= n buff[2] /= n f = open(self.filep, "ab") - f.write("{:7f},{},{:7f},{:7f},{:7f}\n".format( + f.write("{:7f},{}\n".format( math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # estimated arm angle int(wasp.watch.rtc.time() - self._offset), ).encode()) From c2e3ebeb010869a76e413162118c4034163dcee8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Mar 2022 14:59:11 +0100 Subject: [PATCH 319/485] fix: avoid division by 0 --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 00352cb..3602c1d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -338,13 +338,13 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """ buff = self._buff n = self._data_point_nb - self._last_checkpoint - if n >= _STORE_FREQ / _FREQ: + if n >= _STORE_FREQ // _FREQ and sum(buff[0:2]) != 0: buff[0] /= n buff[1] /= n buff[2] /= n f = open(self.filep, "ab") f.write("{:7f},{}\n".format( - math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # estimated arm angle + math.atan(buff[2] / (buff[0]**2 + buff[1]**2 + 0.000000001)), # estimated arm angle int(wasp.watch.rtc.time() - self._offset), ).encode()) f.close() From 5a43017bddb1b96fba9eeabaa4f4335b658e5049 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Mar 2022 15:03:28 +0100 Subject: [PATCH 320/485] remove exception catcher in trackOnce --- SleepTk.py | 55 ++++++++++++++++++++---------------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 3602c1d..6ab4d8d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -289,45 +289,30 @@ class SleepTkApp(): """get one data point of accelerometer every _FREQ seconds, keep the average of each axis then store in a file every _STORE_FREQ seconds""" - try: - if self._is_tracking: - buff = self._buff - xyz = wasp.watch.accel.read_xyz() - buff[0] += xyz[0] - buff[1] += xyz[1] - buff[2] += xyz[2] - self._data_point_nb += 1 + if self._is_tracking: + buff = self._buff + xyz = wasp.watch.accel.read_xyz() + buff[0] += xyz[0] + buff[1] += xyz[1] + buff[2] += xyz[2] + self._data_point_nb += 1 - # add alarm to log accel data in _FREQ seconds - self.next_al = wasp.watch.rtc.time() + _FREQ - wasp.system.set_alarm(self.next_al, self._trackOnce) + # add alarm to log accel data in _FREQ seconds + self.next_al = wasp.watch.rtc.time() + _FREQ + wasp.system.set_alarm(self.next_al, self._trackOnce) - self._periodicSave() - if wasp.watch.battery.level() <= _BATTERY_THRESHOLD: - # strop tracking if battery low - self._disable_tracking(keep_main_alarm=True) - self._smart_alarm_state = _OFF - h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] - wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", - "title": "Bat low", - "body": "Stopped \ + self._periodicSave() + if wasp.watch.battery.level() <= _BATTERY_THRESHOLD: + # strop tracking if battery low + self._disable_tracking(keep_main_alarm=True) + self._smart_alarm_state = _OFF + h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", + "title": "Bat low", + "body": "Stopped \ tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ on.".format(h, m, _BATTERY_THRESHOLD)}) - except Exception as e: - mute = wasp.watch.display.mute - wasp.system.wake() - mute(False) - h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] - msg = "Exception at {}h{}m : {}".format(h, m, str(e)) - f = open("smart_alarm_error_{}.txt".format(int(wasp.watch.rtc.time())), "wb") - f.write(msg.encode()) - f.close() - wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), - {"src": "SleepTk", - "title": "trackOnce", - "body": msg}) - finally: - wasp.gc.collect() + wasp.gc.collect() def _periodicSave(self): """save data to csv with row order: From bd5803985c97ebd8a8f3316da67d18ced647daa1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 21 Mar 2022 15:26:21 +0100 Subject: [PATCH 321/485] fix: reset accelerometer if invalid value + no need to check for 0 division anymore --- SleepTk.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 6ab4d8d..c9c64a3 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -292,6 +292,9 @@ class SleepTkApp(): if self._is_tracking: buff = self._buff xyz = wasp.watch.accel.read_xyz() + if xyz == (0, 0, 0): + wasp.watch.accel.reset() + xyz = wasp.watch.accel.read_xyz() buff[0] += xyz[0] buff[1] += xyz[1] buff[2] += xyz[2] @@ -323,13 +326,13 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """ buff = self._buff n = self._data_point_nb - self._last_checkpoint - if n >= _STORE_FREQ // _FREQ and sum(buff[0:2]) != 0: + if n >= _STORE_FREQ // _FREQ: buff[0] /= n buff[1] /= n buff[2] /= n f = open(self.filep, "ab") f.write("{:7f},{}\n".format( - math.atan(buff[2] / (buff[0]**2 + buff[1]**2 + 0.000000001)), # estimated arm angle + math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # estimated arm angle int(wasp.watch.rtc.time() - self._offset), ).encode()) f.close() @@ -340,7 +343,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._last_checkpoint = self._data_point_nb wasp.gc.collect() - def _signal_processing(self, data): """signal processing over the data read from the local file""" From 715856618fb79cf6f2d711e5f84ff94ef0188078 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 22 Mar 2022 11:39:13 +0100 Subject: [PATCH 322/485] fix: cancel tiny vibration alarm when disabling tracking --- SleepTk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index c9c64a3..db08850 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -280,6 +280,8 @@ class SleepTkApp(): if self._alarm_state: if keep_main_alarm is False: # to keep the alarm when stopping because of low battery wasp.system.cancel_alarm(self._WU_t, self._listen_to_ticks) + for t in _GRADUAL_WAKE: + wasp.system.cancel_alarm(self._WU_t - t*60, self._tiny_vibration) if self._smart_alarm_state: wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_compute) self._periodicSave() From b32040cdd812391d09dc8740422308ff160b4931 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 22 Mar 2022 11:49:12 +0100 Subject: [PATCH 323/485] store arm angle instead of raw atan value --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index db08850..751e302 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -334,7 +334,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff[2] /= n f = open(self.filep, "ab") f.write("{:7f},{}\n".format( - math.atan(buff[2] / (buff[0]**2 + buff[1]**2)), # estimated arm angle + math.atan(buff[2] / (buff[0]**2 + buff[1]**2))*180/3.1415926535, # estimated arm angle int(wasp.watch.rtc.time() - self._offset), ).encode()) f.close() From fac9ce242eff440b44623df374b567f897c3badb Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 22 Mar 2022 18:32:29 +0100 Subject: [PATCH 324/485] todo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f8d36f..fbdc9bd 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,11 @@ ## TODO **misc** +* add a "nap tracking" mode that records sleep tracking with more precision + * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops * log heart rate data every X minutes * implement downsampling to 15 minutes precision to compute best wake up time * if self.foreground is called, record the time. Use it to cancel smart alarm if you woke up too many times (more than 2 times in more than 20 minutes apart). -* add a "nap tracking" mode that records sleep tracking with more precision - * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops * log smart alarm data to file? log user rating of how well he/she felt fresh at wake? * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation? From 407258087d7c90ff5baa3f58c5e0f78d4fa67d9d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 22 Mar 2022 18:33:16 +0100 Subject: [PATCH 325/485] remove link to algorithm from the bibliography --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index fbdc9bd..2878525 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,6 @@ ## Related links: * article with detailed implementation : https://www.nature.com/articles/s41598-018-31266-z * very interesting research paper on the topic : https://academic.oup.com/sleep/article/42/12/zsz180/5549536 -* maybe coding a 1D convolution is a good way to extract peaks -* list of ways to find local maxima in python : https://blog.finxter.com/how-to-find-local-minima-in-1d-and-2d-numpy-arrays/ + https://pythonawesome.com/overview-of-the-peaks-dectection-algorithms-available-in-python/ * interesting study : https://ieeexplore.ieee.org/document/7052479 ### Related project: From fd313b906ce1a85e8d4870a4a8bcb09336419ea3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 22 Mar 2022 18:33:24 +0100 Subject: [PATCH 326/485] refactored bibliogrpahy --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2878525..7affc1f 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,11 @@ * log smart alarm data to file? log user rating of how well he/she felt fresh at wake? * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation? -## Related links: -* article with detailed implementation : https://www.nature.com/articles/s41598-018-31266-z -* very interesting research paper on the topic : https://academic.oup.com/sleep/article/42/12/zsz180/5549536 -* interesting study : https://ieeexplore.ieee.org/document/7052479 +## Bibliography and related links: +* [Estimating sleep parameters using an accelerometer without sleep diary](https://www.nature.com/articles/s41598-018-31266-z) +* [Sleep stage prediction with raw acceleration and photoplethysmography heart rate data derived from a consumer wearable device ](https://academic.oup.com/sleep/article/42/12/zsz180/5549536) +* [Sleep stage prediction with raw acceleration and photoplethysmography heart rate data derived from a consumer wearable device ](https://ieeexplore.ieee.org/document/7052479) +* [Sleep classification from wrist-worn accelerometer data using random forests](https://pubmed.ncbi.nlm.nih.gov/33420133/) ### Related project: * another hackable smartwatch has a similar software: [sleepphasealarm](https://banglejs.com/apps/#sleepphasealarm) and [steelball](https://github.com/jabituyaben/SteelBall) for the [Banglejs](https://banglejs.com/) From 753034493fa7a73c56a887a745fabdfce560c31a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 22 Mar 2022 18:53:17 +0100 Subject: [PATCH 327/485] added more bibliography --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7affc1f..6555e6d 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,18 @@ ## Bibliography and related links: * [Estimating sleep parameters using an accelerometer without sleep diary](https://www.nature.com/articles/s41598-018-31266-z) -* [Sleep stage prediction with raw acceleration and photoplethysmography heart rate data derived from a consumer wearable device ](https://academic.oup.com/sleep/article/42/12/zsz180/5549536) -* [Sleep stage prediction with raw acceleration and photoplethysmography heart rate data derived from a consumer wearable device ](https://ieeexplore.ieee.org/document/7052479) +* [Sleep stage prediction with raw acceleration and photoplethysmography heart rate data derived from a consumer wearable device](https://academic.oup.com/sleep/article/42/12/zsz180/5549536) +* [Towards Benchmarked Sleep Detection with Wrist-Worn Sensing Units](https://ieeexplore.ieee.org/document/7052479) + * [Sleep classification from wrist-worn accelerometer data using random forests](https://pubmed.ncbi.nlm.nih.gov/33420133/) +* [Sleep Monitoring Based on a Tri-Axial Accelerometer and a Pressure Sensor](https://www.mdpi.com/1424-8220/16/5/750) +* [A Sleep Monitoring Application for u-lifecare Using Accelerometer Sensor of Smartphone](https://link.springer.com/chapter/10.1007/978-3-319-03176-7_20) +* [The Promise of Sleep: A Multi-Sensor Approach for Accurate Sleep Stage Detection Using the Oura Ring](https://www.mdpi.com/1424-8220/21/13/4302) +* [Validation of an Accelerometer Based BCG Method for Sleep Analysis](https://aaltodoc.aalto.fi/handle/123456789/21176) +* [Accelerometer-based sleep analysis](https://patents.google.com/patent/US20140364770A1/en) +* [Performance comparison between wrist and chest actigraphy in combination with heart rate variability for sleep classification](https://www.sciencedirect.com/science/article/pii/S0010482517302597) +* [Estimation of sleep stages in a healthy adult population from optical plethysmography and accelerometer signals](https://iopscience.iop.org/article/10.1088/1361-6579/aa9047/meta) +* [SleepPy: A python package for sleep analysis from accelerometer data](https://joss.theoj.org/papers/10.21105/joss.01663.pdf) ### Related project: * another hackable smartwatch has a similar software: [sleepphasealarm](https://banglejs.com/apps/#sleepphasealarm) and [steelball](https://github.com/jabituyaben/SteelBall) for the [Banglejs](https://banglejs.com/) From a32875e40d16944afdd5c13e032b5280241090e4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 23 Mar 2022 09:41:50 +0100 Subject: [PATCH 328/485] change defaults gradual wake intervals and cycle length --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 751e302..dd0ada2 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -33,9 +33,9 @@ _BATTERY_THRESHOLD = const(25) # under X% of battery, stop tracking and only ke # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set -_GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 8, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up +_GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up _TIME_TO_FALL_ASLEEP = const(14) # in minutes, according to https://sleepyti.me/ -_CYCLE_LENGTH = const(90) # in minutes, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! +_CYCLE_LENGTH = const(100) # in minutes, default of 90 or 100, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! class SleepTkApp(): From 1ca76cbfa60ad42b123e52643c9b1e2edc995ee9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Mar 2022 11:56:52 +0100 Subject: [PATCH 329/485] minor --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index dd0ada2..915ba51 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -284,6 +284,7 @@ class SleepTkApp(): wasp.system.cancel_alarm(self._WU_t - t*60, self._tiny_vibration) if self._smart_alarm_state: wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_compute) + self._smart_alarm_state = _OFF self._periodicSave() wasp.gc.collect() @@ -310,7 +311,6 @@ class SleepTkApp(): if wasp.watch.battery.level() <= _BATTERY_THRESHOLD: # strop tracking if battery low self._disable_tracking(keep_main_alarm=True) - self._smart_alarm_state = _OFF h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", "title": "Bat low", From c8bb5ccf6c71ccf3de423aa4b6cd6ff97ac0ac1b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 25 Mar 2022 11:57:40 +0100 Subject: [PATCH 330/485] testing: disable battery threshold --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 915ba51..719bd70 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -29,7 +29,7 @@ _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(60) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(300) # process data and store to file every X seconds -_BATTERY_THRESHOLD = const(25) # under X% of battery, stop tracking and only keep the alarm +_BATTERY_THRESHOLD = const(-500) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set From 6438853ef36c78f2885c38afe03776f804c1593d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 27 Mar 2022 13:13:30 +0200 Subject: [PATCH 331/485] todo --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6555e6d..e68a4f0 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ ## TODO **misc** +* investigate adding snooze feature like in the original `alarms.py` app +* investigate adding a simple feature to wake you up only after a certain movement threshold was passed * add a "nap tracking" mode that records sleep tracking with more precision * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops * log heart rate data every X minutes From bc63cda21ff1dfdd6bf1370d47c207ac42a6472e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 29 Mar 2022 15:03:08 +0200 Subject: [PATCH 332/485] fix: time to fall asleep was counted in seconds instead of minutes --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 719bd70..68b9e0d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -124,7 +124,7 @@ class SleepTkApp(): if self._alarm_state: draw = wasp.watch.drawable draw.set_font(_FONT) - duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP) // 60 + duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP * 60) // 60 duration = max(duration, 0) # if alarm too close draw.string("Total sleep {:02d}h{:02d}m".format( int(duration // 60), From f3bb674de638e7f5269258aebeedbdd1ced17bd0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 29 Mar 2022 15:03:24 +0200 Subject: [PATCH 333/485] change default cycle length to 90 minutes --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 68b9e0d..b2ed42f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -35,7 +35,7 @@ _BATTERY_THRESHOLD = const(-500) # under X% of battery, stop tracking and only _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up _TIME_TO_FALL_ASLEEP = const(14) # in minutes, according to https://sleepyti.me/ -_CYCLE_LENGTH = const(100) # in minutes, default of 90 or 100, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! +_CYCLE_LENGTH = const(90) # in minutes, default of 90 or 100, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! class SleepTkApp(): From a81b310374d4f87a3cf3852b2ecec48b14a1fc35 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 29 Mar 2022 15:03:33 +0200 Subject: [PATCH 334/485] minor: style --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index b2ed42f..f7894d1 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -129,9 +129,9 @@ class SleepTkApp(): draw.string("Total sleep {:02d}h{:02d}m".format( int(duration // 60), int(duration % 60),), 0, 180) - cycl = (duration) / _CYCLE_LENGTH - draw.string("{} cycles ".format(str(cycl)[0:5]), 0, 200) - cycl_modulo = cycl-int(cycl) + cycl = duration / _CYCLE_LENGTH + draw.string("{} cycles ".format(str(cycl)[0:4]), 0, 200) + cycl_modulo = cycl - int(cycl) if cycl_modulo > 0.10 and cycl_modulo < 0.90: draw.string("Not rested!", 0, 220) else: From c7b542f629884fea76f7c123e00ced2b18e4ec7d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 29 Mar 2022 15:05:09 +0200 Subject: [PATCH 335/485] more frequent data logging --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index f7894d1..a240960 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -27,8 +27,8 @@ _SETTINGS1 = const(3) _SETTINGS2 = const(4) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = const(60) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_STORE_FREQ = const(300) # process data and store to file every X seconds +_FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_STORE_FREQ = const(120) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(-500) # under X% of battery, stop tracking and only keep the alarm # user might want to edit this: From 6f7f10fe8d4e989e476c6255b789490b68638315 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 29 Mar 2022 15:05:29 +0200 Subject: [PATCH 336/485] add explanation on how to disable battery threshold --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index a240960..b412662 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -29,7 +29,7 @@ _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(120) # process data and store to file every X seconds -_BATTERY_THRESHOLD = const(-500) # under X% of battery, stop tracking and only keep the alarm +_BATTERY_THRESHOLD = const(-200) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set From b7492f3ee6161d707bab82d8e960154f3a7a296a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 29 Mar 2022 15:05:51 +0200 Subject: [PATCH 337/485] reactivate battery threshold --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index b412662..45e4f4a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -29,7 +29,7 @@ _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _STORE_FREQ = const(120) # process data and store to file every X seconds -_BATTERY_THRESHOLD = const(-200) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable +_BATTERY_THRESHOLD = const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable # user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set From c385c7b4d61240d4b03cad0be917b6d775cf1f85 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 6 Apr 2022 17:27:20 +0200 Subject: [PATCH 338/485] feat: tracking heart rate data --- README.md | 1 - SleepTk.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e68a4f0..aacf340 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,6 @@ * investigate adding a simple feature to wake you up only after a certain movement threshold was passed * add a "nap tracking" mode that records sleep tracking with more precision * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops -* log heart rate data every X minutes * implement downsampling to 15 minutes precision to compute best wake up time * if self.foreground is called, record the time. Use it to cancel smart alarm if you woke up too many times (more than 2 times in more than 20 minutes apart). diff --git a/SleepTk.py b/SleepTk.py index 45e4f4a..332de29 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -14,6 +14,7 @@ import widgets import shell import fonts import math +import ppg from array import array from micropython import const @@ -28,6 +29,7 @@ _SETTINGS2 = const(4) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_HR_FREQ = const(1800) # how many seconds between heart rate data _STORE_FREQ = const(120) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable @@ -47,6 +49,10 @@ class SleepTkApp(): self._alarm_state = _ON self._grad_alarm_state = _ON self._smart_alarm_state = _OFF # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART + self._track_HR_state = _ON + self._last_HR = _OFF + self._last_HR_date = _OFF + self._track_HR_once = _OFF self._spinval_H = 7 # default wake up time self._spinval_M = 30 self._page = _START @@ -74,6 +80,9 @@ class SleepTkApp(): wasp.EventMask.BUTTON) def background(self): + wasp.watch.hrs.disable() + self._track_HR_once = _OFF + self._hrdata = None wasp.gc.collect() def press(self, button, state): @@ -152,6 +161,10 @@ class SleepTkApp(): return if self.btn_sta.touch(event): self._start_tracking() + elif self.btn_HR.touch(event): + self.btn_HR.draw() + self._track_HR_state = self.btn_HR.state + return self._draw() def _draw(self): @@ -202,10 +215,10 @@ class SleepTkApp(): self._spin_M.draw() elif self._page == _SETTINGS2: if self._alarm_state: - self.check_grad = widgets.Checkbox(0, 40, "Gradual wake") + self.check_grad = widgets.Checkbox(0, 80, "Gradual wake") self.check_grad.state = self._grad_alarm_state self.check_grad.draw() - self.check_smart = widgets.Checkbox(x=0, y=80, label="Smart alarm (alpha)") + self.check_smart = widgets.Checkbox(x=0, y=120, label="Smart alarm (alpha)") self.check_smart.state = self._smart_alarm_state self.check_smart.draw() else: @@ -215,6 +228,9 @@ class SleepTkApp(): for i in range(len(chunks)-1): sub = label[chunks[i]:chunks[i+1]].rstrip() draw.string(sub, 0, 50 + 24 * i) + self.btn_HR = widgets.Checkbox(x=0, y=40, label="Heart rate tracking") + self.btn_HR.state = self._track_HR_state + self.btn_HR.draw() self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start tracking") self.btn_sta.draw() self.stat_bar = widgets.StatusBar() @@ -285,6 +301,8 @@ class SleepTkApp(): if self._smart_alarm_state: wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_compute) self._smart_alarm_state = _OFF + self._track_HR_state = _OFF + wasp.watch.hrs.disable() self._periodicSave() wasp.gc.collect() @@ -317,12 +335,20 @@ class SleepTkApp(): "body": "Stopped \ tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ on.".format(h, m, _BATTERY_THRESHOLD)}) + elif self._track_HR_state: + if wasp.watch.rtc.time() - self._last_HR_date > _HR_FREQ and not self._track_HR_once: + wasp.watch.hrs.enable() + self._hrdata = ppg.PPG(wasp.watch.hrs.read_hrs()) + self._track_HR_once = _ON + wasp.system.request_tick(1000 // 8) + wasp.gc.collect() def _periodicSave(self): """save data to csv with row order: 1. average arm angle 2. elapsed times + 3. heart rate if present arm angle formula from https://www.nature.com/articles/s41598-018-31266-z note: math.atan() is faster than using a taylor serie """ @@ -332,10 +358,16 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff[0] /= n buff[1] /= n buff[2] /= n + if self._last_HR != _OFF: + bpm = ",{}".format(self._last_HR) + self._last_HR = _OFF + else: + bpm = "" f = open(self.filep, "ab") - f.write("{:7f},{}\n".format( + f.write("{:7f},{}{}\n".format( math.atan(buff[2] / (buff[0]**2 + buff[1]**2))*180/3.1415926535, # estimated arm angle int(wasp.watch.rtc.time() - self._offset), + bpm ).encode()) f.close() del f @@ -534,9 +566,34 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) wasp.system.request_tick(period_ms=1000) def tick(self, ticks): - """vibrate to wake you up""" + """vibrate to wake you up OR track heart rate using code from heart.py""" if self._page == _RINGING: wasp.watch.vibrator.pulse(duty=50, ms=500) + elif self._track_HR_once: + t = wasp.machine.Timer(id=1, period=8000000) + mute = wasp.watch.display.mute + t.start() + self._subtick(1) + wasp.system.keep_awake() + mute(True) + while t.time() < 41666: + pass + self._subtick(1) + while t.time() < 83332: + pass + self._subtick(1) + t.stop() + del t + + if len(self._hrdata.data) >= 240: # 10 seconds passed + self._last_HR = self._hrdata.get_heart_rate() + self._last_HR_date = int(wasp.watch.rtc.time()) + self._track_HR_once = _OFF + wasp.watch.hrs.disable() + + def _subtick(self, ticks): + """track heart rate at 24Hz""" + self._hrdata.preprocess(wasp.watch.hrs.read_hrs()) def _tiny_vibration(self): """vibrate just a tiny bit before waking up, to gradually return From e4cc325dfc5da1c4457f40e516e96d6c5c5eca63 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 6 Apr 2022 18:48:52 +0200 Subject: [PATCH 339/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index aacf340..54d5e93 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ **misc** * investigate adding snooze feature like in the original `alarms.py` app * investigate adding a simple feature to wake you up only after a certain movement threshold was passed +* investigate using micropython decorator to compute best wake up time * add a "nap tracking" mode that records sleep tracking with more precision * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops * implement downsampling to 15 minutes precision to compute best wake up time From 514b52298f731fcb6a9884f0198e38bc186a6c77 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 6 Apr 2022 19:59:59 +0200 Subject: [PATCH 340/485] docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 54d5e93..7c66081 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ * [Sleep stage prediction with raw acceleration and photoplethysmography heart rate data derived from a consumer wearable device](https://academic.oup.com/sleep/article/42/12/zsz180/5549536) * [Towards Benchmarked Sleep Detection with Wrist-Worn Sensing Units](https://ieeexplore.ieee.org/document/7052479) +### to read : * [Sleep classification from wrist-worn accelerometer data using random forests](https://pubmed.ncbi.nlm.nih.gov/33420133/) * [Sleep Monitoring Based on a Tri-Axial Accelerometer and a Pressure Sensor](https://www.mdpi.com/1424-8220/16/5/750) * [A Sleep Monitoring Application for u-lifecare Using Accelerometer Sensor of Smartphone](https://link.springer.com/chapter/10.1007/978-3-319-03176-7_20) From 575d161caa499aab681a40166d2a55dff4f7c49e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 6 Apr 2022 23:42:00 +0200 Subject: [PATCH 341/485] new: dont turn hr tracking on by default --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 332de29..929779b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -49,7 +49,7 @@ class SleepTkApp(): self._alarm_state = _ON self._grad_alarm_state = _ON self._smart_alarm_state = _OFF # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART - self._track_HR_state = _ON + self._track_HR_state = _OFF self._last_HR = _OFF self._last_HR_date = _OFF self._track_HR_once = _OFF From a9da525e0725d4d18a3b057ef1bc6a02228bbabd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 9 Apr 2022 15:48:58 +0200 Subject: [PATCH 342/485] new: reset acclerometer at startup --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index 929779b..c54ebf8 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -243,6 +243,7 @@ class SleepTkApp(): self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file self._offset = int(wasp.watch.rtc.time()) # makes output more compact + wasp.watch.accel.reset() # create one file per recording session: self.filep = "logs/sleep/{}.csv".format(str(self._offset + _TIMESTAMP)) From 6fc6f01c2e8e98301131f271122f7b06354ee1ce Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 9 Apr 2022 15:49:30 +0200 Subject: [PATCH 343/485] new: reset frequently font and color --- SleepTk.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index c54ebf8..c517c0f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -108,11 +108,13 @@ class SleepTkApp(): def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" wasp.gc.collect() + draw = wasp.watch.drawable if self._page == _TRACKING: if self._conf_view is _OFF: if self.btn_off.touch(event): self._conf_view = widgets.ConfirmationView() self._conf_view.draw("Stop tracking?") + draw.reset() return else: if self._conf_view.touch(event): @@ -120,6 +122,7 @@ class SleepTkApp(): self._disable_tracking() self._page = _START self._conf_view = _OFF + draw.reset() elif self._page == _RINGING: if self.btn_al.touch(event): self._disable_tracking() @@ -131,7 +134,6 @@ class SleepTkApp(): self._spinval_M = self._spin_M.value self._spin_M.update() if self._alarm_state: - draw = wasp.watch.drawable draw.set_font(_FONT) duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP * 60) // 60 duration = max(duration, 0) # if alarm too close @@ -180,6 +182,7 @@ class SleepTkApp(): draw.string(msg, 0, 70) self.btn_al = widgets.Button(x=0, y=70, w=240, h=140, label="WAKE UP") self.btn_al.draw() + draw.reset() elif self._page == _TRACKING: ti = wasp.watch.time.localtime(self._offset) draw.string('Began at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 70) @@ -195,6 +198,7 @@ class SleepTkApp(): draw.string("data points: {} / {}".format(str(self._data_point_nb), str(self._data_point_nb * _FREQ // _STORE_FREQ)), 0, 130) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() + draw.reset() elif self._page == _START: draw.set_font(_FONT) label = 'Sleep tracker with optional wake up alarm, smart alarm up to 40min before, gradual wake up to 15m. Swipe to navigate.' @@ -202,6 +206,7 @@ class SleepTkApp(): for i in range(len(chunks)-1): sub = label[chunks[i]:chunks[i+1]].rstrip() draw.string(sub, 0, 60 + 20 * i) + draw.reset() elif self._page == _SETTINGS1: self.check_al = widgets.Checkbox(x=0, y=40, label="Wake me up") self.check_al.state = self._alarm_state @@ -213,6 +218,7 @@ class SleepTkApp(): self._spin_M = widgets.Spinner(150, 70, 0, 59, 2, 5) self._spin_M.value = self._spinval_M self._spin_M.draw() + draw.reset() elif self._page == _SETTINGS2: if self._alarm_state: self.check_grad = widgets.Checkbox(0, 80, "Gradual wake") @@ -228,11 +234,13 @@ class SleepTkApp(): for i in range(len(chunks)-1): sub = label[chunks[i]:chunks[i+1]].rstrip() draw.string(sub, 0, 50 + 24 * i) + draw.reset() self.btn_HR = widgets.Checkbox(x=0, y=40, label="Heart rate tracking") self.btn_HR.state = self._track_HR_state self.btn_HR.draw() self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start tracking") self.btn_sta.draw() + draw.reset() self.stat_bar = widgets.StatusBar() self.stat_bar.clock = True self.stat_bar.draw() From 6bfc0e2126dd8724e02cbdccf8ec3e485d344c1b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 13 Apr 2022 13:17:58 +0200 Subject: [PATCH 344/485] new: print duration string in a separate function --- SleepTk.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index c517c0f..d54ed21 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -134,20 +134,8 @@ class SleepTkApp(): self._spinval_M = self._spin_M.value self._spin_M.update() if self._alarm_state: - draw.set_font(_FONT) - duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP * 60) // 60 - duration = max(duration, 0) # if alarm too close - draw.string("Total sleep {:02d}h{:02d}m".format( - int(duration // 60), - int(duration % 60),), 0, 180) - cycl = duration / _CYCLE_LENGTH - draw.string("{} cycles ".format(str(cycl)[0:4]), 0, 200) - cycl_modulo = cycl - int(cycl) - if cycl_modulo > 0.10 and cycl_modulo < 0.90: - draw.string("Not rested!", 0, 220) - else: - draw.string("Well rested", 0, 220) - return + self._draw_duration(draw) + return elif self.check_al.touch(event): self._alarm_state = self.check_al.state self.check_al.update() @@ -169,6 +157,21 @@ class SleepTkApp(): return self._draw() + def _draw_duration(self, draw): + draw.set_font(_FONT) + duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP * 60) // 60 + duration = max(duration, 0) # if alarm too close + draw.string("Total sleep {:02d}h{:02d}m".format( + int(duration // 60), + int(duration % 60),), 0, 180) + cycl = duration / _CYCLE_LENGTH + draw.string("{} cycles ".format(str(cycl)[0:4]), 0, 200) + cycl_modulo = cycl % 1 + if cycl_modulo > 0.10 and cycl_modulo < 0.90: + draw.string("Not rested!", 0, 220) + else: + draw.string("Well rested", 0, 220) + def _draw(self): """GUI""" draw = wasp.watch.drawable @@ -218,6 +221,8 @@ class SleepTkApp(): self._spin_M = widgets.Spinner(150, 70, 0, 59, 2, 5) self._spin_M.value = self._spinval_M self._spin_M.draw() + if self._alarm_state: + self._draw_duration(draw) draw.reset() elif self._page == _SETTINGS2: if self._alarm_state: From 8241080c9bf56a3e0a1e58353bc1399bbcfd3cbc Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 13 Apr 2022 13:49:25 +0200 Subject: [PATCH 345/485] minor: more elegant def._read_time() --- SleepTk.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index d54ed21..e804cf2 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -292,15 +292,12 @@ class SleepTkApp(): def _read_time(self, HH, MM): "convert time from spinners to seconds" - now = wasp.watch.rtc.get_localtime() - yyyy = now[0] - mm = now[1] - dd = now[2] + (Y, Mo, d, h, m) = wasp.watch.rtc.get_localtime()[0:5] HH = self._spinval_H MM = self._spinval_M - if HH < now[3] or (HH == now[3] and MM <= now[4]): - dd += 1 - return wasp.watch.time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0)) + if HH < h or (HH == h and MM <= m): + d += 1 + return wasp.watch.time.mktime((Y, Mo, d, HH, MM, 0, 0, 0, 0)) def _disable_tracking(self, keep_main_alarm=False): """called by touching "STOP TRACKING" or when computing best alarm time From c54499f89fe9bbfe1617f096abb5e0d4a56710b0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 13 Apr 2022 13:49:49 +0200 Subject: [PATCH 346/485] minor: fix losing accuracy when printing sleep duration --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index e804cf2..c47e102 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -159,7 +159,7 @@ class SleepTkApp(): def _draw_duration(self, draw): draw.set_font(_FONT) - duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP * 60) // 60 + duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP * 60) / 60 duration = max(duration, 0) # if alarm too close draw.string("Total sleep {:02d}h{:02d}m".format( int(duration // 60), From f61e5b123c64326b7481fa671daba9464417a848 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 13 Apr 2022 15:41:09 +0200 Subject: [PATCH 347/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7c66081..6d795e8 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ ## TODO **misc** +* remove duplicate variables that store spinner values * investigate adding snooze feature like in the original `alarms.py` app * investigate adding a simple feature to wake you up only after a certain movement threshold was passed * investigate using micropython decorator to compute best wake up time From ad79dc7ab0ebcbe7227e486675b4904f114264e5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 13 Apr 2022 15:34:58 +0200 Subject: [PATCH 348/485] new: auto suggest best wake up time --- SleepTk.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index c47e102..35470f8 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -53,8 +53,15 @@ class SleepTkApp(): self._last_HR = _OFF self._last_HR_date = _OFF self._track_HR_once = _OFF - self._spinval_H = 7 # default wake up time - self._spinval_M = 30 + + # suggest wake up time, on the basis of 7h30m of sleep + time to fall asleep + (H, M) = wasp.watch.time.localtime()[3:5] + M += 30 + _TIME_TO_FALL_ASLEEP + while M % 5 != 0: + M += 1 + self._spinval_H = (H + 7) % 24 + (M // 60) + self._spinval_M = M % 60 + self._page = _START self._is_tracking = _OFF self._conf_view = _OFF # confirmation view @@ -159,11 +166,11 @@ class SleepTkApp(): def _draw_duration(self, draw): draw.set_font(_FONT) - duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time() - _TIME_TO_FALL_ASLEEP * 60) / 60 - duration = max(duration, 0) # if alarm too close + duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time()) / 60 - _TIME_TO_FALL_ASLEEP + assert duration >= _TIME_TO_FALL_ASLEEP draw.string("Total sleep {:02d}h{:02d}m".format( int(duration // 60), - int(duration % 60),), 0, 180) + int(duration % 60)), 0, 180) cycl = duration / _CYCLE_LENGTH draw.string("{} cycles ".format(str(cycl)[0:4]), 0, 200) cycl_modulo = cycl % 1 @@ -292,7 +299,7 @@ class SleepTkApp(): def _read_time(self, HH, MM): "convert time from spinners to seconds" - (Y, Mo, d, h, m) = wasp.watch.rtc.get_localtime()[0:5] + (Y, Mo, d, h, m) = wasp.watch.time.localtime()[0:5] HH = self._spinval_H MM = self._spinval_M if HH < h or (HH == h and MM <= m): From 30f2ff42a14f119633cc3e319bca744552984e13 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 13 Apr 2022 15:40:03 +0200 Subject: [PATCH 349/485] new: suggest wake up time at each run instead of first startup --- SleepTk.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 35470f8..78a291b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -53,15 +53,8 @@ class SleepTkApp(): self._last_HR = _OFF self._last_HR_date = _OFF self._track_HR_once = _OFF - - # suggest wake up time, on the basis of 7h30m of sleep + time to fall asleep - (H, M) = wasp.watch.time.localtime()[3:5] - M += 30 + _TIME_TO_FALL_ASLEEP - while M % 5 != 0: - M += 1 - self._spinval_H = (H + 7) % 24 + (M // 60) - self._spinval_M = M % 60 - + self._spinval_H = _OFF + self._spinval_M = _OFF self._page = _START self._is_tracking = _OFF self._conf_view = _OFF # confirmation view @@ -222,6 +215,14 @@ class SleepTkApp(): self.check_al.state = self._alarm_state self.check_al.draw() if self._alarm_state: + if (self._spinval_H, self._spinval_M) == (_OFF, _OFF): + # suggest wake up time, on the basis of 7h30m of sleep + time to fall asleep + (H, M) = wasp.watch.rtc.get_localtime()[3:5] + M += 30 + _TIME_TO_FALL_ASLEEP + while M % 5 != 0: + M += 1 + self._spinval_H = ((H + 7) % 24 + (M // 60)) % 24 + self._spinval_M = M % 60 self._spin_H = widgets.Spinner(30, 70, 0, 23, 2) self._spin_H.value = self._spinval_H self._spin_H.draw() @@ -299,7 +300,7 @@ class SleepTkApp(): def _read_time(self, HH, MM): "convert time from spinners to seconds" - (Y, Mo, d, h, m) = wasp.watch.time.localtime()[0:5] + (Y, Mo, d, h, m) = wasp.watch.rtc.get_localtime()[0:5] HH = self._spinval_H MM = self._spinval_M if HH < h or (HH == h and MM <= m): @@ -322,6 +323,9 @@ class SleepTkApp(): self._track_HR_state = _OFF wasp.watch.hrs.disable() self._periodicSave() + if self._page == _RINGING: # reset values + self._spinval_H = _OFF + self._spinval_M = _OFF wasp.gc.collect() def _trackOnce(self): From 6b860e6e568bb89ca732bf792aca6c500ad49989 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 14 Apr 2022 11:52:23 +0200 Subject: [PATCH 350/485] fix: better way to reset spinval values between runs --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 78a291b..6fa56d5 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -210,6 +210,9 @@ class SleepTkApp(): sub = label[chunks[i]:chunks[i+1]].rstrip() draw.string(sub, 0, 60 + 20 * i) draw.reset() + # reset spinval values between runs + self._spinval_H = _OFF + self._spinval_M = _OFF elif self._page == _SETTINGS1: self.check_al = widgets.Checkbox(x=0, y=40, label="Wake me up") self.check_al.state = self._alarm_state @@ -323,9 +326,6 @@ class SleepTkApp(): self._track_HR_state = _OFF wasp.watch.hrs.disable() self._periodicSave() - if self._page == _RINGING: # reset values - self._spinval_H = _OFF - self._spinval_M = _OFF wasp.gc.collect() def _trackOnce(self): From 11ab117e956a8fedfce623ef7a7ec844e1091a11 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 15 Apr 2022 13:08:05 +0200 Subject: [PATCH 351/485] major: move smart alarm to separate class + use micropython decorators --- README.md | 1 - SleepTk.py | 332 +++++++++++++++++++++++++++-------------------------- 2 files changed, 172 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 6d795e8..1ad3167 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ * remove duplicate variables that store spinner values * investigate adding snooze feature like in the original `alarms.py` app * investigate adding a simple feature to wake you up only after a certain movement threshold was passed -* investigate using micropython decorator to compute best wake up time * add a "nap tracking" mode that records sleep tracking with more precision * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops * implement downsampling to 15 minutes precision to compute best wake up time diff --git a/SleepTk.py b/SleepTk.py index 6fa56d5..776cfc7 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -16,28 +16,28 @@ import fonts import math import ppg from array import array -from micropython import const +import micropython # HARDCODED VARIABLES: -_ON = const(1) -_OFF = const(0) -_TRACKING = const(0) -_RINGING = const(1) -_START = const(2) # page values: -_SETTINGS1 = const(3) -_SETTINGS2 = const(4) +_ON = micropython.const(1) +_OFF = micropython.const(0) +_TRACKING = micropython.const(0) +_RINGING = micropython.const(1) +_START = micropython.const(2) # page values: +_SETTINGS1 = micropython.const(3) +_SETTINGS2 = micropython.const(4) _FONT = fonts.sans18 -_TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_HR_FREQ = const(1800) # how many seconds between heart rate data -_STORE_FREQ = const(120) # process data and store to file every X seconds -_BATTERY_THRESHOLD = const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable +_TIMESTAMP = micropython.const(946684800) # unix time and time used by wasp os don't have the same reference date +_FREQ = micropython.const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_HR_FREQ = micropython.const(1800) # how many seconds between heart rate data +_STORE_FREQ = micropython.const(120) # process data and store to file every X seconds +_BATTERY_THRESHOLD = micropython.const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable # user might want to edit this: -_ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set +_ANTICIPATE_ALLOWED = micropython.const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up -_TIME_TO_FALL_ASLEEP = const(14) # in minutes, according to https://sleepyti.me/ -_CYCLE_LENGTH = const(90) # in minutes, default of 90 or 100, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! +_TIME_TO_FALL_ASLEEP = micropython.const(14) # in minutes, according to https://sleepyti.me/ +_CYCLE_LENGTH = micropython.const(90) # in minutes, default of 90 or 100, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! class SleepTkApp(): @@ -297,7 +297,7 @@ class SleepTkApp(): # wake up SleepTk 2min before earliest possible wake up if self._smart_alarm_state: self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 - wasp.system.set_alarm(self._WU_a, self._smart_alarm_compute) + wasp.system.set_alarm(self._WU_a, self._smart_alarm_start) wasp.system.notify_level = 1 # silent notifications self._page = _TRACKING @@ -321,7 +321,7 @@ class SleepTkApp(): for t in _GRADUAL_WAKE: wasp.system.cancel_alarm(self._WU_t - t*60, self._tiny_vibration) if self._smart_alarm_state: - wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_compute) + wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_start) self._smart_alarm_state = _OFF self._track_HR_state = _OFF wasp.watch.hrs.disable() @@ -366,6 +366,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() + @micropython.native def _periodicSave(self): """save data to csv with row order: 1. average arm angle @@ -399,6 +400,156 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._last_checkpoint = self._data_point_nb wasp.gc.collect() + + def _listen_to_ticks(self): + """listen to ticks every second, telling the watch to vibrate""" + wasp.gc.collect() + wasp.system.notify_level = self._old_notification_level # restore notification level + self._page = _RINGING + mute = wasp.watch.display.mute + mute(True) + wasp.system.wake() + wasp.system.switch(self) + self._draw() + wasp.system.request_tick(period_ms=1000) + + def tick(self, ticks): + """vibrate to wake you up OR track heart rate using code from heart.py""" + if self._page == _RINGING: + wasp.watch.vibrator.pulse(duty=50, ms=500) + elif self._track_HR_once: + t = wasp.machine.Timer(id=1, period=8000000) + mute = wasp.watch.display.mute + t.start() + self._subtick(1) + wasp.system.keep_awake() + mute(True) + while t.time() < 41666: + pass + self._subtick(1) + while t.time() < 83332: + pass + self._subtick(1) + t.stop() + del t + + if len(self._hrdata.data) >= 240: # 10 seconds passed + self._last_HR = self._hrdata.get_heart_rate() + self._last_HR_date = int(wasp.watch.rtc.time()) + self._track_HR_once = _OFF + wasp.watch.hrs.disable() + + def _subtick(self, ticks): + """track heart rate at 24Hz""" + self._hrdata.preprocess(wasp.watch.hrs.read_hrs()) + + def _tiny_vibration(self): + """vibrate just a tiny bit before waking up, to gradually return + to consciousness""" + wasp.gc.collect() + mute = wasp.watch.display.mute + mute(True) + wasp.system.wake() + wasp.system.switch(self) + wasp.watch.vibrator.pulse(duty=60, ms=100) + + def _smart_alarm_start(self): + SmartAlarm(self) + + +class SmartAlarm(): + def __init__(self, sleeptk): + self.sleeptk = sleeptk + self._smart_alarm_compute() + + @micropython.native + def _smart_alarm_compute(self): + """computes best wake up time from sleep data""" + wasp.gc.collect() + if not self.sleeptk._smart_alarm_state: + t = wasp.watch.time.localtime(wasp.watch.rtc.time()) + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), + {"src": "SleepTk", + "title": "Smart alarm computation", + "body": "Started computation for the smart alarm \ +BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) + return + mute = wasp.watch.display.mute + mute(True) + wasp.system.wake() + wasp.system.switch(self.sleeptk) + t = wasp.watch.time.localtime(wasp.watch.rtc.time()) + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), + {"src": "SleepTk", + "title": "Starting smart alarm computation", + "body": "Starting computation for the smart alarm at {:02d}h{:02d}m".format(t[3], t[4])} + ) + try: + start_time = wasp.watch.rtc.time() + # stop tracking to save memory, keep the alarm just in case + #self.sleeptk._disable_tracking(keep_main_alarm=True) + + # read file one character at a time, to get only the 1st + # value of each row, which is the arm angle + data = array("f") + buff = b"" + f = open(self.sleeptk.filep, "rb") + skip = False + while True: + char = f.read(1) + if char == b",": # start ignoring after the first col + skip = True + continue + if char == b"\n": + skip = False # stop skipping because reading a new line + data.append(float(buff)) + buff = b"" + continue + if char == b"": # end of file + data.append(float(buff)) + break + if not skip: # digit of arm angle value + buff += char + + f.close() + del f, char, buff + wasp.gc.collect() + wasp.system.keep_awake() + + earlier, cycle = self.sleeptk._signal_processing(data) + WU_t = self.sleeptk._WU_t + wasp.gc.collect() + + # add new alarm + wasp.system.set_alarm(max(WU_t - earlier, int(wasp.watch.rtc.time()) + 3), # not before right now, to make sure it rings + self.sleeptk._listen_to_ticks) + + # replace old gentle alarm by another one + if self.sleeptk._grad_alarm_state: + for t in _GRADUAL_WAKE: + wasp.system.cancel_alarm(WU_t - t*60, self.sleeptk._tiny_vibration) + if earlier + t*60 < _ANTICIPATE_ALLOWED: + wasp.system.set_alarm(WU_t - earlier - t*60, self.sleeptk._tiny_vibration) + + self.sleeptk._earlier = earlier + self.sleeptk._page = _TRACKING + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", + "title": "Finished smart alarm computation", + "body": "Finished computing best wake up time in {:2f}s. Sleep cycle: {:.2f}h".format(wasp.watch.rtc.time() - start_time, cycle) + }) + except Exception as e: + wasp.gc.collect() + h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] + msg = "Exception occured at {:02d}h{:02d}m: '{}'%".format(h, m, str(e)) + f = open("smart_alarm_error_{}.txt".format(int(wasp.watch.rtc.time())), "wb") + f.write(msg.encode()) + f.close() + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", + "title": "Smart alarm error", + "body": msg}) + wasp.gc.collect() + + @micropython.native def _signal_processing(self, data): """signal processing over the data read from the local file""" @@ -473,8 +624,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # sleep cycle duration is the average time distance between those N peaks cycle = sum([x_maximas[i+1] - x_maximas[i] for i in range(len(x_maximas) -1)]) / N * _STORE_FREQ - last_peak = self._offset + x_maximas[-1] * _STORE_FREQ - WU_t = self._WU_t + last_peak = self.sleeptk._offset + x_maximas[-1] * _STORE_FREQ + WU_t = self.sleeptk._WU_t # check if too late, already woken up: if last_peak + cycle > WU_t: @@ -484,145 +635,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) if last_peak + cycle < WU_t - _ANTICIPATE_ALLOWED: earlier = _ANTICIPATE_ALLOWED else: # will wake you up at computed time - earlier = last_peak - self._offset + cycle + earlier = last_peak - self.sleeptk._offset + cycle wasp.system.keep_awake() return (earlier, cycle) - - def _smart_alarm_compute(self): - """computes best wake up time from sleep data""" - wasp.gc.collect() - if not self._smart_alarm_state: - t = wasp.watch.time.localtime(wasp.watch.rtc.time()) - wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), - {"src": "SleepTk", - "title": "Smart alarm computation", - "body": "Started computation for the smart alarm \ -BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) - return - mute = wasp.watch.display.mute - mute(True) - wasp.system.wake() - wasp.system.switch(self) - t = wasp.watch.time.localtime(wasp.watch.rtc.time()) - wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), - {"src": "SleepTk", - "title": "Starting smart alarm computation", - "body": "Starting computation for the smart alarm at {:02d}h{:02d}m".format(t[3], t[4])} - ) - try: - start_time = wasp.watch.rtc.time() - # stop tracking to save memory, keep the alarm just in case - #self._disable_tracking(keep_main_alarm=True) - - # read file one character at a time, to get only the 1st - # value of each row, which is the arm angle - data = array("f") - buff = b"" - f = open(self.filep, "rb") - skip = False - while True: - char = f.read(1) - if char == b",": # start ignoring after the first col - skip = True - continue - if char == b"\n": - skip = False # stop skipping because reading a new line - data.append(float(buff)) - buff = b"" - continue - if char == b"": # end of file - data.append(float(buff)) - break - if not skip: # digit of arm angle value - buff += char - - f.close() - del f, char, buff - wasp.gc.collect() - wasp.system.keep_awake() - - earlier, cycle = self._signal_processing(data) - WU_t = self._WU_t - wasp.gc.collect() - - # add new alarm - wasp.system.set_alarm(max(WU_t - earlier, int(wasp.watch.rtc.time()) + 3), # not before right now, to make sure it rings - self._listen_to_ticks) - - # replace old gentle alarm by another one - if self._grad_alarm_state: - for t in _GRADUAL_WAKE: - wasp.system.cancel_alarm(WU_t - t*60, self._tiny_vibration) - if earlier + t*60 < _ANTICIPATE_ALLOWED: - wasp.system.set_alarm(WU_t - earlier - t*60, self._tiny_vibration) - - self._earlier = earlier - self._page = _TRACKING - wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", - "title": "Finished smart alarm computation", - "body": "Finished computing best wake up time in {:2f}s. Sleep cycle: {:.2f}h".format(wasp.watch.rtc.time() - start_time, cycle) - }) - except Exception as e: - wasp.gc.collect() - h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] - msg = "Exception occured at {:02d}h{:02d}m: '{}'%".format(h, m, str(e)) - f = open("smart_alarm_error_{}.txt".format(int(wasp.watch.rtc.time())), "wb") - f.write(msg.encode()) - f.close() - wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), {"src": "SleepTk", - "title": "Smart alarm error", - "body": msg}) - wasp.gc.collect() - - - def _listen_to_ticks(self): - """listen to ticks every second, telling the watch to vibrate""" - wasp.gc.collect() - wasp.system.notify_level = self._old_notification_level # restore notification level - self._page = _RINGING - mute = wasp.watch.display.mute - mute(True) - wasp.system.wake() - wasp.system.switch(self) - self._draw() - wasp.system.request_tick(period_ms=1000) - - def tick(self, ticks): - """vibrate to wake you up OR track heart rate using code from heart.py""" - if self._page == _RINGING: - wasp.watch.vibrator.pulse(duty=50, ms=500) - elif self._track_HR_once: - t = wasp.machine.Timer(id=1, period=8000000) - mute = wasp.watch.display.mute - t.start() - self._subtick(1) - wasp.system.keep_awake() - mute(True) - while t.time() < 41666: - pass - self._subtick(1) - while t.time() < 83332: - pass - self._subtick(1) - t.stop() - del t - - if len(self._hrdata.data) >= 240: # 10 seconds passed - self._last_HR = self._hrdata.get_heart_rate() - self._last_HR_date = int(wasp.watch.rtc.time()) - self._track_HR_once = _OFF - wasp.watch.hrs.disable() - - def _subtick(self, ticks): - """track heart rate at 24Hz""" - self._hrdata.preprocess(wasp.watch.hrs.read_hrs()) - - def _tiny_vibration(self): - """vibrate just a tiny bit before waking up, to gradually return - to consciousness""" - wasp.gc.collect() - mute = wasp.watch.display.mute - mute(True) - wasp.system.wake() - wasp.system.switch(self) - wasp.watch.vibrator.pulse(duty=60, ms=100) From b66e5c3093d7a24f8b9355e3f3af23bce34f5c45 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 15 Apr 2022 17:09:57 +0200 Subject: [PATCH 352/485] removed micropython decorators because it was taking too much space --- SleepTk.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 776cfc7..8493dee 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -16,28 +16,28 @@ import fonts import math import ppg from array import array -import micropython +from micropython import const # HARDCODED VARIABLES: -_ON = micropython.const(1) -_OFF = micropython.const(0) -_TRACKING = micropython.const(0) -_RINGING = micropython.const(1) -_START = micropython.const(2) # page values: -_SETTINGS1 = micropython.const(3) -_SETTINGS2 = micropython.const(4) +_ON = const(1) +_OFF = const(0) +_TRACKING = const(0) +_RINGING = const(1) +_START = const(2) # page values: +_SETTINGS1 = const(3) +_SETTINGS2 = const(4) _FONT = fonts.sans18 -_TIMESTAMP = micropython.const(946684800) # unix time and time used by wasp os don't have the same reference date -_FREQ = micropython.const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_HR_FREQ = micropython.const(1800) # how many seconds between heart rate data -_STORE_FREQ = micropython.const(120) # process data and store to file every X seconds -_BATTERY_THRESHOLD = micropython.const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable +_TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date +_FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds +_HR_FREQ = const(1800) # how many seconds between heart rate data +_STORE_FREQ = const(120) # process data and store to file every X seconds +_BATTERY_THRESHOLD = const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable # user might want to edit this: -_ANTICIPATE_ALLOWED = micropython.const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set +_ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up -_TIME_TO_FALL_ASLEEP = micropython.const(14) # in minutes, according to https://sleepyti.me/ -_CYCLE_LENGTH = micropython.const(90) # in minutes, default of 90 or 100, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! +_TIME_TO_FALL_ASLEEP = const(14) # in minutes, according to https://sleepyti.me/ +_CYCLE_LENGTH = const(90) # in minutes, default of 90 or 100, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! class SleepTkApp(): @@ -366,7 +366,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() - @micropython.native def _periodicSave(self): """save data to csv with row order: 1. average arm angle @@ -456,13 +455,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def _smart_alarm_start(self): SmartAlarm(self) - class SmartAlarm(): def __init__(self, sleeptk): self.sleeptk = sleeptk self._smart_alarm_compute() - @micropython.native def _smart_alarm_compute(self): """computes best wake up time from sleep data""" wasp.gc.collect() @@ -549,7 +546,6 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) "body": msg}) wasp.gc.collect() - @micropython.native def _signal_processing(self, data): """signal processing over the data read from the local file""" From ab886793867c7c52f0bdcaa42ebfa05165e3d8cb Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 16 Apr 2022 10:57:59 +0200 Subject: [PATCH 353/485] minor: draw blank scren when starting tracking --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index 8493dee..9e67139 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -150,6 +150,7 @@ class SleepTkApp(): self.check_grad.draw() return if self.btn_sta.touch(event): + draw.fill() self._start_tracking() elif self.btn_HR.touch(event): self.btn_HR.draw() From baaf78f30ef49594ef498bc1c940e30d41ed36c9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 19 Apr 2022 14:41:37 +0200 Subject: [PATCH 354/485] minor: style --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1ad3167..e0fedf4 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ **Goal:** privacy friendly sleep tracker with smart alarm for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). ## Features: -* **sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file +* **sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file. * **Flexible**: does not make too many assumption regarding time to fall asleep, sleep cycle duration etc. SleepTk tries various data to see what fits best for your profile. If you still want to customize things, all the hardcoded and commented settings are easily accessible at the top of the file. -* **suggested alarm clock**: suggests wake up time according to average sleep cycles length -* **gradual alarm clock**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness +* **suggested alarm clock**: suggests wake up time according to average sleep cycles length. +* **gradual alarm clock**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness. * **smart alarm clock (alpha)**: adaptative alarm that wakes you up to 40 minutes before the set time to make sure you wake up feeling refreshed. -* **privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download if if needed) -* open source +* **privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download if if needed). +* **open source** ## **How to install**: *(for now you need my slightly forked wasp-os that allows to use accelerometer data)* From f8c0fc94cb9dbeb0a0b7a47b47c89cbed10dc729 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 19 Apr 2022 14:44:17 +0200 Subject: [PATCH 355/485] minor: style --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e0fedf4..f059077 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ **Goal:** privacy friendly sleep tracker with smart alarm for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). ## Features: -* **sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file. +* **Sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file. * **Flexible**: does not make too many assumption regarding time to fall asleep, sleep cycle duration etc. SleepTk tries various data to see what fits best for your profile. If you still want to customize things, all the hardcoded and commented settings are easily accessible at the top of the file. -* **suggested alarm clock**: suggests wake up time according to average sleep cycles length. -* **gradual alarm clock**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness. -* **smart alarm clock (alpha)**: adaptative alarm that wakes you up to 40 minutes before the set time to make sure you wake up feeling refreshed. -* **privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download if if needed). -* **open source** +* **Suggests best alarm time**: suggests wake up time according to average sleep cycles length. +* **Gradual wake**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness. +* **Smart alarm clock (alpha)**: adaptative alarm that wakes you at the best time of your sleep cycle (up to 40 minutes before the set time) to make sure you wake up feeling refreshed. +* **Privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download it if needed). +* **Open source** ## **How to install**: *(for now you need my slightly forked wasp-os that allows to use accelerometer data)* From c991af0b45a41b6883ead7f4379ee618635491df Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 19 Apr 2022 14:45:29 +0200 Subject: [PATCH 356/485] mention alpha stage of heart tracking --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f059077..f9cb2be 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ * **Suggests best alarm time**: suggests wake up time according to average sleep cycles length. * **Gradual wake**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness. * **Smart alarm clock (alpha)**: adaptative alarm that wakes you at the best time of your sleep cycle (up to 40 minutes before the set time) to make sure you wake up feeling refreshed. +* **Heart tracking (alpha)**: tracks your heart rate throughout the night. * **Privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download it if needed). * **Open source** From 7e5341328003f12a3aebae9e3352a713f8400f0b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 21 Apr 2022 12:32:21 +0200 Subject: [PATCH 357/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f9cb2be..c184604 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ ## TODO **misc** +* write sleep duration and cycle during the night * remove duplicate variables that store spinner values * investigate adding snooze feature like in the original `alarms.py` app * investigate adding a simple feature to wake you up only after a certain movement threshold was passed From 3d3f82f3a8f7b53ab790f5f6ba0993a2f273cfd3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 21 Apr 2022 12:33:25 +0200 Subject: [PATCH 358/485] new: removed start page that was useless --- README.md | 1 - SleepTk.py | 39 ++++++++++++++----------------------- screenshots/start_page.png | Bin 7516 -> 0 bytes 3 files changed, 15 insertions(+), 25 deletions(-) delete mode 100644 screenshots/start_page.png diff --git a/README.md b/README.md index c184604..d0839d6 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ * the notifications are set to "silent" during the tracking session and are restored to the previously used level when the alarm is ringing # Screenshots: -![start](./screenshots/start_page.png) ![settings](./screenshots/settings_page.png) ![settings2](./screenshots/settings_page2.png) ![tracking](./screenshots/tracking_page.png) diff --git a/SleepTk.py b/SleepTk.py index 9e67139..4624486 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -23,9 +23,8 @@ _ON = const(1) _OFF = const(0) _TRACKING = const(0) _RINGING = const(1) -_START = const(2) # page values: -_SETTINGS1 = const(3) -_SETTINGS2 = const(4) +_SETTINGS1 = const(2) +_SETTINGS2 = const(3) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds @@ -55,7 +54,7 @@ class SleepTkApp(): self._track_HR_once = _OFF self._spinval_H = _OFF self._spinval_M = _OFF - self._page = _START + self._page = _SETTINGS1 self._is_tracking = _OFF self._conf_view = _OFF # confirmation view self._earlier = 0 # number of seconds between the alarm you set manually and the smart alarm time @@ -90,20 +89,20 @@ class SleepTkApp(): if state: if self._page == _RINGING: self._disable_tracking() - self._page = _START + self._page = _SETTINGS1 else: wasp.system.navigate(wasp.EventType.HOME) def swipe(self, event): - "navigate between start and various settings page" - if self._page >= 2: + "navigate between settings page" + if self._page == _SETTINGS1: + if event[0] == wasp.EventType.LEFT: + self._page = _SETTINGS2 + self._draw() + elif self._page == _SETTINGS2: if event[0] == wasp.EventType.RIGHT: - self._page -= 1 - else: - self._page += 1 - self._page = max(self._page, _START) - self._page = min(self._page, _SETTINGS2) - self._draw() + self._page = _SETTINGS1 + self._draw() def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" @@ -120,13 +119,13 @@ class SleepTkApp(): if self._conf_view.touch(event): if self._conf_view.value: self._disable_tracking() - self._page = _START + self._page = _SETTINGS1 self._conf_view = _OFF draw.reset() elif self._page == _RINGING: if self.btn_al.touch(event): self._disable_tracking() - self._page = _START + self._page = _SETTINGS1 elif self._page == _SETTINGS1: if self._alarm_state and (self._spin_H.touch(event) or self._spin_M.touch(event)): self._spinval_H = self._spin_H.value @@ -203,18 +202,10 @@ class SleepTkApp(): self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() draw.reset() - elif self._page == _START: - draw.set_font(_FONT) - label = 'Sleep tracker with optional wake up alarm, smart alarm up to 40min before, gradual wake up to 15m. Swipe to navigate.' - chunks = draw.wrap(label, 240) - for i in range(len(chunks)-1): - sub = label[chunks[i]:chunks[i+1]].rstrip() - draw.string(sub, 0, 60 + 20 * i) - draw.reset() + elif self._page == _SETTINGS1: # reset spinval values between runs self._spinval_H = _OFF self._spinval_M = _OFF - elif self._page == _SETTINGS1: self.check_al = widgets.Checkbox(x=0, y=40, label="Wake me up") self.check_al.state = self._alarm_state self.check_al.draw() diff --git a/screenshots/start_page.png b/screenshots/start_page.png deleted file mode 100644 index 40d636e45a779985fa38047591cc80e7e92ce533..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7516 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 VC(cd>plb?xrmCY-rDXl)-vI6GI!*up From 553d1addfa85689ac0938bc5a5d43028b65ea590 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 21 Apr 2022 12:33:45 +0200 Subject: [PATCH 359/485] fix: string warning that no alarm was set was not offset --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4624486..df1fa1b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -241,7 +241,7 @@ class SleepTkApp(): chunks = draw.wrap(label, 240) for i in range(len(chunks)-1): sub = label[chunks[i]:chunks[i+1]].rstrip() - draw.string(sub, 0, 50 + 24 * i) + draw.string(sub, 0, 80 + 20 * i) draw.reset() self.btn_HR = widgets.Checkbox(x=0, y=40, label="Heart rate tracking") self.btn_HR.state = self._track_HR_state From 942be77775f5fc49eb8d4fa31049959825f08a43 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 11:15:20 +0200 Subject: [PATCH 360/485] new: added icon and credits to plan5 --- README.md | 3 +++ SleepTk.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/README.md b/README.md index d0839d6..990bb8d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ * **Privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download it if needed). * **Open source** +## Credits: +* Many thanks to Emanuel Loffler (https://github.com/plan5) who kindly created the logo. + ## **How to install**: *(for now you need my slightly forked wasp-os that allows to use accelerometer data)* * download the latest [forked wasp-os](https://github.com/thiswillbeyourgithub/wasp-os) diff --git a/SleepTk.py b/SleepTk.py index df1fa1b..98300da 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -18,6 +18,26 @@ import ppg from array import array from micropython import const +# 2-bit RLE, 60x60, 225 bytes, kindly designed by Emanuel Lofler (https://github.com/plan5) +icon = ( + b'\x02' + b'<<' + b'?\x11\xd2*\xd2*\xd2*\xd8$\xd82\xca2\xca6' + b'\xc4\x02\xc24\xc4\x02\xc24\xc4\x04\xc40\xc4\x04\xc40' + b'\xc4\x04\xc40\xc4\x04\xc42\xc2\x04\xc8.\xc2\x04\xc8.' + b'\xc2\x04\xc8.\xc2\x04\xc8 \xc4\n\xc2\x06\xc6 \xc4\n' + b'\xc2\x06\xc6 \xc4\x08\xc6\x04\xc6 \xc4\x08\xc6\x04\xc6\x1e' + b'\xc8\x06\xc6\x04\xc6\x1e\xc8\x06\xc6\x04\xc6\x1e\xc8\x06\xc6\x04' + b'\xc6\x1e\xc8\x06\xc6\x04\xc6\x1c\xca\x06\xc6\x04\xc6\x1c\xd6\x04' + b'\xc6\x1c\xd6\x04\xc6\x1c\xd6\x04\xc6\x07\xed\x08\xc4\x03\xed\x08' + b'\xc4\x03\xed\x08\xc4\x03\xed\x08\xc4"\xc6\x04\xc4\x06\xc2&' + b'\xc6\x04\xc4\x06\xc2&\xc6\x04\xc4\x06\xc2&\xc6\x04\xc4\x06' + b'\xc2&\xc6\x04\xc6\x04\xc2&\xc6\x04\xc6\x04\xc2(\xc2\x04' + b'\xc8\x02\xc2*\xc2\x04\xc8\x02\xc2.\xce.\xce.\xce.' + b'\xce*\xd1+\xd1+\xd1+\xd1%\xd7%\xd4\x16\xe6\x16' + b'\xe6\x16\xe0\x1c\xe0\x1c\xde\x1e\xde"\xd4(\xd4\x1a' +) + # HARDCODED VARIABLES: _ON = const(1) _OFF = const(0) @@ -41,6 +61,7 @@ _CYCLE_LENGTH = const(90) # in minutes, default of 90 or 100, according to http class SleepTkApp(): NAME = 'SleepTk' + ICON = icon def __init__(self): wasp.gc.collect() From 1527adaea9cbd37a76d9500101c497d49fa88d0a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 12:00:31 +0200 Subject: [PATCH 361/485] new: delete button during tracking to save memory --- SleepTk.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 98300da..9bbf403 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -314,6 +314,13 @@ class SleepTkApp(): wasp.system.notify_level = 1 # silent notifications self._page = _TRACKING + # save some memory + self.btn_sta = None + self.btn_al = None + self.btn_off = None + self.btn_HR = None + del self.btn_sta, self.btn_al, self.btn_off, self.btn_HR + def _read_time(self, HH, MM): "convert time from spinners to seconds" (Y, Mo, d, h, m) = wasp.watch.rtc.get_localtime()[0:5] From d657776654f42db9aeeeed0625a7e2b631340fd1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 12:01:16 +0200 Subject: [PATCH 362/485] minor: consider more variable to be of interest to the user --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 9bbf403..2f12cbc 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -47,12 +47,12 @@ _SETTINGS1 = const(2) _SETTINGS2 = const(3) _FONT = fonts.sans18 _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date + +# user might want to edit this: _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _HR_FREQ = const(1800) # how many seconds between heart rate data _STORE_FREQ = const(120) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable - -# user might want to edit this: _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up _TIME_TO_FALL_ASLEEP = const(14) # in minutes, according to https://sleepyti.me/ From 7504800bd82b494e351caf64b64e793afb376aad Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 12:12:28 +0200 Subject: [PATCH 363/485] fix: missing umlaut in credits --- README.md | 2 +- SleepTk.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 990bb8d..0da82bb 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ * **Open source** ## Credits: -* Many thanks to Emanuel Loffler (https://github.com/plan5) who kindly created the logo. +* Many thanks to Emanuel Löffler (https://github.com/plan5) who kindly created the logo. ## **How to install**: *(for now you need my slightly forked wasp-os that allows to use accelerometer data)* diff --git a/SleepTk.py b/SleepTk.py index 2f12cbc..453560a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -18,7 +18,7 @@ import ppg from array import array from micropython import const -# 2-bit RLE, 60x60, 225 bytes, kindly designed by Emanuel Lofler (https://github.com/plan5) +# 2-bit RLE, 60x60, 225 bytes, kindly designed by Emanuel Löffler (https://github.com/plan5) icon = ( b'\x02' b'<<' From 2192397a3c41e738766a7abf2ee35ef6e689c3c4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 12:22:27 +0200 Subject: [PATCH 364/485] new: keep awake during heart tracking --- SleepTk.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 453560a..3021b39 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -440,18 +440,21 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) t = wasp.machine.Timer(id=1, period=8000000) mute = wasp.watch.display.mute t.start() - self._subtick(1) wasp.system.keep_awake() + self._subtick(1) mute(True) while t.time() < 41666: pass + wasp.system.keep_awake() self._subtick(1) while t.time() < 83332: pass + wasp.system.keep_awake() self._subtick(1) t.stop() del t + wasp.system.keep_awake() if len(self._hrdata.data) >= 240: # 10 seconds passed self._last_HR = self._hrdata.get_heart_rate() self._last_HR_date = int(wasp.watch.rtc.time()) From c63f5ebee62783bac8e3edbd0726ab0372e08bf4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 12:35:40 +0200 Subject: [PATCH 365/485] fix: switch to SleepTk to receive ticks when tracking heart rate --- SleepTk.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 3021b39..8be95a9 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -382,6 +382,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.watch.hrs.enable() self._hrdata = ppg.PPG(wasp.watch.hrs.read_hrs()) self._track_HR_once = _ON + mute = wasp.watch.display.mute + mute(True) + wasp.system.wake() + mute(True) + wasp.system.switch(self) wasp.system.request_tick(1000 // 8) wasp.gc.collect() @@ -419,7 +424,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._last_checkpoint = self._data_point_nb wasp.gc.collect() - def _listen_to_ticks(self): """listen to ticks every second, telling the watch to vibrate""" wasp.gc.collect() From 9e4756fda86126431a909fd62666cf3908b5afbf Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 12:43:21 +0200 Subject: [PATCH 366/485] new: tell user when it's tracking HR --- SleepTk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 8be95a9..64fed2c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -220,6 +220,8 @@ class SleepTkApp(): else: draw.string("No alarm set", 0, 90) draw.string("data points: {} / {}".format(str(self._data_point_nb), str(self._data_point_nb * _FREQ // _STORE_FREQ)), 0, 130) + if self._track_HR_once: + draw.string("(Currently tracking HR)", 0, 150) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() draw.reset() From d4047ab34e11e841b23006bdd71c6d842d750e55 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 12:43:35 +0200 Subject: [PATCH 367/485] new: don't exit app when sleeping --- SleepTk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 64fed2c..cbb8242 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -99,6 +99,9 @@ class SleepTkApp(): wasp.EventMask.SWIPE_LEFTRIGHT | wasp.EventMask.BUTTON) + def sleep(self): + return True + def background(self): wasp.watch.hrs.disable() self._track_HR_once = _OFF From 1b891927b6e093fb40bca8015287a72f2664d84e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 12:44:27 +0200 Subject: [PATCH 368/485] new: add loading screen --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index cbb8242..299e6d8 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -174,6 +174,7 @@ class SleepTkApp(): return if self.btn_sta.touch(event): draw.fill() + draw.string("Loading", 0, 100) self._start_tracking() elif self.btn_HR.touch(event): self.btn_HR.draw() From 07dd718b7377d3409c1d8ec55e32b934375c0d41 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 12:59:49 +0200 Subject: [PATCH 369/485] new: display sleep duration while sleeping --- SleepTk.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 299e6d8..9dfed7e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -184,18 +184,25 @@ class SleepTkApp(): def _draw_duration(self, draw): draw.set_font(_FONT) - duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time()) / 60 - _TIME_TO_FALL_ASLEEP - assert duration >= _TIME_TO_FALL_ASLEEP + if self._page == _SETTINGS1: + duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time()) / 60 - _TIME_TO_FALL_ASLEEP + assert duration >= _TIME_TO_FALL_ASLEEP + y = 180 + elif self._page == _TRACKING: + duration = (wasp.watch.rtc.time() - self._offset) / 60 # time slept + y = 130 + draw.string("Total sleep {:02d}h{:02d}m".format( int(duration // 60), - int(duration % 60)), 0, 180) + int(duration % 60)), 0, y + 20) cycl = duration / _CYCLE_LENGTH - draw.string("{} cycles ".format(str(cycl)[0:4]), 0, 200) cycl_modulo = cycl % 1 - if cycl_modulo > 0.10 and cycl_modulo < 0.90: - draw.string("Not rested!", 0, 220) - else: - draw.string("Well rested", 0, 220) + draw.string("{} cycles ".format(str(cycl)[0:4]), 0, y) + if duration > 30 and not self._track_HR_once: + if cycl_modulo > 0.10 and cycl_modulo < 0.90: + draw.string("Not rested!", 0, y + 40) + else: + draw.string("Well rested", 0, y + 40) def _draw(self): """GUI""" @@ -213,22 +220,23 @@ class SleepTkApp(): draw.reset() elif self._page == _TRACKING: ti = wasp.watch.time.localtime(self._offset) - draw.string('Began at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 70) + draw.string('Began at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 50) if self._alarm_state: word = "Alarm at " if self._smart_alarm_state: word = "Alarm BEFORE " ti = wasp.watch.time.localtime(self._WU_t) - draw.string("{}{:02d}:{:02d}".format(word, ti[3], ti[4]), 0, 90) - draw.string("Gradual wake: {}".format(True if self._grad_alarm_state else False), 0, 110) + draw.string("{}{:02d}:{:02d}".format(word, ti[3], ti[4]), 0, 70) + draw.string("Gradual wake: {}".format(True if self._grad_alarm_state else False), 0, 90) else: - draw.string("No alarm set", 0, 90) - draw.string("data points: {} / {}".format(str(self._data_point_nb), str(self._data_point_nb * _FREQ // _STORE_FREQ)), 0, 130) + draw.string("No alarm set", 0, 70) + draw.string("data points: {} / {}".format(str(self._data_point_nb), str(self._data_point_nb * _FREQ // _STORE_FREQ)), 0, 110) if self._track_HR_once: - draw.string("(Currently tracking HR)", 0, 150) + draw.string("(Currently tracking HR)", 0, 170) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() draw.reset() + self._draw_duration(draw) elif self._page == _SETTINGS1: # reset spinval values between runs self._spinval_H = _OFF From d26312444659d294665908a9b1e198f0f12b277e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 13:00:51 +0200 Subject: [PATCH 370/485] new: delete spinners to save memory --- SleepTk.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 9dfed7e..b8a9012 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -333,7 +333,9 @@ class SleepTkApp(): self.btn_al = None self.btn_off = None self.btn_HR = None - del self.btn_sta, self.btn_al, self.btn_off, self.btn_HR + self._spin_H = None + self._spin_M = None + del self.btn_sta, self.btn_al, self.btn_off, self.btn_HR, self._spin_H, self._spin_M def _read_time(self, HH, MM): "convert time from spinners to seconds" From bd88b781157e37d82b0bc62351f8912a9ea35e19 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 13:01:14 +0200 Subject: [PATCH 371/485] todo --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 0da82bb..9b655fb 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,6 @@ ## TODO **misc** -* write sleep duration and cycle during the night -* remove duplicate variables that store spinner values * investigate adding snooze feature like in the original `alarms.py` app * investigate adding a simple feature to wake you up only after a certain movement threshold was passed * add a "nap tracking" mode that records sleep tracking with more precision From 2e2840a6515a3a69143187f311b77c16a118e04e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 13:01:54 +0200 Subject: [PATCH 372/485] track heart rate every 10 minutes --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index b8a9012..0120d56 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -50,7 +50,7 @@ _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have t # user might want to edit this: _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_HR_FREQ = const(1800) # how many seconds between heart rate data +_HR_FREQ = const(600) # how many seconds between heart rate data _STORE_FREQ = const(120) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set From 0d27e8e2d83cb2f4cea86fa9f8a161039a175682 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 13:03:53 +0200 Subject: [PATCH 373/485] new: remove useless warning when alarm not set --- SleepTk.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 0120d56..57d201b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -270,13 +270,6 @@ class SleepTkApp(): self.check_smart = widgets.Checkbox(x=0, y=120, label="Smart alarm (alpha)") self.check_smart.state = self._smart_alarm_state self.check_smart.draw() - else: - draw.set_font(_FONT) - label = 'Skipping smart and gradual alarm because no regular alarm is set' - chunks = draw.wrap(label, 240) - for i in range(len(chunks)-1): - sub = label[chunks[i]:chunks[i+1]].rstrip() - draw.string(sub, 0, 80 + 20 * i) draw.reset() self.btn_HR = widgets.Checkbox(x=0, y=40, label="Heart rate tracking") self.btn_HR.state = self._track_HR_state From 5cf1558dc9ab2d4d2d49db35197e675976ac2f8c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 13:30:53 +0200 Subject: [PATCH 374/485] new: don't track HR right away, wait for 60s --- SleepTk.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 57d201b..a336e0a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -318,6 +318,10 @@ class SleepTkApp(): if self._smart_alarm_state: self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 wasp.system.set_alarm(self._WU_a, self._smart_alarm_start) + + # don't track heart rate right away, wait 60s + if self._track_HR_state: + self._last_HR_date = int(wasp.watch.rtc.time()) + 60 wasp.system.notify_level = 1 # silent notifications self._page = _TRACKING From b95f0285e9fc775137cfc249060962ea64f47b46 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 13:52:48 +0200 Subject: [PATCH 375/485] new: auto restart watch when downloading all sleep data --- pull_latest_sleep_data.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pull_latest_sleep_data.py b/pull_latest_sleep_data.py index cab5bb6..7a78d75 100644 --- a/pull_latest_sleep_data.py +++ b/pull_latest_sleep_data.py @@ -1,6 +1,7 @@ #!/usr/local/bin/python3 +import time import os import subprocess import shlex @@ -18,6 +19,8 @@ out = subprocess.check_output(shlex.split(ls_cmd)).decode() files = re.findall(r"\d*\.csv", out) print(f"Found files {', '.join(files)}") +reset_cmd = './tools/wasptool --verbose --reset' + if mode == "latest": to_dl = files[-1] @@ -41,4 +44,9 @@ for fi in to_dl: except Exception as e: print(f"Error happened while downloading {fi}, deleting local incomplete file") os.system(f"rm ./logs/sleep/{fi}") + if mode == "all": + print("Restarting watch.") + out = subprocess.check_output(shlex.split(reset_cmd)) + time.sleep(10) + print("\n\n") From d723f08a61823d71330d9427b008e9b17252f90d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 13:53:00 +0200 Subject: [PATCH 376/485] new: add script to delete all sleep data --- rm_all_sleep_data.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 rm_all_sleep_data.py diff --git a/rm_all_sleep_data.py b/rm_all_sleep_data.py new file mode 100644 index 0000000..e4dcb9b --- /dev/null +++ b/rm_all_sleep_data.py @@ -0,0 +1,38 @@ +#!/usr/local/bin/python3 + +import time +import subprocess +import shlex +import re + +print("\n\nRunning gc.collect()...") +mem_cmd = './tools/wasptool --verbose --eval \'wasp.gc.collect()\'' +subprocess.check_output(shlex.split(mem_cmd)) + +print("\n\nListing remote files...") +ls_cmd = './tools/wasptool --verbose --eval \'from shell import ls ; ls(\"/flash/logs/sleep/\")\'' +out = subprocess.check_output(shlex.split(ls_cmd)).decode() +files = re.findall(r"\d*\.csv", out) +print(f"Found files {', '.join(files)}") + +reset_cmd = './tools/wasptool --verbose --reset' + +to_rm = files + +print("\n\n") +for fi in to_rm: + print(f"Removing file '{fi}'") + rm_cmd = f'./tools/wasptool --verbose --eval \'from shell import rm ; rm(\"logs/sleep/{fi}\")\'' + try: + out = subprocess.check_output(shlex.split(rm_cmd)) + if b"Watch reported error" in out: + raise Exception("Watch reported error") + print(f"Succesfully removed to './logs/sleep/{fi}'") + except Exception as e: + print(f"Error happened while removing {fi}") + + print("Restarting watch.") + out = subprocess.check_output(shlex.split(reset_cmd)) + time.sleep(10) + + print("\n\n") From 47748c612ba3b6892b9857908a137ac2fa375227 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 13:54:30 +0200 Subject: [PATCH 377/485] fix: wrong name for companion scripts --- README.md | 5 +++-- pull_latest_sleep_data.py => pull_sleep_data.py | 0 rm_all_sleep_data.py => rm_sleep_data.py | 0 3 files changed, 3 insertions(+), 2 deletions(-) rename pull_latest_sleep_data.py => pull_sleep_data.py (100%) rename rm_all_sleep_data.py => rm_sleep_data.py (100%) diff --git a/README.md b/README.md index 9b655fb..501b8dc 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,13 @@ * compile `wasp-os`: `make submodules && make softdevice && make BOARD=pinetime all && echo "SUCCESS"` * upload it to your pinetime: `./tools/ota-dfu/dfu.py -z build-pinetime/micropython.zip -a XX:XX:XX:XX:XX:XX --legacy` * reboot the watch and enjoy `SleepTk` -* *optional: download your latest sleep data using the script `pull_latest_sleep_data.py`* +* *optional: download your latest sleep data using the script `pull_sleep_data.py`* +* *optional: delete all the sleep data present in your watch using the script `rm_sleep_data.py`* ### Note to reader: * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) * Status as of end of February 2022: *UI (**done**), regular alarm (**done**), smart alarm (**mostly done but untested**)* -* you can download your sleep data file using the file `pull_latest_sleep_data`. A suggested workflow to load it into [pandas](https://pypi.org/project/pandas/) can be found at the bottom of the page. +* you can download your sleep data file using the file `pull_sleep_data`. A suggested workflow to load it into [pandas](https://pypi.org/project/pandas/) can be found at the bottom of the page. * the notifications are set to "silent" during the tracking session and are restored to the previously used level when the alarm is ringing # Screenshots: diff --git a/pull_latest_sleep_data.py b/pull_sleep_data.py similarity index 100% rename from pull_latest_sleep_data.py rename to pull_sleep_data.py diff --git a/rm_all_sleep_data.py b/rm_sleep_data.py similarity index 100% rename from rm_all_sleep_data.py rename to rm_sleep_data.py From 059d198f7e0e146044b8675be48c7e585eee57d6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 15:47:22 +0200 Subject: [PATCH 378/485] new: in case of error during HR tracking, retry --- SleepTk.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index a336e0a..a74ddf7 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -473,10 +473,17 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.keep_awake() if len(self._hrdata.data) >= 240: # 10 seconds passed - self._last_HR = self._hrdata.get_heart_rate() - self._last_HR_date = int(wasp.watch.rtc.time()) - self._track_HR_once = _OFF - wasp.watch.hrs.disable() + bpm = self._hrdata.get_heart_rate() + if bpm < 150 and bpm > 30: + self._last_HR = bpm + self._last_HR_date = int(wasp.watch.rtc.time()) + self._track_HR_once = _OFF + wasp.watch.hrs.disable() + else: + # in case of invalid data, write it in the file but + # keep trying to read HR + self._last_HR = "?" + self._hrdata = None def _subtick(self, ticks): """track heart rate at 24Hz""" From 87062e4243c46ad3b440080a6e44edff406c04f9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 15:47:37 +0200 Subject: [PATCH 379/485] better HR tracking code --- SleepTk.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index a74ddf7..a6dce57 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -70,6 +70,7 @@ class SleepTkApp(): self._grad_alarm_state = _ON self._smart_alarm_state = _OFF # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._track_HR_state = _OFF + self._hrdata = None self._last_HR = _OFF self._last_HR_date = _OFF self._track_HR_once = _OFF @@ -104,7 +105,6 @@ class SleepTkApp(): def background(self): wasp.watch.hrs.disable() - self._track_HR_once = _OFF self._hrdata = None wasp.gc.collect() @@ -392,14 +392,12 @@ tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ on.".format(h, m, _BATTERY_THRESHOLD)}) elif self._track_HR_state: if wasp.watch.rtc.time() - self._last_HR_date > _HR_FREQ and not self._track_HR_once: - wasp.watch.hrs.enable() - self._hrdata = ppg.PPG(wasp.watch.hrs.read_hrs()) - self._track_HR_once = _ON mute = wasp.watch.display.mute mute(True) wasp.system.wake() mute(True) wasp.system.switch(self) + self._track_HR_once = _ON wasp.system.request_tick(1000 // 8) wasp.gc.collect() @@ -454,6 +452,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) if self._page == _RINGING: wasp.watch.vibrator.pulse(duty=50, ms=500) elif self._track_HR_once: + wasp.watch.hrs.enable() + if self._hrdata is None: + self._hrdata = ppg.PPG(wasp.watch.hrs.read_hrs()) t = wasp.machine.Timer(id=1, period=8000000) mute = wasp.watch.display.mute t.start() From 14b8cedd46baca65ba82afe63c6c7cfeff347338 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 15:49:42 +0200 Subject: [PATCH 380/485] new: store data to file only if not currently tracking HR --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index a6dce57..319d92d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -412,7 +412,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """ buff = self._buff n = self._data_point_nb - self._last_checkpoint - if n >= _STORE_FREQ // _FREQ: + if n >= _STORE_FREQ // _FREQ and not self._track_HR_once: buff[0] /= n buff[1] /= n buff[2] /= n From 19f2fb4ce071634478c3fb3d919d6b685db90558 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 15:50:16 +0200 Subject: [PATCH 381/485] store data every 5 minutes by default --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 319d92d..720b253 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -51,7 +51,7 @@ _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have t # user might want to edit this: _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _HR_FREQ = const(600) # how many seconds between heart rate data -_STORE_FREQ = const(120) # process data and store to file every X seconds +_STORE_FREQ = const(300) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up From 404140986245a441218d91fc504f1a2763cecda6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 16:16:40 +0200 Subject: [PATCH 382/485] fix: restart tracking heart rate if interupted --- SleepTk.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 720b253..7bfc6f8 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -99,10 +99,17 @@ class SleepTkApp(): wasp.system.request_event(wasp.EventMask.TOUCH | wasp.EventMask.SWIPE_LEFTRIGHT | wasp.EventMask.BUTTON) + if self._page == _TRACKING and self._track_HR_once: + wasp.system.request_tick(1000 // 8) def sleep(self): return True + def wake(self): + self._draw() + if self._page == _TRACKING and self._track_HR_once: + wasp.system.request_tick(1000 // 8) + def background(self): wasp.watch.hrs.disable() self._hrdata = None From e9844e389bfa9f6d8078e6f2df921ea5f556e179 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 16:17:00 +0200 Subject: [PATCH 383/485] new: print last HR value when tracking --- SleepTk.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 7bfc6f8..8d276ec 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -72,6 +72,7 @@ class SleepTkApp(): self._track_HR_state = _OFF self._hrdata = None self._last_HR = _OFF + self._last_HR_printed = "?" self._last_HR_date = _OFF self._track_HR_once = _OFF self._spinval_H = _OFF @@ -240,6 +241,8 @@ class SleepTkApp(): draw.string("data points: {} / {}".format(str(self._data_point_nb), str(self._data_point_nb * _FREQ // _STORE_FREQ)), 0, 110) if self._track_HR_once: draw.string("(Currently tracking HR)", 0, 170) + if self._track_HR_state: + draw.string("HR:{}".format(self._last_HR_printed), 200, 50) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() draw.reset() @@ -492,6 +495,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # keep trying to read HR self._last_HR = "?" self._hrdata = None + self._last_HR_printed = self._last_HR def _subtick(self, ticks): """track heart rate at 24Hz""" From 385d3064414a0c843e6621136d3a5d9abf2024f2 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 16:17:13 +0200 Subject: [PATCH 384/485] new: wait less time before tracking HR --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 8d276ec..8946c30 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -329,9 +329,9 @@ class SleepTkApp(): self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 wasp.system.set_alarm(self._WU_a, self._smart_alarm_start) - # don't track heart rate right away, wait 60s + # don't track heart rate right away, wait a few seconds if self._track_HR_state: - self._last_HR_date = int(wasp.watch.rtc.time()) + 60 + self._last_HR_date = int(wasp.watch.rtc.time()) + 10 wasp.system.notify_level = 1 # silent notifications self._page = _TRACKING From 8f3e465cb5189b84fa88d3d86f455a33dd3f1711 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 16:19:55 +0200 Subject: [PATCH 385/485] fix: display HR --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 8946c30..1a89905 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -242,7 +242,7 @@ class SleepTkApp(): if self._track_HR_once: draw.string("(Currently tracking HR)", 0, 170) if self._track_HR_state: - draw.string("HR:{}".format(self._last_HR_printed), 200, 50) + draw.string("HR:{}".format(self._last_HR_printed), 160, 50) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() draw.reset() From 98646be3ebe7c52e36d7adc4090087d3e0295a28 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:01:10 +0200 Subject: [PATCH 386/485] track HR over 30s instead of 10 --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 1a89905..155ed89 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -483,7 +483,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del t wasp.system.keep_awake() - if len(self._hrdata.data) >= 240: # 10 seconds passed + if len(self._hrdata.data) >= 720: # 30 seconds passed bpm = self._hrdata.get_heart_rate() if bpm < 150 and bpm > 30: self._last_HR = bpm From d732a3135b861941ff9bfca5b577c74bc2d4e686 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:04:47 +0200 Subject: [PATCH 387/485] minor: better position to print HR --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 155ed89..5ba78eb 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -242,7 +242,7 @@ class SleepTkApp(): if self._track_HR_once: draw.string("(Currently tracking HR)", 0, 170) if self._track_HR_state: - draw.string("HR:{}".format(self._last_HR_printed), 160, 50) + draw.string("HR:{}".format(self._last_HR_printed), 160, 170) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() draw.reset() From 664bf1902d7e12f065dcaa5b9f21d8f4222626d7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:05:01 +0200 Subject: [PATCH 388/485] minor: rename start tracking to start --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 5ba78eb..02d4f7a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -284,7 +284,7 @@ class SleepTkApp(): self.btn_HR = widgets.Checkbox(x=0, y=40, label="Heart rate tracking") self.btn_HR.state = self._track_HR_state self.btn_HR.draw() - self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start tracking") + self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start") self.btn_sta.draw() draw.reset() self.stat_bar = widgets.StatusBar() From 8b0be2eacc0b6e8d84ade3d0a513e68310a513f6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:05:14 +0200 Subject: [PATCH 389/485] new: reset HR data in between run --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index 02d4f7a..b4045da 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -489,6 +489,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._last_HR = bpm self._last_HR_date = int(wasp.watch.rtc.time()) self._track_HR_once = _OFF + self._hrdata = None wasp.watch.hrs.disable() else: # in case of invalid data, write it in the file but From 02dafb63dd3baf9dbd4abf7a44170cf10187427e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:17:41 +0200 Subject: [PATCH 390/485] feat: ability to disable tracking --- SleepTk.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index b4045da..bf558d8 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -67,6 +67,8 @@ class SleepTkApp(): wasp.gc.collect() # default values: self._alarm_state = _ON + self._tracking_enabled_state = _ON + self._grad_alarm_state = _ON self._smart_alarm_state = _OFF # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART self._track_HR_state = _OFF @@ -188,6 +190,10 @@ class SleepTkApp(): self.btn_HR.draw() self._track_HR_state = self.btn_HR.state return + elif self.check_track.touch(event): + self._tracking_enabled_state = self.check_track.state + self.check_track.draw() + return self._draw() def _draw_duration(self, draw): @@ -280,6 +286,9 @@ class SleepTkApp(): self.check_smart = widgets.Checkbox(x=0, y=120, label="Smart alarm (alpha)") self.check_smart.state = self._smart_alarm_state self.check_smart.draw() + self.check_track = widgets.Checkbox(x=0, y=160, label="Track") + self.check_track.state = self._tracking_enabled_state + self.check_track.draw() draw.reset() self.btn_HR = widgets.Checkbox(x=0, y=40, label="Heart rate tracking") self.btn_HR.state = self._track_HR_state @@ -305,9 +314,10 @@ class SleepTkApp(): f.write(b"") f.close() - # add alarm to log accel data in _FREQ seconds - self.next_al = wasp.watch.rtc.time() + _FREQ - wasp.system.set_alarm(self.next_al, self._trackOnce) + # if enabled, add alarm to log accel data in _FREQ seconds + if self._tracking_enabled_state: + self.next_al = wasp.watch.rtc.time() + _FREQ + wasp.system.set_alarm(self.next_al, self._trackOnce) if self._grad_alarm_state and not self._alarm_state: # fix incompatible settings From 4da2828d2931440bfeaf948fcc50d0bda073ad5b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:17:59 +0200 Subject: [PATCH 391/485] better way to save memory whan starting --- SleepTk.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index bf558d8..4cf0c9a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -301,7 +301,21 @@ class SleepTkApp(): self.stat_bar.draw() def _start_tracking(self): + # save some memory + self.check_al = None + self.check_smart = None + self.check_track = None + self.check_grad = None + self.btn_sta = None + self.btn_al = None + self.btn_off = None + self.btn_HR = None + self._spin_H = None + self._spin_M = None + del self.check_al, self.check_smart, self.check_track, self.check_grad, self.btn_sta, self.btn_al, self.btn_off, self.btn_HR, self._spin_H, self._spin_M + self._is_tracking = True + # accel data not yet written to disk: self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file @@ -345,15 +359,6 @@ class SleepTkApp(): wasp.system.notify_level = 1 # silent notifications self._page = _TRACKING - # save some memory - self.btn_sta = None - self.btn_al = None - self.btn_off = None - self.btn_HR = None - self._spin_H = None - self._spin_M = None - del self.btn_sta, self.btn_al, self.btn_off, self.btn_HR, self._spin_H, self._spin_M - def _read_time(self, HH, MM): "convert time from spinners to seconds" (Y, Mo, d, h, m) = wasp.watch.rtc.get_localtime()[0:5] From e87464c28b43eadc8dba2387e9185c18bc3aaed0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:25:41 +0200 Subject: [PATCH 392/485] fix: disabling tracking automatically disables smart tracking --- SleepTk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4cf0c9a..e3d0952 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -193,7 +193,8 @@ class SleepTkApp(): elif self.check_track.touch(event): self._tracking_enabled_state = self.check_track.state self.check_track.draw() - return + if not self._tracking_enabled_state: + self._smart_alarm_state = _OFF self._draw() def _draw_duration(self, draw): From e225c902ea0f8d1e15ecdc127a51a1b01ed4381f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:26:01 +0200 Subject: [PATCH 393/485] style: reorder smart alarm and body tracking --- SleepTk.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index e3d0952..e9c6019 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -284,12 +284,13 @@ class SleepTkApp(): self.check_grad = widgets.Checkbox(0, 80, "Gradual wake") self.check_grad.state = self._grad_alarm_state self.check_grad.draw() - self.check_smart = widgets.Checkbox(x=0, y=120, label="Smart alarm (alpha)") - self.check_smart.state = self._smart_alarm_state - self.check_smart.draw() - self.check_track = widgets.Checkbox(x=0, y=160, label="Track") + self.check_track = widgets.Checkbox(x=0, y=120, label="Body tracking") self.check_track.state = self._tracking_enabled_state self.check_track.draw() + if self._tracking_enabled_state: + self.check_smart = widgets.Checkbox(x=0, y=160, label="Smart alarm (alpha)") + self.check_smart.state = self._smart_alarm_state + self.check_smart.draw() draw.reset() self.btn_HR = widgets.Checkbox(x=0, y=40, label="Heart rate tracking") self.btn_HR.state = self._track_HR_state From 40d956a0dea3282178e95fc491a205be02daeb14 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:27:54 +0200 Subject: [PATCH 394/485] fix: finish heart tracking when disabling --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index e9c6019..b03600b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -383,9 +383,9 @@ class SleepTkApp(): if self._smart_alarm_state: wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_start) self._smart_alarm_state = _OFF - self._track_HR_state = _OFF wasp.watch.hrs.disable() self._periodicSave() + self._track_HR_state = _OFF wasp.gc.collect() def _trackOnce(self): From 9c05c549a7d46b603a62ec188694932ec16b2bed Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:28:07 +0200 Subject: [PATCH 395/485] fix: undeclared var when disabling tracking if not tracking --- SleepTk.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index b03600b..e6e282c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -334,6 +334,8 @@ class SleepTkApp(): if self._tracking_enabled_state: self.next_al = wasp.watch.rtc.time() + _FREQ wasp.system.set_alarm(self.next_al, self._trackOnce) + else: + self.next_al = None if self._grad_alarm_state and not self._alarm_state: # fix incompatible settings @@ -374,7 +376,8 @@ class SleepTkApp(): """called by touching "STOP TRACKING" or when computing best alarm time to wake up you disables tracking features and alarms""" self._is_tracking = False - wasp.system.cancel_alarm(self.next_al, self._trackOnce) + if self.next_al: + wasp.system.cancel_alarm(self.next_al, self._trackOnce) if self._alarm_state: if keep_main_alarm is False: # to keep the alarm when stopping because of low battery wasp.system.cancel_alarm(self._WU_t, self._listen_to_ticks) From 83d59cb7b1800fd4cec6a11e26b511fa302755f0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:37:30 +0200 Subject: [PATCH 396/485] renamed a lot of var --- SleepTk.py | 132 +++++++++++++++++++++++++++-------------------------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index e6e282c..116837a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -65,26 +65,27 @@ class SleepTkApp(): def __init__(self): wasp.gc.collect() - # default values: - self._alarm_state = _ON - self._tracking_enabled_state = _ON - self._grad_alarm_state = _ON - self._smart_alarm_state = _OFF # activate waking you up at optimal time based on accelerometer data, at the earliest at _WU_LAT - _WU_SMART - self._track_HR_state = _OFF + # default button state + self._state_alarm = _ON + self._state_body_tracking = _ON + self._state_gradual_wake = _ON + self._state_smart_alarm = _OFF + self._state_HR_tracking = _OFF + self._state_spinval_H = _OFF + self._state_spinval_M = _OFF + self._hrdata = None self._last_HR = _OFF self._last_HR_printed = "?" self._last_HR_date = _OFF self._track_HR_once = _OFF - self._spinval_H = _OFF - self._spinval_M = _OFF + self._page = _SETTINGS1 - self._is_tracking = _OFF + self._currently_tracking = _OFF self._conf_view = _OFF # confirmation view - self._earlier = 0 # number of seconds between the alarm you set manually and the smart alarm time - self._old_notification_level = wasp.system.notify_level - self._buff = array("f", [_OFF, _OFF, _OFF]) + self._smart_offset = _OFF # number of seconds between the alarm you set manually and the smart alarm time + self._buff = array("f", [_OFF, _OFF, _OFF]) # contains accelerometer values try: shell.mkdir("logs/") @@ -161,25 +162,25 @@ class SleepTkApp(): self._disable_tracking() self._page = _SETTINGS1 elif self._page == _SETTINGS1: - if self._alarm_state and (self._spin_H.touch(event) or self._spin_M.touch(event)): - self._spinval_H = self._spin_H.value + if self._state_alarm and (self._spin_H.touch(event) or self._spin_M.touch(event)): + self._state_spinval_H = self._spin_H.value self._spin_H.update() - self._spinval_M = self._spin_M.value + self._state_spinval_M = self._spin_M.value self._spin_M.update() - if self._alarm_state: + if self._state_alarm: self._draw_duration(draw) return elif self.check_al.touch(event): - self._alarm_state = self.check_al.state + self._state_alarm = self.check_al.state self.check_al.update() elif self._page == _SETTINGS2: - if self._alarm_state: + if self._state_alarm: if self.check_smart.touch(event): - self._smart_alarm_state = self.check_smart.state + self._state_smart_alarm = self.check_smart.state self.check_smart.draw() return elif self.check_grad.touch(event): - self._grad_alarm_state = self.check_grad.state + self._state_gradual_wake = self.check_grad.state self.check_grad.draw() return if self.btn_sta.touch(event): @@ -188,19 +189,19 @@ class SleepTkApp(): self._start_tracking() elif self.btn_HR.touch(event): self.btn_HR.draw() - self._track_HR_state = self.btn_HR.state + self._state_HR_tracking = self.btn_HR.state return elif self.check_track.touch(event): - self._tracking_enabled_state = self.check_track.state + self._state_body_tracking = self.check_track.state self.check_track.draw() - if not self._tracking_enabled_state: - self._smart_alarm_state = _OFF + if not self._state_body_tracking: + self._state_smart_alarm = _OFF self._draw() def _draw_duration(self, draw): draw.set_font(_FONT) if self._page == _SETTINGS1: - duration = (self._read_time(self._spinval_H, self._spinval_M) - wasp.watch.rtc.time()) / 60 - _TIME_TO_FALL_ASLEEP + duration = (self._read_time(self._state_spinval_H, self._state_spinval_M) - wasp.watch.rtc.time()) / 60 - _TIME_TO_FALL_ASLEEP assert duration >= _TIME_TO_FALL_ASLEEP y = 180 elif self._page == _TRACKING: @@ -225,8 +226,8 @@ class SleepTkApp(): draw.fill(0) draw.set_font(_FONT) if self._page == _RINGING: - if self._earlier != 0: - msg = "WAKE UP ({}m early)".format(str(self._earlier/60)[0:2]) + if self._smart_offset != 0: + msg = "WAKE UP ({}m early)".format(str(self._smart_offset/60)[0:2]) else: msg = "WAKE UP" draw.string(msg, 0, 70) @@ -236,19 +237,19 @@ class SleepTkApp(): elif self._page == _TRACKING: ti = wasp.watch.time.localtime(self._offset) draw.string('Began at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 50) - if self._alarm_state: + if self._state_alarm: word = "Alarm at " - if self._smart_alarm_state: + if self._state_smart_alarm: word = "Alarm BEFORE " ti = wasp.watch.time.localtime(self._WU_t) draw.string("{}{:02d}:{:02d}".format(word, ti[3], ti[4]), 0, 70) - draw.string("Gradual wake: {}".format(True if self._grad_alarm_state else False), 0, 90) + draw.string("Gradual wake: {}".format(True if self._state_gradual_wake else False), 0, 90) else: draw.string("No alarm set", 0, 70) draw.string("data points: {} / {}".format(str(self._data_point_nb), str(self._data_point_nb * _FREQ // _STORE_FREQ)), 0, 110) if self._track_HR_once: draw.string("(Currently tracking HR)", 0, 170) - if self._track_HR_state: + if self._state_HR_tracking: draw.string("HR:{}".format(self._last_HR_printed), 160, 170) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") self.btn_off.draw() @@ -256,44 +257,44 @@ class SleepTkApp(): self._draw_duration(draw) elif self._page == _SETTINGS1: # reset spinval values between runs - self._spinval_H = _OFF - self._spinval_M = _OFF + self._state_spinval_H = _OFF + self._state_spinval_M = _OFF self.check_al = widgets.Checkbox(x=0, y=40, label="Wake me up") - self.check_al.state = self._alarm_state + self.check_al.state = self._state_alarm self.check_al.draw() - if self._alarm_state: - if (self._spinval_H, self._spinval_M) == (_OFF, _OFF): + if self._state_alarm: + if (self._state_spinval_H, self._state_spinval_M) == (_OFF, _OFF): # suggest wake up time, on the basis of 7h30m of sleep + time to fall asleep (H, M) = wasp.watch.rtc.get_localtime()[3:5] M += 30 + _TIME_TO_FALL_ASLEEP while M % 5 != 0: M += 1 - self._spinval_H = ((H + 7) % 24 + (M // 60)) % 24 - self._spinval_M = M % 60 + self._state_spinval_H = ((H + 7) % 24 + (M // 60)) % 24 + self._state_spinval_M = M % 60 self._spin_H = widgets.Spinner(30, 70, 0, 23, 2) - self._spin_H.value = self._spinval_H + self._spin_H.value = self._state_spinval_H self._spin_H.draw() self._spin_M = widgets.Spinner(150, 70, 0, 59, 2, 5) - self._spin_M.value = self._spinval_M + self._spin_M.value = self._state_spinval_M self._spin_M.draw() - if self._alarm_state: + if self._state_alarm: self._draw_duration(draw) draw.reset() elif self._page == _SETTINGS2: - if self._alarm_state: + if self._state_alarm: self.check_grad = widgets.Checkbox(0, 80, "Gradual wake") - self.check_grad.state = self._grad_alarm_state + self.check_grad.state = self._state_gradual_wake self.check_grad.draw() self.check_track = widgets.Checkbox(x=0, y=120, label="Body tracking") - self.check_track.state = self._tracking_enabled_state + self.check_track.state = self._state_body_tracking self.check_track.draw() - if self._tracking_enabled_state: + if self._state_body_tracking: self.check_smart = widgets.Checkbox(x=0, y=160, label="Smart alarm (alpha)") - self.check_smart.state = self._smart_alarm_state + self.check_smart.state = self._state_smart_alarm self.check_smart.draw() draw.reset() self.btn_HR = widgets.Checkbox(x=0, y=40, label="Heart rate tracking") - self.btn_HR.state = self._track_HR_state + self.btn_HR.state = self._state_HR_tracking self.btn_HR.draw() self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start") self.btn_sta.draw() @@ -316,7 +317,7 @@ class SleepTkApp(): self._spin_M = None del self.check_al, self.check_smart, self.check_track, self.check_grad, self.btn_sta, self.btn_al, self.btn_off, self.btn_HR, self._spin_H, self._spin_M - self._is_tracking = True + self._currently_tracking = True # accel data not yet written to disk: self._data_point_nb = 0 # total number of data points so far @@ -331,34 +332,35 @@ class SleepTkApp(): f.close() # if enabled, add alarm to log accel data in _FREQ seconds - if self._tracking_enabled_state: + if self._state_body_tracking: self.next_al = wasp.watch.rtc.time() + _FREQ wasp.system.set_alarm(self.next_al, self._trackOnce) else: self.next_al = None - if self._grad_alarm_state and not self._alarm_state: + if self._state_gradual_wake and not self._state_alarm: # fix incompatible settings - self._grad_alarm_state = _OFF + self._state_gradual_wake = _OFF # setting up alarm - if self._alarm_state: - self._WU_t = self._read_time(self._spinval_H, self._spinval_M) + if self._state_alarm: + self._old_notification_level = wasp.system.notify_level + self._WU_t = self._read_time(self._state_spinval_H, self._state_spinval_M) wasp.system.set_alarm(self._WU_t, self._listen_to_ticks) # also set alarm to vibrate a tiny bit before wake up time # to wake up gradually - if self._grad_alarm_state: + if self._state_gradual_wake: for t in _GRADUAL_WAKE: wasp.system.set_alarm(self._WU_t - t*60, self._tiny_vibration) # wake up SleepTk 2min before earliest possible wake up - if self._smart_alarm_state: + if self._state_smart_alarm: self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 wasp.system.set_alarm(self._WU_a, self._smart_alarm_start) # don't track heart rate right away, wait a few seconds - if self._track_HR_state: + if self._state_HR_tracking: self._last_HR_date = int(wasp.watch.rtc.time()) + 10 wasp.system.notify_level = 1 # silent notifications self._page = _TRACKING @@ -366,8 +368,8 @@ class SleepTkApp(): def _read_time(self, HH, MM): "convert time from spinners to seconds" (Y, Mo, d, h, m) = wasp.watch.rtc.get_localtime()[0:5] - HH = self._spinval_H - MM = self._spinval_M + HH = self._state_spinval_H + MM = self._state_spinval_M if HH < h or (HH == h and MM <= m): d += 1 return wasp.watch.time.mktime((Y, Mo, d, HH, MM, 0, 0, 0, 0)) @@ -375,27 +377,27 @@ class SleepTkApp(): def _disable_tracking(self, keep_main_alarm=False): """called by touching "STOP TRACKING" or when computing best alarm time to wake up you disables tracking features and alarms""" - self._is_tracking = False + self._currently_tracking = False if self.next_al: wasp.system.cancel_alarm(self.next_al, self._trackOnce) - if self._alarm_state: + if self._state_alarm: if keep_main_alarm is False: # to keep the alarm when stopping because of low battery wasp.system.cancel_alarm(self._WU_t, self._listen_to_ticks) for t in _GRADUAL_WAKE: wasp.system.cancel_alarm(self._WU_t - t*60, self._tiny_vibration) - if self._smart_alarm_state: + if self._state_smart_alarm: wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_start) - self._smart_alarm_state = _OFF + self._state_smart_alarm = _OFF wasp.watch.hrs.disable() self._periodicSave() - self._track_HR_state = _OFF + self._state_HR_tracking = _OFF wasp.gc.collect() def _trackOnce(self): """get one data point of accelerometer every _FREQ seconds, keep the average of each axis then store in a file every _STORE_FREQ seconds""" - if self._is_tracking: + if self._currently_tracking: buff = self._buff xyz = wasp.watch.accel.read_xyz() if xyz == (0, 0, 0): @@ -420,7 +422,7 @@ class SleepTkApp(): "body": "Stopped \ tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ on.".format(h, m, _BATTERY_THRESHOLD)}) - elif self._track_HR_state: + elif self._state_HR_tracking: if wasp.watch.rtc.time() - self._last_HR_date > _HR_FREQ and not self._track_HR_once: mute = wasp.watch.display.mute mute(True) From 61c7d108234fd0f83fcc832dbe4acd1865254704 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:39:19 +0200 Subject: [PATCH 397/485] renamed a lot of var 2 --- SleepTk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 116837a..35f2d5f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -346,7 +346,7 @@ class SleepTkApp(): if self._state_alarm: self._old_notification_level = wasp.system.notify_level self._WU_t = self._read_time(self._state_spinval_H, self._state_spinval_M) - wasp.system.set_alarm(self._WU_t, self._listen_to_ticks) + wasp.system.set_alarm(self._WU_t, self._activate_ticks_to_ring) # also set alarm to vibrate a tiny bit before wake up time # to wake up gradually @@ -382,7 +382,7 @@ class SleepTkApp(): wasp.system.cancel_alarm(self.next_al, self._trackOnce) if self._state_alarm: if keep_main_alarm is False: # to keep the alarm when stopping because of low battery - wasp.system.cancel_alarm(self._WU_t, self._listen_to_ticks) + wasp.system.cancel_alarm(self._WU_t, self._activate_ticks_to_ring) for t in _GRADUAL_WAKE: wasp.system.cancel_alarm(self._WU_t - t*60, self._tiny_vibration) if self._state_smart_alarm: @@ -467,7 +467,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._last_checkpoint = self._data_point_nb wasp.gc.collect() - def _listen_to_ticks(self): + def _activate_ticks_to_ring(self): """listen to ticks every second, telling the watch to vibrate""" wasp.gc.collect() wasp.system.notify_level = self._old_notification_level # restore notification level @@ -601,7 +601,7 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) # add new alarm wasp.system.set_alarm(max(WU_t - earlier, int(wasp.watch.rtc.time()) + 3), # not before right now, to make sure it rings - self.sleeptk._listen_to_ticks) + self.sleeptk._activate_ticks_to_ring) # replace old gentle alarm by another one if self.sleeptk._grad_alarm_state: From 42b87c29255afe3f3a9ba21c5689f595e36350da Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:51:24 +0200 Subject: [PATCH 398/485] fix: button management --- SleepTk.py | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 35f2d5f..c19024b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -174,28 +174,31 @@ class SleepTkApp(): self._state_alarm = self.check_al.state self.check_al.update() elif self._page == _SETTINGS2: - if self._state_alarm: - if self.check_smart.touch(event): - self._state_smart_alarm = self.check_smart.state - self.check_smart.draw() + if self._state_body_tracking: + if self.btn_HR.touch(event): + self.btn_HR.draw() + self._state_HR_tracking = self.btn_HR.state return - elif self.check_grad.touch(event): + if self._state_alarm: + if self.check_grad.touch(event): self._state_gradual_wake = self.check_grad.state self.check_grad.draw() return + if self._state_body_tracking: + if self.check_smart.touch(event): + self._state_smart_alarm = self.check_smart.state + self.check_smart.draw() + return if self.btn_sta.touch(event): draw.fill() draw.string("Loading", 0, 100) self._start_tracking() - elif self.btn_HR.touch(event): - self.btn_HR.draw() - self._state_HR_tracking = self.btn_HR.state - return - elif self.check_track.touch(event): - self._state_body_tracking = self.check_track.state - self.check_track.draw() + elif self.check_body_tracking.touch(event): + self._state_body_tracking = self.check_body_tracking.state + self.check_body_tracking.draw() if not self._state_body_tracking: self._state_smart_alarm = _OFF + self._state_HR_tracking = _OFF self._draw() def _draw_duration(self, draw): @@ -281,21 +284,21 @@ class SleepTkApp(): self._draw_duration(draw) draw.reset() elif self._page == _SETTINGS2: + self.check_body_tracking = widgets.Checkbox(x=0, y=40, label="Body tracking") + self.check_body_tracking.state = self._state_body_tracking + self.check_body_tracking.draw() + if self._state_body_tracking: + self.btn_HR = widgets.Checkbox(x=0, y=80, label="Heart rate tracking") + self.btn_HR.state = self._state_HR_tracking + self.btn_HR.draw() if self._state_alarm: - self.check_grad = widgets.Checkbox(0, 80, "Gradual wake") + self.check_grad = widgets.Checkbox(0, 120, "Gradual wake") self.check_grad.state = self._state_gradual_wake self.check_grad.draw() - self.check_track = widgets.Checkbox(x=0, y=120, label="Body tracking") - self.check_track.state = self._state_body_tracking - self.check_track.draw() if self._state_body_tracking: self.check_smart = widgets.Checkbox(x=0, y=160, label="Smart alarm (alpha)") self.check_smart.state = self._state_smart_alarm self.check_smart.draw() - draw.reset() - self.btn_HR = widgets.Checkbox(x=0, y=40, label="Heart rate tracking") - self.btn_HR.state = self._state_HR_tracking - self.btn_HR.draw() self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start") self.btn_sta.draw() draw.reset() @@ -307,7 +310,7 @@ class SleepTkApp(): # save some memory self.check_al = None self.check_smart = None - self.check_track = None + self.check_body_tracking = None self.check_grad = None self.btn_sta = None self.btn_al = None @@ -315,7 +318,7 @@ class SleepTkApp(): self.btn_HR = None self._spin_H = None self._spin_M = None - del self.check_al, self.check_smart, self.check_track, self.check_grad, self.btn_sta, self.btn_al, self.btn_off, self.btn_HR, self._spin_H, self._spin_M + del self.check_al, self.check_smart, self.check_body_tracking, self.check_grad, self.btn_sta, self.btn_al, self.btn_off, self.btn_HR, self._spin_H, self._spin_M self._currently_tracking = True From 8b4b80ae7710b9ac3981684f9b6f6ccc2a222414 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:53:47 +0200 Subject: [PATCH 399/485] fix: sleeptk was returning to home for some reason --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index c19024b..4fb16ca 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -522,6 +522,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._last_HR = "?" self._hrdata = None self._last_HR_printed = self._last_HR + wasp.system.switch(self) def _subtick(self, ticks): """track heart rate at 24Hz""" From 7c0c5550ea90352c312126a92bd01dce5c9e9302 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 17:59:20 +0200 Subject: [PATCH 400/485] new: if updating HR too often averages the values instead of erasing it --- SleepTk.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4fb16ca..56b0c93 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -511,7 +511,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) if len(self._hrdata.data) >= 720: # 30 seconds passed bpm = self._hrdata.get_heart_rate() if bpm < 150 and bpm > 30: - self._last_HR = bpm + # if HR was already computed since last periodicSave, + # then average the two values + if self._last_HR != _OFF and self._last_HR != "?": + self._last_HR = (int(self._last_HR) + bpm) // 2 + else: + self._last_HR = bpm self._last_HR_date = int(wasp.watch.rtc.time()) self._track_HR_once = _OFF self._hrdata = None From 6e051df404bafa1e1ea98d3a4a32f5fb7c6c640c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:01:18 +0200 Subject: [PATCH 401/485] pressing button while tracking does nothing instead of returning to home --- SleepTk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 56b0c93..4068358 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -125,6 +125,9 @@ class SleepTkApp(): if self._page == _RINGING: self._disable_tracking() self._page = _SETTINGS1 + elif self._page == _TRACKING: + # disable pressing to exit, use swipe up instead + self._draw() else: wasp.system.navigate(wasp.EventType.HOME) From 17b52302c3d3e41376654bd92401a2c903d92912 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:06:34 +0200 Subject: [PATCH 402/485] docs: heart tracking is not in alpha anymore --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 501b8dc..e109ac3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ * **Suggests best alarm time**: suggests wake up time according to average sleep cycles length. * **Gradual wake**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness. * **Smart alarm clock (alpha)**: adaptative alarm that wakes you at the best time of your sleep cycle (up to 40 minutes before the set time) to make sure you wake up feeling refreshed. -* **Heart tracking (alpha)**: tracks your heart rate throughout the night. +* **Heart tracking**: tracks your heart rate throughout the night. * **Privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download it if needed). * **Open source** From 31681f85a33d7c384bc6bddc32ca6a9ad3b786d0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:06:47 +0200 Subject: [PATCH 403/485] docs: mention insomnia insights --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e109ac3..f7247d9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ * **Gradual wake**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness. * **Smart alarm clock (alpha)**: adaptative alarm that wakes you at the best time of your sleep cycle (up to 40 minutes before the set time) to make sure you wake up feeling refreshed. * **Heart tracking**: tracks your heart rate throughout the night. +* **Insomnia insights**: if you turn on the screen during the night, SleepTk will tell you how long you slept and in what part of the sleep cycle you are supposed to be. * **Privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download it if needed). * **Open source** From b9a1718d9b0a8d60c2e0e0283c17f765deba519c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:22:07 +0200 Subject: [PATCH 404/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f7247d9..a3186d4 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ ## TODO **misc** +* greatly simplify the code by simply adding a large tick function every second instead of managing tons of counters. * investigate adding snooze feature like in the original `alarms.py` app * investigate adding a simple feature to wake you up only after a certain movement threshold was passed * add a "nap tracking" mode that records sleep tracking with more precision From eeb9eddb701f1a35f0c3932d9a6daec71eee4b09 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:22:13 +0200 Subject: [PATCH 405/485] minor --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 4068358..e2f3522 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -257,7 +257,7 @@ class SleepTkApp(): draw.string("(Currently tracking HR)", 0, 170) if self._state_HR_tracking: draw.string("HR:{}".format(self._last_HR_printed), 160, 170) - self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop tracking") + self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop") self.btn_off.draw() draw.reset() self._draw_duration(draw) From 1b3a0651e8f3a0fde19b92979e88a8e123c10a9e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:24:44 +0200 Subject: [PATCH 406/485] new: consider HR normal if between 30 and 100 --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index e2f3522..c043faf 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -513,7 +513,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.keep_awake() if len(self._hrdata.data) >= 720: # 30 seconds passed bpm = self._hrdata.get_heart_rate() - if bpm < 150 and bpm > 30: + if bpm < 100 and bpm > 40: # if HR was already computed since last periodicSave, # then average the two values if self._last_HR != _OFF and self._last_HR != "?": From 41d557c44c9fe3d7c48ae63a87c60b8de3f0fc2e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:25:33 +0200 Subject: [PATCH 407/485] minor: comments --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index c043faf..3789657 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -50,7 +50,7 @@ _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have t # user might want to edit this: _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_HR_FREQ = const(600) # how many seconds between heart rate data +_HR_FREQ = const(600) # how many seconds between heart rate data, this has to be at least 120 _STORE_FREQ = const(300) # process data and store to file every X seconds _BATTERY_THRESHOLD = const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set From 6e758dca110cdc804170f0a4f6b7407c3445ff33 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:45:15 +0200 Subject: [PATCH 408/485] minor: comments --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 3789657..842510d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -76,7 +76,7 @@ class SleepTkApp(): self._state_spinval_M = _OFF self._hrdata = None - self._last_HR = _OFF + self._last_HR = _OFF # if _OFF, no HR to write, if "?": error during last HR, else: heart rate self._last_HR_printed = "?" self._last_HR_date = _OFF self._track_HR_once = _OFF From da84052a0988cba14d69659766784a2c6e00f0b5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:45:30 +0200 Subject: [PATCH 409/485] fix: cast last HR to str before saving --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 842510d..5c05266 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -455,7 +455,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff[1] /= n buff[2] /= n if self._last_HR != _OFF: - bpm = ",{}".format(self._last_HR) + bpm = ",{}".format(str(self._last_HR)) self._last_HR = _OFF else: bpm = "" From fe7ed1f99a87c8f2bed97abd23f1f76d8cb98710 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 22 Apr 2022 18:48:20 +0200 Subject: [PATCH 410/485] new: more robust handling of bpm --- SleepTk.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 5c05266..0b0090e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -512,23 +512,23 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.keep_awake() if len(self._hrdata.data) >= 720: # 30 seconds passed - bpm = self._hrdata.get_heart_rate() - if bpm < 100 and bpm > 40: + bpm = int(self._hrdata.get_heart_rate()) + if not isinstance(int, bpm): + # in case of invalid data, write it in the file but + # keep trying to read HR + self._last_HR = "?" + self._hrdata = None + elif bpm < 100 and bpm > 40: # if HR was already computed since last periodicSave, # then average the two values - if self._last_HR != _OFF and self._last_HR != "?": - self._last_HR = (int(self._last_HR) + bpm) // 2 + if self._last_HR != _OFF and self._last_HR != "?" and isinstance(int, self._last_HR): + self._last_HR = (self._last_HR + bpm) // 2 else: self._last_HR = bpm self._last_HR_date = int(wasp.watch.rtc.time()) self._track_HR_once = _OFF self._hrdata = None wasp.watch.hrs.disable() - else: - # in case of invalid data, write it in the file but - # keep trying to read HR - self._last_HR = "?" - self._hrdata = None self._last_HR_printed = self._last_HR wasp.system.switch(self) From b00ae8cb4b6cd6aaa5ccb6aa8bacbdc332ef600d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 13:20:03 +0200 Subject: [PATCH 411/485] new: red font to reduce eye strain --- SleepTk.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 0b0090e..bd6f241 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -46,6 +46,7 @@ _RINGING = const(1) _SETTINGS1 = const(2) _SETTINGS2 = const(3) _FONT = fonts.sans18 +_FONT_COLOR = const(0xf800) # red font to reduce eye strain at night _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date # user might want to edit this: @@ -211,6 +212,7 @@ class SleepTkApp(): assert duration >= _TIME_TO_FALL_ASLEEP y = 180 elif self._page == _TRACKING: + draw.set_color(_FONT_COLOR) duration = (wasp.watch.rtc.time() - self._offset) / 60 # time slept y = 130 @@ -231,6 +233,7 @@ class SleepTkApp(): draw = wasp.watch.drawable draw.fill(0) draw.set_font(_FONT) + draw.set_color(_FONT_COLOR) if self._page == _RINGING: if self._smart_offset != 0: msg = "WAKE UP ({}m early)".format(str(self._smart_offset/60)[0:2]) @@ -258,7 +261,7 @@ class SleepTkApp(): if self._state_HR_tracking: draw.string("HR:{}".format(self._last_HR_printed), 160, 170) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop") - self.btn_off.draw() + self.btn_off.update(txt=_FONT_COLOR, frame=0, bg=0) draw.reset() self._draw_duration(draw) elif self._page == _SETTINGS1: From 3aad2490a168738788dca936a42a7218e07a3189 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 13:21:45 +0200 Subject: [PATCH 412/485] update screenshots --- screenshots/settings_page2.png | Bin 6927 -> 7589 bytes screenshots/tracking_page.png | Bin 7297 -> 7358 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/screenshots/settings_page2.png b/screenshots/settings_page2.png index e3ab768fff7d2c4aa9b291ac3051495b0830d849..62fcf71186540058f74a63c3001e9e40a840d9ce 100644 GIT binary patch literal 7589 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 literal 6927 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 diff --git a/screenshots/tracking_page.png b/screenshots/tracking_page.png index cb85955ee3565a4444181d9dc7f9e21ff5de01bb..6f354bce3ca740184d94d18c2fb60815136c76ef 100644 GIT binary patch literal 7358 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 KR;pC63i~(ZD#KX- From 7e88ff8929812ca435973b24b0caf5f10689e89f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 13:34:24 +0200 Subject: [PATCH 413/485] new: put watch back to sleep after vibrating or tracking heart rate --- SleepTk.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index bd6f241..0178715 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -431,15 +431,18 @@ class SleepTkApp(): "body": "Stopped \ tracking sleep at {}h{}m because your battery went below {}%. Alarm kept \ on.".format(h, m, _BATTERY_THRESHOLD)}) - elif self._state_HR_tracking: - if wasp.watch.rtc.time() - self._last_HR_date > _HR_FREQ and not self._track_HR_once: - mute = wasp.watch.display.mute - mute(True) - wasp.system.wake() - mute(True) - wasp.system.switch(self) - self._track_HR_once = _ON - wasp.system.request_tick(1000 // 8) + elif self._state_HR_tracking and \ + wasp.watch.rtc.time() - self._last_HR_date > _HR_FREQ and \ + not self._track_HR_once: + mute = wasp.watch.display.mute + mute(True) + wasp.system.wake() + mute(True) + wasp.system.switch(self) + self._track_HR_once = _ON + wasp.system.request_tick(1000 // 8) + else: + wasp.system.sleep() wasp.gc.collect() @@ -532,6 +535,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._track_HR_once = _OFF self._hrdata = None wasp.watch.hrs.disable() + wasp.system.sleep() self._last_HR_printed = self._last_HR wasp.system.switch(self) @@ -548,6 +552,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.wake() wasp.system.switch(self) wasp.watch.vibrator.pulse(duty=60, ms=100) + if not self._track_HR_once: + wasp.system.sleep() def _smart_alarm_start(self): SmartAlarm(self) @@ -631,6 +637,8 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) "title": "Finished smart alarm computation", "body": "Finished computing best wake up time in {:2f}s. Sleep cycle: {:.2f}h".format(wasp.watch.rtc.time() - start_time, cycle) }) + if not self.sleeptk._track_HR_once: + wasp.system.sleep() except Exception as e: wasp.gc.collect() h, m = wasp.watch.time.localtime(wasp.watch.rtc.time())[3:5] From 19470cf07cd40bfce0cce4d2272a444bf2ea4573 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 13:34:42 +0200 Subject: [PATCH 414/485] new: add tqdm to scripts --- pull_sleep_data.py | 3 ++- rm_sleep_data.py | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pull_sleep_data.py b/pull_sleep_data.py index 7a78d75..edbd94b 100644 --- a/pull_sleep_data.py +++ b/pull_sleep_data.py @@ -6,6 +6,7 @@ import os import subprocess import shlex import re +from tqdm import tqdm mode = "all" # download "all" files or only "latest" @@ -30,7 +31,7 @@ else: raise Exception("Wrong value for 'mode'") print("\n\n") -for fi in to_dl: +for fi in tqdm(to_dl): if os.path.exists(f"./logs/sleep/{fi}"): print(f"Skipping file {fi}: already exists") else: diff --git a/rm_sleep_data.py b/rm_sleep_data.py index e4dcb9b..5867413 100644 --- a/rm_sleep_data.py +++ b/rm_sleep_data.py @@ -4,6 +4,7 @@ import time import subprocess import shlex import re +from tqdm import tqdm print("\n\nRunning gc.collect()...") mem_cmd = './tools/wasptool --verbose --eval \'wasp.gc.collect()\'' @@ -20,19 +21,19 @@ reset_cmd = './tools/wasptool --verbose --reset' to_rm = files print("\n\n") -for fi in to_rm: - print(f"Removing file '{fi}'") +for fi in tqdm(to_rm): + tqdm.write(f"Removing file '{fi}'") rm_cmd = f'./tools/wasptool --verbose --eval \'from shell import rm ; rm(\"logs/sleep/{fi}\")\'' try: out = subprocess.check_output(shlex.split(rm_cmd)) if b"Watch reported error" in out: raise Exception("Watch reported error") - print(f"Succesfully removed to './logs/sleep/{fi}'") + tqdm.write(f"Succesfully removed to './logs/sleep/{fi}'") except Exception as e: - print(f"Error happened while removing {fi}") + tqdm.write(f"Error happened while removing {fi}") - print("Restarting watch.") + tqdm.write("Restarting watch.") out = subprocess.check_output(shlex.split(reset_cmd)) time.sleep(10) - print("\n\n") + tqdm.write("\n\n") From bbbe90bd7813f9b23adf7cec1c485101d9bbadb4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 13:59:48 +0200 Subject: [PATCH 415/485] feat: ability to snooze and make it harder to disable when ringing --- README.md | 1 - SleepTk.py | 28 ++++++++++++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a3186d4..9117158 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,6 @@ ## TODO **misc** * greatly simplify the code by simply adding a large tick function every second instead of managing tons of counters. -* investigate adding snooze feature like in the original `alarms.py` app * investigate adding a simple feature to wake you up only after a certain movement threshold was passed * add a "nap tracking" mode that records sleep tracking with more precision * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops diff --git a/SleepTk.py b/SleepTk.py index 0178715..9dc4fb2 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -50,6 +50,8 @@ _FONT_COLOR = const(0xf800) # red font to reduce eye strain at night _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date # user might want to edit this: +_STOP_LIMIT = const(10) # number of times to swipe or press the button to turn off ringing +_SNOOZE_TIME = const(300) # number of seconds to snooze for _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds _HR_FREQ = const(600) # how many seconds between heart rate data, this has to be at least 120 _STORE_FREQ = const(300) # process data and store to file every X seconds @@ -75,6 +77,7 @@ class SleepTkApp(): self._state_HR_tracking = _OFF self._state_spinval_H = _OFF self._state_spinval_M = _OFF + self.n_swipe = 0 # number of time swiped, to turn off alarm self._hrdata = None self._last_HR = _OFF # if _OFF, no HR to write, if "?": error during last HR, else: heart rate @@ -103,6 +106,7 @@ class SleepTkApp(): self._draw() wasp.system.request_event(wasp.EventMask.TOUCH | wasp.EventMask.SWIPE_LEFTRIGHT | + wasp.EventMask.SWIPE_UPDOWN | wasp.EventMask.BUTTON) if self._page == _TRACKING and self._track_HR_once: wasp.system.request_tick(1000 // 8) @@ -120,12 +124,21 @@ class SleepTkApp(): self._hrdata = None wasp.gc.collect() + def _try_stop_alarm(self): + """If button or swipe more than _STOP_LIMIT, then stop ringing""" + self.n_swipe += 1 + if self.n_swipe > _STOP_LIMIT: + self._disable_tracking() + self._page = _SETTINGS1 + draw = wasp.watch.drawable + draw.set_color(_FONT_COLOR) + draw.string("{} to stop".format(_STOP_LIMIT - self.n_swipe), 0, 70) + def press(self, button, state): "stop ringing alarm if pressed physical button" if state: if self._page == _RINGING: - self._disable_tracking() - self._page = _SETTINGS1 + self._try_stop_alarm() elif self._page == _TRACKING: # disable pressing to exit, use swipe up instead self._draw() @@ -142,6 +155,8 @@ class SleepTkApp(): if event[0] == wasp.EventType.RIGHT: self._page = _SETTINGS1 self._draw() + elif self._page == _RINGING: + self._try_stop_alarm() def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" @@ -163,8 +178,9 @@ class SleepTkApp(): draw.reset() elif self._page == _RINGING: if self.btn_al.touch(event): - self._disable_tracking() - self._page = _SETTINGS1 + self._page = _TRACKING + wasp.system.set_alarm(int(wasp.watch.rtc.time()) + _SNOOZE_TIME, self._activate_ticks_to_ring) + wasp.system.sleep() elif self._page == _SETTINGS1: if self._state_alarm and (self._spin_H.touch(event) or self._spin_M.touch(event)): self._state_spinval_H = self._spin_H.value @@ -239,8 +255,8 @@ class SleepTkApp(): msg = "WAKE UP ({}m early)".format(str(self._smart_offset/60)[0:2]) else: msg = "WAKE UP" - draw.string(msg, 0, 70) - self.btn_al = widgets.Button(x=0, y=70, w=240, h=140, label="WAKE UP") + draw.string(msg, 0, 50) + self.btn_al = widgets.Button(x=0, y=90, w=240, h=120, label="SNOOZE") self.btn_al.draw() draw.reset() elif self._page == _TRACKING: From 631ee530abb4171d1c6045f51e73c9c70a2c3639 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 14:00:11 +0200 Subject: [PATCH 416/485] new: faster screen redraw --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 9dc4fb2..12859f6 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -101,6 +101,8 @@ class SleepTkApp(): pass def foreground(self): + self.stat_bar = widgets.StatusBar() + self.stat_bar.clock = True self._conf_view = _OFF wasp.gc.collect() self._draw() @@ -248,6 +250,7 @@ class SleepTkApp(): """GUI""" draw = wasp.watch.drawable draw.fill(0) + self.stat_bar.draw() draw.set_font(_FONT) draw.set_color(_FONT_COLOR) if self._page == _RINGING: @@ -324,9 +327,6 @@ class SleepTkApp(): self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start") self.btn_sta.draw() draw.reset() - self.stat_bar = widgets.StatusBar() - self.stat_bar.clock = True - self.stat_bar.draw() def _start_tracking(self): # save some memory From 20df0355592f0e6e09dce91fafc533c3623b38a5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 14:00:26 +0200 Subject: [PATCH 417/485] minor fix --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 12859f6..df36086 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -255,7 +255,7 @@ class SleepTkApp(): draw.set_color(_FONT_COLOR) if self._page == _RINGING: if self._smart_offset != 0: - msg = "WAKE UP ({}m early)".format(str(self._smart_offset/60)[0:2]) + msg = "WAKE UP ({}m early)".format(str(self._smart_offset//60)[0:2]) else: msg = "WAKE UP" draw.string(msg, 0, 50) From 8131613a8e46ff2178d4672b01e6cd0c0cd39d30 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 14:02:34 +0200 Subject: [PATCH 418/485] renamed n_swipe + make it reset between run --- SleepTk.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index df36086..d9d791e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -77,7 +77,6 @@ class SleepTkApp(): self._state_HR_tracking = _OFF self._state_spinval_H = _OFF self._state_spinval_M = _OFF - self.n_swipe = 0 # number of time swiped, to turn off alarm self._hrdata = None self._last_HR = _OFF # if _OFF, no HR to write, if "?": error during last HR, else: heart rate @@ -114,6 +113,7 @@ class SleepTkApp(): wasp.system.request_tick(1000 // 8) def sleep(self): + self._stop_trial = 0 return True def wake(self): @@ -128,13 +128,13 @@ class SleepTkApp(): def _try_stop_alarm(self): """If button or swipe more than _STOP_LIMIT, then stop ringing""" - self.n_swipe += 1 - if self.n_swipe > _STOP_LIMIT: + self._stop_trial += 1 + if self._stop_trial > _STOP_LIMIT: self._disable_tracking() self._page = _SETTINGS1 draw = wasp.watch.drawable draw.set_color(_FONT_COLOR) - draw.string("{} to stop".format(_STOP_LIMIT - self.n_swipe), 0, 70) + draw.string("{} to stop".format(_STOP_LIMIT - self._stop_trial), 0, 70) def press(self, button, state): "stop ringing alarm if pressed physical button" @@ -389,6 +389,7 @@ class SleepTkApp(): self._last_HR_date = int(wasp.watch.rtc.time()) + 10 wasp.system.notify_level = 1 # silent notifications self._page = _TRACKING + self._stop_trial = 0 def _read_time(self, HH, MM): "convert time from spinners to seconds" From 9cb09327842d635f3467e8517181fe5ba91996b6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:16:02 +0200 Subject: [PATCH 419/485] fix: make sure to turn on the screen if drawing --- SleepTk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index d9d791e..8c1da54 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -248,6 +248,8 @@ class SleepTkApp(): def _draw(self): """GUI""" + mute = wasp.watch.display.mute + mute(False) draw = wasp.watch.drawable draw.fill(0) self.stat_bar.draw() From 641d3bcb2add97e765bac10b0740c0fced7e45b8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:16:11 +0200 Subject: [PATCH 420/485] minor: style --- SleepTk.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 8c1da54..6132c1c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -138,14 +138,15 @@ class SleepTkApp(): def press(self, button, state): "stop ringing alarm if pressed physical button" - if state: - if self._page == _RINGING: - self._try_stop_alarm() - elif self._page == _TRACKING: - # disable pressing to exit, use swipe up instead - self._draw() - else: - wasp.system.navigate(wasp.EventType.HOME) + if not state: + return + if self._page == _RINGING: + self._try_stop_alarm() + elif self._page == _TRACKING: + # disable pressing to exit, use swipe up instead + self._draw() + else: + wasp.system.navigate(wasp.EventType.HOME) def swipe(self, event): "navigate between settings page" From 46ba76aae91f1df69c71fdf79bcd5a0dd792faa5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:16:29 +0200 Subject: [PATCH 421/485] fix: handle swipe gestures better --- SleepTk.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 6132c1c..b6c3dd9 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -154,12 +154,18 @@ class SleepTkApp(): if event[0] == wasp.EventType.LEFT: self._page = _SETTINGS2 self._draw() + else: + return True elif self._page == _SETTINGS2: if event[0] == wasp.EventType.RIGHT: self._page = _SETTINGS1 self._draw() + else: + return True elif self._page == _RINGING: self._try_stop_alarm() + else: + return True def touch(self, event): """either start trackign or disable it, draw the screen in all cases""" From f6d3c772fbcb623f8b27284afd0bbbbe9da3c12f Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:16:43 +0200 Subject: [PATCH 422/485] fix: don't redraw if touching duration --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index b6c3dd9..e8964db 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -202,6 +202,7 @@ class SleepTkApp(): elif self.check_al.touch(event): self._state_alarm = self.check_al.state self.check_al.update() + return elif self._page == _SETTINGS2: if self._state_body_tracking: if self.btn_HR.touch(event): From 189011039d75885bffea41fa1a6a7f5fab95d3f3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:17:03 +0200 Subject: [PATCH 423/485] fix: potential race condition --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index e8964db..ba1f3bb 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -461,12 +461,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) elif self._state_HR_tracking and \ wasp.watch.rtc.time() - self._last_HR_date > _HR_FREQ and \ not self._track_HR_once: + self._track_HR_once = _ON mute = wasp.watch.display.mute mute(True) wasp.system.wake() mute(True) wasp.system.switch(self) - self._track_HR_once = _ON wasp.system.request_tick(1000 // 8) else: wasp.system.sleep() From 28528f4f7ed536f354bd8c35eb9ba180b1a02dd4 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:17:38 +0200 Subject: [PATCH 424/485] fix: make sure not to sleep watch at the wrong time --- SleepTk.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index ba1f3bb..e9c9b62 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -468,8 +468,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) mute(True) wasp.system.switch(self) wasp.system.request_tick(1000 // 8) - else: - wasp.system.sleep() wasp.gc.collect() @@ -551,6 +549,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) # keep trying to read HR self._last_HR = "?" self._hrdata = None + self._last_HR_printed = self._last_HR elif bpm < 100 and bpm > 40: # if HR was already computed since last periodicSave, # then average the two values @@ -558,12 +557,12 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._last_HR = (self._last_HR + bpm) // 2 else: self._last_HR = bpm + self._last_HR_printed = self._last_HR self._last_HR_date = int(wasp.watch.rtc.time()) self._track_HR_once = _OFF self._hrdata = None wasp.watch.hrs.disable() - wasp.system.sleep() - self._last_HR_printed = self._last_HR + return wasp.system.sleep() wasp.system.switch(self) def _subtick(self, ticks): From a4f2dfba8816cae295633af4a81c53b010fb6b06 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:17:50 +0200 Subject: [PATCH 425/485] docs: better docstring --- SleepTk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index e9c9b62..a901b60 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -505,7 +505,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() def _activate_ticks_to_ring(self): - """listen to ticks every second, telling the watch to vibrate""" + """listen to ticks every second, telling the watch to vibrate and + completely wake the user up""" wasp.gc.collect() wasp.system.notify_level = self._old_notification_level # restore notification level self._page = _RINGING From 9ed2e9ccd02ff443d0481dbdfb6f4a5bcf1d7561 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:20:08 +0200 Subject: [PATCH 426/485] new: save space by deleting Smart Alarm if it's not started --- SleepTk.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index a901b60..6ab9e2f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -391,8 +391,13 @@ class SleepTkApp(): # wake up SleepTk 2min before earliest possible wake up if self._state_smart_alarm: + if not SmartAlarm in locals(): + raise Exception("SmartAlarm was removed in previous run to save memory. Restart the watch to use it.") self._WU_a = self._WU_t - _ANTICIPATE_ALLOWED - 120 wasp.system.set_alarm(self._WU_a, self._smart_alarm_start) + else: + SmartAlarm = None + del SmartAlarm # don't track heart rate right away, wait a few seconds if self._state_HR_tracking: From dbda0488fa575c1e60883981db04748c90640f0a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:23:59 +0200 Subject: [PATCH 427/485] docs: reorder items --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9117158..2bf2051 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ ## Features: * **Sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file. +* **Heart tracking**: tracks your heart rate throughout the night. * **Flexible**: does not make too many assumption regarding time to fall asleep, sleep cycle duration etc. SleepTk tries various data to see what fits best for your profile. If you still want to customize things, all the hardcoded and commented settings are easily accessible at the top of the file. -* **Suggests best alarm time**: suggests wake up time according to average sleep cycles length. +* **Privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download it if needed). +* **Completely open source** +* **Best alarm time suggestion**: suggests wake up time according to average sleep cycles length. * **Gradual wake**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness. * **Smart alarm clock (alpha)**: adaptative alarm that wakes you at the best time of your sleep cycle (up to 40 minutes before the set time) to make sure you wake up feeling refreshed. -* **Heart tracking**: tracks your heart rate throughout the night. * **Insomnia insights**: if you turn on the screen during the night, SleepTk will tell you how long you slept and in what part of the sleep cycle you are supposed to be. -* **Privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download it if needed). -* **Open source** ## Credits: * Many thanks to Emanuel Löffler (https://github.com/plan5) who kindly created the logo. From 576198da21f3b33ac96ff538944caae09ea8a8ae Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:24:10 +0200 Subject: [PATCH 428/485] docs: mention snooze feature --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2bf2051..bc06da2 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ * **Completely open source** * **Best alarm time suggestion**: suggests wake up time according to average sleep cycles length. * **Gradual wake**: vibrates the watch a tiny bit a few times before the alarm to lift you gently back to consciousness. +* **Easy to snooze but hard to stop** You have to swipe several times to make it stop, but can snooze easily. * **Smart alarm clock (alpha)**: adaptative alarm that wakes you at the best time of your sleep cycle (up to 40 minutes before the set time) to make sure you wake up feeling refreshed. * **Insomnia insights**: if you turn on the screen during the night, SleepTk will tell you how long you slept and in what part of the sleep cycle you are supposed to be. From 2f97bfbb9631d53cebf407a3e34a68e1ea69a67c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:26:49 +0200 Subject: [PATCH 429/485] fix: snooze time was not updated to tracking page --- SleepTk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 6ab9e2f..15b310d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -188,7 +188,8 @@ class SleepTkApp(): elif self._page == _RINGING: if self.btn_al.touch(event): self._page = _TRACKING - wasp.system.set_alarm(int(wasp.watch.rtc.time()) + _SNOOZE_TIME, self._activate_ticks_to_ring) + self._WU_t = int(wasp.watch.rtc.time()) + _SNOOZE_TIME + wasp.system.set_alarm(self._WU_t, self._activate_ticks_to_ring) wasp.system.sleep() elif self._page == _SETTINGS1: if self._state_alarm and (self._spin_H.touch(event) or self._spin_M.touch(event)): From 3aa6a22c835dfa5e0fcd55e6097d7d9e32293f9b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:58:48 +0200 Subject: [PATCH 430/485] fix: force screen to be on if user is interacting --- SleepTk.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 15b310d..c2c0437 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -140,6 +140,8 @@ class SleepTkApp(): "stop ringing alarm if pressed physical button" if not state: return + mute = wasp.watch.display.mute + mute(False) if self._page == _RINGING: self._try_stop_alarm() elif self._page == _TRACKING: @@ -150,6 +152,8 @@ class SleepTkApp(): def swipe(self, event): "navigate between settings page" + mute = wasp.watch.display.mute + mute(False) if self._page == _SETTINGS1: if event[0] == wasp.EventType.LEFT: self._page = _SETTINGS2 @@ -171,6 +175,8 @@ class SleepTkApp(): """either start trackign or disable it, draw the screen in all cases""" wasp.gc.collect() draw = wasp.watch.drawable + mute = wasp.watch.display.mute + mute(False) if self._page == _TRACKING: if self._conf_view is _OFF: if self.btn_off.touch(event): From 4964197e41abfc4a1b2dd29a1debcb0c208df125 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 16:59:35 +0200 Subject: [PATCH 431/485] fix: cancelling alarm when stopping snoozing --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index c2c0437..729b92f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -130,6 +130,7 @@ class SleepTkApp(): """If button or swipe more than _STOP_LIMIT, then stop ringing""" self._stop_trial += 1 if self._stop_trial > _STOP_LIMIT: + wasp.system.cancel_alarm(self._WU_t, self._activate_ticks_to_ring) self._disable_tracking() self._page = _SETTINGS1 draw = wasp.watch.drawable From 6fea95eafb840faf920a298d437418800d9cc3e1 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 17:28:34 +0200 Subject: [PATCH 432/485] fix: make HR tracking less memory hungry --- SleepTk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 729b92f..ae9c344 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -532,6 +532,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def tick(self, ticks): """vibrate to wake you up OR track heart rate using code from heart.py""" + wasp.gc.collect() if self._page == _RINGING: wasp.watch.vibrator.pulse(duty=50, ms=500) elif self._track_HR_once: @@ -556,7 +557,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del t wasp.system.keep_awake() - if len(self._hrdata.data) >= 720: # 30 seconds passed + if len(self._hrdata.data) >= 360: # 15 seconds passed bpm = int(self._hrdata.get_heart_rate()) if not isinstance(int, bpm): # in case of invalid data, write it in the file but From 158b7d95b304e1b443e8d2cda60af5880fdf8bcc Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 17:28:41 +0200 Subject: [PATCH 433/485] minor --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index ae9c344..39987b1 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -540,11 +540,11 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) if self._hrdata is None: self._hrdata = ppg.PPG(wasp.watch.hrs.read_hrs()) t = wasp.machine.Timer(id=1, period=8000000) - mute = wasp.watch.display.mute t.start() + mute = wasp.watch.display.mute wasp.system.keep_awake() - self._subtick(1) mute(True) + self._subtick(1) while t.time() < 41666: pass wasp.system.keep_awake() From 30fe0c1d9f0e1594a663b5d45795d7c34820b2c3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 17:53:28 +0200 Subject: [PATCH 434/485] fix: remove wake() as it is not needed I think --- SleepTk.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 39987b1..d2521d7 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -116,11 +116,6 @@ class SleepTkApp(): self._stop_trial = 0 return True - def wake(self): - self._draw() - if self._page == _TRACKING and self._track_HR_once: - wasp.system.request_tick(1000 // 8) - def background(self): wasp.watch.hrs.disable() self._hrdata = None From ef9cfe664a8eba591955c6fcbeea15b370356781 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 17:53:46 +0200 Subject: [PATCH 435/485] fix: handle case where heart rate read is None --- SleepTk.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index d2521d7..87d9866 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -553,8 +553,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.keep_awake() if len(self._hrdata.data) >= 360: # 15 seconds passed - bpm = int(self._hrdata.get_heart_rate()) - if not isinstance(int, bpm): + bpm = self._hrdata.get_heart_rate() + bpm = int(bpm) if bpm is not None else None + if bpm is None: # in case of invalid data, write it in the file but # keep trying to read HR self._last_HR = "?" From ac336f2736492869edee86aa9668b3598c334c41 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 17:54:12 +0200 Subject: [PATCH 436/485] fix: try to make sure app is on when ticking --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 87d9866..e8026a9 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -528,6 +528,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) def tick(self, ticks): """vibrate to wake you up OR track heart rate using code from heart.py""" wasp.gc.collect() + wasp.system.switch(self) if self._page == _RINGING: wasp.watch.vibrator.pulse(duty=50, ms=500) elif self._track_HR_once: @@ -574,7 +575,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._hrdata = None wasp.watch.hrs.disable() return wasp.system.sleep() - wasp.system.switch(self) def _subtick(self, ticks): """track heart rate at 24Hz""" From d577393abaf091d92a0a06aace113a8ae40d711e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 18:35:40 +0200 Subject: [PATCH 437/485] new: enable heart rate tracking by default --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index e8026a9..5dd8729 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -72,9 +72,9 @@ class SleepTkApp(): # default button state self._state_alarm = _ON self._state_body_tracking = _ON + self._state_HR_tracking = _ON self._state_gradual_wake = _ON self._state_smart_alarm = _OFF - self._state_HR_tracking = _OFF self._state_spinval_H = _OFF self._state_spinval_M = _OFF From e252c660d07e718be81db04d77cf5c0edf16c94e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 18:36:05 +0200 Subject: [PATCH 438/485] minor: style --- README.md | 2 +- SleepTk.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bc06da2..8a942e8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Goal:** privacy friendly sleep tracker with smart alarm for the [pinetime smartwatch](https://pine64.com/product/pinetime-smartwatch-sealed/) by Pine64, on python, to run on [wasp-os](https://github.com/daniel-thompson/wasp-os). ## Features: -* **Sleep tracking**: logs your movement during the night, infers your sleep cycle and write it all down in a `.csv` file. +* **Sleep tracking**: logs your body movement during the night, infers your sleep cycle and write it all down in a `.csv` file. * **Heart tracking**: tracks your heart rate throughout the night. * **Flexible**: does not make too many assumption regarding time to fall asleep, sleep cycle duration etc. SleepTk tries various data to see what fits best for your profile. If you still want to customize things, all the hardcoded and commented settings are easily accessible at the top of the file. * **Privacy friendly**: your data is not sent to anyone, it is stored and analyzed directly on the watch (but you can still download it if needed). diff --git a/SleepTk.py b/SleepTk.py index 5dd8729..5b9ad4d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -322,7 +322,7 @@ class SleepTkApp(): self._draw_duration(draw) draw.reset() elif self._page == _SETTINGS2: - self.check_body_tracking = widgets.Checkbox(x=0, y=40, label="Body tracking") + self.check_body_tracking = widgets.Checkbox(x=0, y=40, label="Movement tracking") self.check_body_tracking.state = self._state_body_tracking self.check_body_tracking.draw() if self._state_body_tracking: From 767c0a9ed96e50572ef406494dbc39f228cc7f74 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 19:00:59 +0200 Subject: [PATCH 439/485] minor: call collect when sleeping --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index 5b9ad4d..90c657c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -114,6 +114,7 @@ class SleepTkApp(): def sleep(self): self._stop_trial = 0 + wasp.gc.collect() return True def background(self): From 5740be7e0c9cc380ba7b0945b96f57fa2fe90204 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 19:01:19 +0200 Subject: [PATCH 440/485] new: request tick on foreground but disable ticks if not needed anymore --- SleepTk.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 90c657c..442a522 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -111,6 +111,8 @@ class SleepTkApp(): wasp.EventMask.BUTTON) if self._page == _TRACKING and self._track_HR_once: wasp.system.request_tick(1000 // 8) + else: + wasp.system.request_tick(1000) # tick at least once def sleep(self): self._stop_trial = 0 @@ -575,7 +577,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._track_HR_once = _OFF self._hrdata = None wasp.watch.hrs.disable() - return wasp.system.sleep() + wasp.system.sleep() + else: + wasp.system.sleep() # stop receiving ticks if not used def _subtick(self, ticks): """track heart rate at 24Hz""" From 576a0e75093a721e32e899df06220158beae4d13 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 19:14:17 +0200 Subject: [PATCH 441/485] feat: auto kill bluetooth while tracking --- SleepTk.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 442a522..e60a2f8 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -50,6 +50,7 @@ _FONT_COLOR = const(0xf800) # red font to reduce eye strain at night _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date # user might want to edit this: +_KILL_BT = const(1) # set to 0 to disable turning off bluetooth while tracking to save battery (you have to reboot the watch to reactivate BT) _STOP_LIMIT = const(10) # number of times to swipe or press the button to turn off ringing _SNOOZE_TIME = const(300) # number of seconds to snooze for _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds @@ -409,6 +410,13 @@ class SleepTkApp(): if self._state_HR_tracking: self._last_HR_date = int(wasp.watch.rtc.time()) + 10 wasp.system.notify_level = 1 # silent notifications + + # kill bluetooth + if _KILL_BT: + import ble + if ble.enabled(): + ble.disable() + self._page = _TRACKING self._stop_trial = 0 From 8fefcaa6272b6d74de657db7cc4be2907549e6c7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 19:16:20 +0200 Subject: [PATCH 442/485] fix: tweaking ticks was a bad idea --- SleepTk.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index e60a2f8..1c3c0c6 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -112,8 +112,6 @@ class SleepTkApp(): wasp.EventMask.BUTTON) if self._page == _TRACKING and self._track_HR_once: wasp.system.request_tick(1000 // 8) - else: - wasp.system.request_tick(1000) # tick at least once def sleep(self): self._stop_trial = 0 @@ -586,8 +584,6 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._hrdata = None wasp.watch.hrs.disable() wasp.system.sleep() - else: - wasp.system.sleep() # stop receiving ticks if not used def _subtick(self, ticks): """track heart rate at 24Hz""" From 6fa24dd9a58727fc918832030b377da777a0f238 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 23 Apr 2022 19:32:16 +0200 Subject: [PATCH 443/485] minor: better way to reset draw --- SleepTk.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 1c3c0c6..63824ed 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -296,7 +296,6 @@ class SleepTkApp(): draw.string("HR:{}".format(self._last_HR_printed), 160, 170) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop") self.btn_off.update(txt=_FONT_COLOR, frame=0, bg=0) - draw.reset() self._draw_duration(draw) elif self._page == _SETTINGS1: # reset spinval values between runs @@ -322,7 +321,6 @@ class SleepTkApp(): self._spin_M.draw() if self._state_alarm: self._draw_duration(draw) - draw.reset() elif self._page == _SETTINGS2: self.check_body_tracking = widgets.Checkbox(x=0, y=40, label="Movement tracking") self.check_body_tracking.state = self._state_body_tracking @@ -341,7 +339,7 @@ class SleepTkApp(): self.check_smart.draw() self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start") self.btn_sta.draw() - draw.reset() + draw.reset() def _start_tracking(self): # save some memory From 8ec294b48d028a20f3e92cc84535210191f13a71 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 13:53:24 +0200 Subject: [PATCH 444/485] renamed _offset to _track_start_time --- SleepTk.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 63824ed..61a5a40 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -244,7 +244,7 @@ class SleepTkApp(): y = 180 elif self._page == _TRACKING: draw.set_color(_FONT_COLOR) - duration = (wasp.watch.rtc.time() - self._offset) / 60 # time slept + duration = (wasp.watch.rtc.time() - self._track_start_time) / 60 # time slept y = 130 draw.string("Total sleep {:02d}h{:02d}m".format( @@ -278,7 +278,7 @@ class SleepTkApp(): self.btn_al.draw() draw.reset() elif self._page == _TRACKING: - ti = wasp.watch.time.localtime(self._offset) + ti = wasp.watch.time.localtime(self._track_start_time) draw.string('Began at {:02d}:{:02d}'.format(ti[3], ti[4]), 0, 50) if self._state_alarm: word = "Alarm at " @@ -360,11 +360,11 @@ class SleepTkApp(): # accel data not yet written to disk: self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file - self._offset = int(wasp.watch.rtc.time()) # makes output more compact + self._track_start_time = int(wasp.watch.rtc.time()) # makes output more compact wasp.watch.accel.reset() # create one file per recording session: - self.filep = "logs/sleep/{}.csv".format(str(self._offset + _TIMESTAMP)) + self.filep = "logs/sleep/{}.csv".format(str(self._track_start_time + _TIMESTAMP)) f = open(self.filep, "wb") f.write(b"") f.close() @@ -508,7 +508,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f = open(self.filep, "ab") f.write("{:7f},{}{}\n".format( math.atan(buff[2] / (buff[0]**2 + buff[1]**2))*180/3.1415926535, # estimated arm angle - int(wasp.watch.rtc.time() - self._offset), + int(wasp.watch.rtc.time() - self._track_start_time), bpm ).encode()) f.close() @@ -769,7 +769,7 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) # sleep cycle duration is the average time distance between those N peaks cycle = sum([x_maximas[i+1] - x_maximas[i] for i in range(len(x_maximas) -1)]) / N * _STORE_FREQ - last_peak = self.sleeptk._offset + x_maximas[-1] * _STORE_FREQ + last_peak = self.sleeptk._track_start_time + x_maximas[-1] * _STORE_FREQ WU_t = self.sleeptk._WU_t # check if too late, already woken up: @@ -780,6 +780,6 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) if last_peak + cycle < WU_t - _ANTICIPATE_ALLOWED: earlier = _ANTICIPATE_ALLOWED else: # will wake you up at computed time - earlier = last_peak - self.sleeptk._offset + cycle + earlier = last_peak - self.sleeptk._track_start_time + cycle wasp.system.keep_awake() return (earlier, cycle) From f7dbc28df7c4923c3d07e0b0e6c9f3c4e705a890 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 14:17:15 +0200 Subject: [PATCH 445/485] style: clearer comments for user settings --- SleepTk.py | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 61a5a40..46c8cca 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -49,18 +49,37 @@ _FONT = fonts.sans18 _FONT_COLOR = const(0xf800) # red font to reduce eye strain at night _TIMESTAMP = const(946684800) # unix time and time used by wasp os don't have the same reference date -# user might want to edit this: -_KILL_BT = const(1) # set to 0 to disable turning off bluetooth while tracking to save battery (you have to reboot the watch to reactivate BT) -_STOP_LIMIT = const(10) # number of times to swipe or press the button to turn off ringing -_SNOOZE_TIME = const(300) # number of seconds to snooze for -_FREQ = const(5) # get accelerometer data every X seconds, but process and store them only every _STORE_FREQ seconds -_HR_FREQ = const(600) # how many seconds between heart rate data, this has to be at least 120 -_STORE_FREQ = const(300) # process data and store to file every X seconds -_BATTERY_THRESHOLD = const(15) # under X% of battery, stop tracking and only keep the alarm, set at -200 or lower to disable -_ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set -_GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) # nb of minutes before alarm to send a tiny vibration to make a smoother wake up -_TIME_TO_FALL_ASLEEP = const(14) # in minutes, according to https://sleepyti.me/ -_CYCLE_LENGTH = const(90) # in minutes, default of 90 or 100, according to https://sleepyti.me/ # currently used only to display best wake up time, not to compute smart alarm! +## USER SETTINGS ################################# +_KILL_BT = const(1) +# set to 0 to disable turning off bluetooth while tracking to save battery +# (you have to reboot the watch to reactivate BT, default: 1) +_STOP_LIMIT = const(10) +# number of times to swipe or press the button to turn off ringing (default: 10) +_SNOOZE_TIME = const(300) +# number of seconds to snooze for (default: 5 minutes) +_FREQ = const(5) +# get accelerometer data every X seconds, but process and store them only +# every _STORE_FREQ seconds (default: 5) +_HR_FREQ = const(600) +# how many seconds between heart rate data (default: 600, minimum 120) +_STORE_FREQ = const(300) +# process data and store to file every X seconds (default: 300) +_BATTERY_THRESHOLD = const(15) +# under X% of battery, stop tracking and only keep the alarm, set at -200 +# or lower to disable (default: 15) +_ANTICIPATE_ALLOWED = const(2400) +# number of seconds SleepTk can wake you up before the alarm clock you set +# only relevant if smart_alarm is enabled. (default: 2400) +_GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) +# nb of minutes before alarm to send a tiny vibration, designed to wake +# you more gently. (default: array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) ) +_TIME_TO_FALL_ASLEEP = const(14) +# minutes you take to fall asleep (default: 14, according to https://sleepyti.me/) +_CYCLE_LENGTH = const(90) +# sleep cycle length in minutes. Currently used only to display best wake up +# time but not to compute smart alarm! (default: 90 or 100, according to +# https://sleepyti.me/) +################################################## class SleepTkApp(): From d3c8f0bc1291e260d05ccdf91c7e3069c844a0f6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 14:17:33 +0200 Subject: [PATCH 446/485] new: default suggested wake up time is user modifiable now --- SleepTk.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 46c8cca..0434a9d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -79,6 +79,10 @@ _CYCLE_LENGTH = const(90) # sleep cycle length in minutes. Currently used only to display best wake up # time but not to compute smart alarm! (default: 90 or 100, according to # https://sleepyti.me/) +_SLEEP_GOAL_H = const(7) +_SLEEP_GOAL_M = const(30) +# suggests to sleep for this amount of hours and this amount of minutes. +# (default: 7 30 for 7 hours and 30 minutes) ################################################## @@ -325,12 +329,12 @@ class SleepTkApp(): self.check_al.draw() if self._state_alarm: if (self._state_spinval_H, self._state_spinval_M) == (_OFF, _OFF): - # suggest wake up time, on the basis of 7h30m of sleep + time to fall asleep + # suggest wake up time, on the basis of desired sleep goal + time to fall asleep (H, M) = wasp.watch.rtc.get_localtime()[3:5] - M += 30 + _TIME_TO_FALL_ASLEEP + M += _SLEEP_GOAL_M + _TIME_TO_FALL_ASLEEP while M % 5 != 0: M += 1 - self._state_spinval_H = ((H + 7) % 24 + (M // 60)) % 24 + self._state_spinval_H = ((H + _SLEEP_GOAL_H) % 24 + (M // 60)) % 24 self._state_spinval_M = M % 60 self._spin_H = widgets.Spinner(30, 70, 0, 23, 2) self._spin_H.value = self._state_spinval_H From 872907a2576a1c9126607739b37f1b1fd895f41c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 14:18:10 +0200 Subject: [PATCH 447/485] better comment --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 0434a9d..cc71c67 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -82,7 +82,7 @@ _CYCLE_LENGTH = const(90) _SLEEP_GOAL_H = const(7) _SLEEP_GOAL_M = const(30) # suggests to sleep for this amount of hours and this amount of minutes. -# (default: 7 30 for 7 hours and 30 minutes) +# (default: 7 30 for 7 hours and 30 minutes, which is about 5 sleep cycle.) ################################################## From 68914d65050217b01d4fc454fd7c80329a73ed3e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 14:21:44 +0200 Subject: [PATCH 448/485] new: number of sleep cycle goal is now modifiable --- SleepTk.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index cc71c67..8d69164 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -79,10 +79,9 @@ _CYCLE_LENGTH = const(90) # sleep cycle length in minutes. Currently used only to display best wake up # time but not to compute smart alarm! (default: 90 or 100, according to # https://sleepyti.me/) -_SLEEP_GOAL_H = const(7) -_SLEEP_GOAL_M = const(30) -# suggests to sleep for this amount of hours and this amount of minutes. -# (default: 7 30 for 7 hours and 30 minutes, which is about 5 sleep cycle.) +_SLEEP_GOAL_CYCLE = const(5) +# number of sleep cycle you wish to sleep. With _CYCLE_LENGTH this is used +# to suggest best wake up time to user when setting the alarm. (default: 5) ################################################## @@ -331,10 +330,12 @@ class SleepTkApp(): if (self._state_spinval_H, self._state_spinval_M) == (_OFF, _OFF): # suggest wake up time, on the basis of desired sleep goal + time to fall asleep (H, M) = wasp.watch.rtc.get_localtime()[3:5] - M += _SLEEP_GOAL_M + _TIME_TO_FALL_ASLEEP + goal_h = _SLEEP_GOAL_CYCLE * _CYCLE_LENGTH // 60 + goal_m = _SLEEP_GOAL_CYCLE * _CYCLE_LENGTH % 60 + M += goal_m + _TIME_TO_FALL_ASLEEP while M % 5 != 0: M += 1 - self._state_spinval_H = ((H + _SLEEP_GOAL_H) % 24 + (M // 60)) % 24 + self._state_spinval_H = ((H + goal_h) % 24 + (M // 60)) % 24 self._state_spinval_M = M % 60 self._spin_H = widgets.Spinner(30, 70, 0, 23, 2) self._spin_H.value = self._state_spinval_H From ae474ea1d96c53ba020975e5620d7004d190eb87 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 14:25:57 +0200 Subject: [PATCH 449/485] docs: mention to edit settings and implement relevant commits --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8a942e8..3ff466b 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ ## **How to install**: *(for now you need my slightly forked wasp-os that allows to use accelerometer data)* -* download the latest [forked wasp-os](https://github.com/thiswillbeyourgithub/wasp-os) -* download the latest [SleepTk.py](./SleepTk.py) -* put the latest app in `wasp-os/wasp/apps/SleepTk.py` +* download the latest [forked wasp-os](https://github.com/thiswillbeyourgithub/wasp-os), or at least implement the relevant commits (09a5ff2). +* download the latest [SleepTk.py](./SleepTk.py) and put it in `wasp-os/wasp/apps/SleepTk.py` +* open `SleepTk.py` to edit the user settings (at the top of the file) * compile `wasp-os`: `make submodules && make softdevice && make BOARD=pinetime all && echo "SUCCESS"` * upload it to your pinetime: `./tools/ota-dfu/dfu.py -z build-pinetime/micropython.zip -a XX:XX:XX:XX:XX:XX --legacy` * reboot the watch and enjoy `SleepTk` From 911921784b0bd491fd2364a25f08cbe7af24331b Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 14:29:18 +0200 Subject: [PATCH 450/485] fix: reduce font for smart alarm button because too large for unmodified waspos --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 8d69164..111e5a5 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -358,7 +358,7 @@ class SleepTkApp(): self.check_grad.state = self._state_gradual_wake self.check_grad.draw() if self._state_body_tracking: - self.check_smart = widgets.Checkbox(x=0, y=160, label="Smart alarm (alpha)") + self.check_smart = widgets.Checkbox(x=0, y=160, label="Smart alarm") self.check_smart.state = self._state_smart_alarm self.check_smart.draw() self.btn_sta = widgets.Button(x=0, y=200, w=240, h=40, label="Start") From 206333dc670d5d253881647bf107d49e38f730b7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 15:02:26 +0200 Subject: [PATCH 451/485] update which commit is important --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ff466b..29b178c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ## **How to install**: *(for now you need my slightly forked wasp-os that allows to use accelerometer data)* -* download the latest [forked wasp-os](https://github.com/thiswillbeyourgithub/wasp-os), or at least implement the relevant commits (09a5ff2). +* download the latest [forked wasp-os](https://github.com/thiswillbeyourgithub/wasp-os), or at least implement the relevant commits (624d1c675343cd4d95caea5ac4cec32766b3119b). * download the latest [SleepTk.py](./SleepTk.py) and put it in `wasp-os/wasp/apps/SleepTk.py` * open `SleepTk.py` to edit the user settings (at the top of the file) * compile `wasp-os`: `make submodules && make softdevice && make BOARD=pinetime all && echo "SUCCESS"` From e297d37c34eb337c306cf33132869a1db7313181 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 15:05:58 +0200 Subject: [PATCH 452/485] docs: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 29b178c..7f459fa 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ ### Note to reader: * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) -* Status as of end of February 2022: *UI (**done**), regular alarm (**done**), smart alarm (**mostly done but untested**)* +* Status as of end of May 2022: *UI (**done**), regular alarm (**done**), heart tracking (**done** but might be a bit dodgy), smart alarm (**mostly done but untested**)* * you can download your sleep data file using the file `pull_sleep_data`. A suggested workflow to load it into [pandas](https://pypi.org/project/pandas/) can be found at the bottom of the page. * the notifications are set to "silent" during the tracking session and are restored to the previously used level when the alarm is ringing From 78d6a6ce0403347dc94e74e8659f5906e4898d5a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 15:06:19 +0200 Subject: [PATCH 453/485] docs: warn about bluetooth being turned off and the simulator having a hard time --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7f459fa..bdfab4a 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ * Status as of end of May 2022: *UI (**done**), regular alarm (**done**), heart tracking (**done** but might be a bit dodgy), smart alarm (**mostly done but untested**)* * you can download your sleep data file using the file `pull_sleep_data`. A suggested workflow to load it into [pandas](https://pypi.org/project/pandas/) can be found at the bottom of the page. * the notifications are set to "silent" during the tracking session and are restored to the previously used level when the alarm is ringing +* by default, the bluetooth is turned off when tracking to save battery. Wasp OS doesn't allow to turn it back on without restarting the watch. +* it seems the simulator is having a rough time with daylight saving mode or time management. I personnaly have a 1h offset between sleep estimation on the simulator compared to the pinetime, don't worry. # Screenshots: ![settings](./screenshots/settings_page.png) From f2e5914f30d10f819bef23ec86ee65b4b115d50d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 15:11:12 +0200 Subject: [PATCH 454/485] fix: reset last printed HR when starting tracking --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index 111e5a5..e03a295 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -385,6 +385,7 @@ class SleepTkApp(): self._data_point_nb = 0 # total number of data points so far self._last_checkpoint = 0 # to know when to save to file self._track_start_time = int(wasp.watch.rtc.time()) # makes output more compact + self._last_HR_printed = "?" wasp.watch.accel.reset() # create one file per recording session: From a05739b8569de953ed48f68b9483979ca3f3e3c0 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 15:24:23 +0200 Subject: [PATCH 455/485] todo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bdfab4a..0366ce0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ * investigate adding a simple feature to wake you up only after a certain movement threshold was passed * add a "nap tracking" mode that records sleep tracking with more precision * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops +* implement a simple Neural Network or decision tree to compute best wake up time, using one found in the scientific literature. * implement downsampling to 15 minutes precision to compute best wake up time * if self.foreground is called, record the time. Use it to cancel smart alarm if you woke up too many times (more than 2 times in more than 20 minutes apart). From c72c7aec7baa892e0de1b3d3f4cbe6fa96e1487a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 15:29:20 +0200 Subject: [PATCH 456/485] fix: when snoozing, disable HR tracking --- SleepTk.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index e03a295..326aa0b 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -213,6 +213,10 @@ class SleepTkApp(): draw.reset() elif self._page == _RINGING: if self.btn_al.touch(event): + if self._track_HR_once: # if currently tracking HR, stop + self._track_HR_once = _OFF + self._hrdata = None + wasp.watch.hrs.disable() self._page = _TRACKING self._WU_t = int(wasp.watch.rtc.time()) + _SNOOZE_TIME wasp.system.set_alarm(self._WU_t, self._activate_ticks_to_ring) From b62875eb5e433709f9bc871cc8fce21b860feb6c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 25 Apr 2022 15:31:12 +0200 Subject: [PATCH 457/485] renamed btn_al to btn_snooz --- SleepTk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 326aa0b..a31495d 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -212,7 +212,7 @@ class SleepTkApp(): self._conf_view = _OFF draw.reset() elif self._page == _RINGING: - if self.btn_al.touch(event): + if self.btn_snooz.touch(event): if self._track_HR_once: # if currently tracking HR, stop self._track_HR_once = _OFF self._hrdata = None @@ -300,8 +300,8 @@ class SleepTkApp(): else: msg = "WAKE UP" draw.string(msg, 0, 50) - self.btn_al = widgets.Button(x=0, y=90, w=240, h=120, label="SNOOZE") - self.btn_al.draw() + self.btn_snooz = widgets.Button(x=0, y=90, w=240, h=120, label="SNOOZE") + self.btn_snooz.draw() draw.reset() elif self._page == _TRACKING: ti = wasp.watch.time.localtime(self._track_start_time) @@ -376,12 +376,12 @@ class SleepTkApp(): self.check_body_tracking = None self.check_grad = None self.btn_sta = None - self.btn_al = None + self.btn_snooz = None self.btn_off = None self.btn_HR = None self._spin_H = None self._spin_M = None - del self.check_al, self.check_smart, self.check_body_tracking, self.check_grad, self.btn_sta, self.btn_al, self.btn_off, self.btn_HR, self._spin_H, self._spin_M + del self.check_al, self.check_smart, self.check_body_tracking, self.check_grad, self.btn_sta, self.btn_snooz, self.btn_off, self.btn_HR, self._spin_H, self._spin_M self._currently_tracking = True From dbd72798f2c7062cfd8b93d8cc10e07ad08031b6 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 26 Apr 2022 12:22:44 +0200 Subject: [PATCH 458/485] fix: forgot to take time to fall asleep into account --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index a31495d..47ac75e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -270,7 +270,7 @@ class SleepTkApp(): y = 180 elif self._page == _TRACKING: draw.set_color(_FONT_COLOR) - duration = (wasp.watch.rtc.time() - self._track_start_time) / 60 # time slept + duration = (wasp.watch.rtc.time() - self._track_start_time - _TIME_TO_FALL_ASLEEP) / 60 # time slept y = 130 draw.string("Total sleep {:02d}h{:02d}m".format( From 901de46037d689e6dace44a34dc13e0620844151 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 26 Apr 2022 12:23:57 +0200 Subject: [PATCH 459/485] fix: don't print sleep duration before user fell asleep --- SleepTk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SleepTk.py b/SleepTk.py index 47ac75e..856a22e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -271,6 +271,8 @@ class SleepTkApp(): elif self._page == _TRACKING: draw.set_color(_FONT_COLOR) duration = (wasp.watch.rtc.time() - self._track_start_time - _TIME_TO_FALL_ASLEEP) / 60 # time slept + if duration <= 0: # don't print when not yet asleep + return y = 130 draw.string("Total sleep {:02d}h{:02d}m".format( From 8d951d6c39ed02dfebd2e0882dfbb96eeb74e586 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 26 Apr 2022 12:23:57 +0200 Subject: [PATCH 460/485] fix: don't print sleep duration before user fell asleep --- SleepTk.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 47ac75e..db90d6e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -270,7 +270,9 @@ class SleepTkApp(): y = 180 elif self._page == _TRACKING: draw.set_color(_FONT_COLOR) - duration = (wasp.watch.rtc.time() - self._track_start_time - _TIME_TO_FALL_ASLEEP) / 60 # time slept + duration = (wasp.watch.rtc.time() - self._track_start_time) / 60 - _TIME_TO_FALL_ASLEEP # time slept + if duration <= 0: # don't print when not yet asleep + return y = 130 draw.string("Total sleep {:02d}h{:02d}m".format( From 82d1e6ae0bdcefc964eb3a9cf8a1f158d189ecca Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 27 Apr 2022 11:12:01 +0200 Subject: [PATCH 461/485] disable gradual wake by default --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index db90d6e..de6478e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -96,7 +96,7 @@ class SleepTkApp(): self._state_alarm = _ON self._state_body_tracking = _ON self._state_HR_tracking = _ON - self._state_gradual_wake = _ON + self._state_gradual_wake = _OFF self._state_smart_alarm = _OFF self._state_spinval_H = _OFF self._state_spinval_M = _OFF From 025342dd8fe7fa3ecc6764cd870f4279048e05b9 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 27 Apr 2022 22:05:44 +0200 Subject: [PATCH 462/485] add plotting instructions for heart rate --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0366ce0..6f14e6d 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,8 @@ fname = "./logs/sleep/YOUR_TIME.csv" import pandas as pd import plotly.express as plt -df = pd.read_csv(fname, names=["motion", "elapsed", "x_avg", "y_avg", "z_avg", "battery"]) +#df = pd.read_csv(fname, names=["motion", "elapsed", "x_avg", "y_avg", "z_avg", "battery"]) +df = pd.read_csv(fname, names=["motion", "elapsed", "heart_rate"]) start_time = int(fname.split("/")[-1].split(".csv")[0]) df["time"] = pd.to_datetime(df["elapsed"]+start_time, unit='s') @@ -112,6 +113,11 @@ fig.update_xaxes(type="date", tickformat="%H:%M" ) fig.show() + +df_HR = df.set_index("human_time")["heart_rate"] +df_HR = df_HR[~df_HR.isna()] +df_HR.plot() + ``` Now, to play around with the signal processing function: From 2b1b69f1b4bd7005b1115a806438b0ca2b4833a8 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 27 Apr 2022 22:06:43 +0200 Subject: [PATCH 463/485] autoreboot when downloading files + use tqdm --- pull_sleep_data.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pull_sleep_data.py b/pull_sleep_data.py index edbd94b..4ca55a7 100644 --- a/pull_sleep_data.py +++ b/pull_sleep_data.py @@ -9,6 +9,7 @@ import re from tqdm import tqdm mode = "all" # download "all" files or only "latest" +autoreboot = True # reboot between each download, to avoid memory issues print("\n\nRunning gc.collect()...") mem_cmd = './tools/wasptool --verbose --eval \'wasp.gc.collect()\'' @@ -33,20 +34,20 @@ else: print("\n\n") for fi in tqdm(to_dl): if os.path.exists(f"./logs/sleep/{fi}"): - print(f"Skipping file {fi}: already exists") + tqdm.write(f"Skipping file {fi}: already exists") else: - print(f"Downloading file '{fi}'") + tqdm.write(f"Downloading file '{fi}'") pull_cmd = f'./tools/wasptool --verbose --pull logs/sleep/{fi}' try: out = subprocess.check_output(shlex.split(pull_cmd)) if b"Watch reported error" in out: raise Exception("Watch reported error") - print(f"Succesfully downloaded to './logs/sleep/{fi}'") + tqdm.write(f"Succesfully downloaded to './logs/sleep/{fi}'") except Exception as e: - print(f"Error happened while downloading {fi}, deleting local incomplete file") + tqdm.write(f"Error happened while downloading {fi}, deleting local incomplete file") os.system(f"rm ./logs/sleep/{fi}") - if mode == "all": - print("Restarting watch.") + if mode == "all" and autoreboot: + tqdm.write("Restarting watch.") out = subprocess.check_output(shlex.split(reset_cmd)) time.sleep(10) From 9658416e4636c57f76a2349feeb5afa8167e7cfe Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 27 Apr 2022 22:08:38 +0200 Subject: [PATCH 464/485] track heart rate for only 10s --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index de6478e..415e48f 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -591,7 +591,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) del t wasp.system.keep_awake() - if len(self._hrdata.data) >= 360: # 15 seconds passed + if len(self._hrdata.data) >= 240: # 10 seconds passed bpm = self._hrdata.get_heart_rate() bpm = int(bpm) if bpm is not None else None if bpm is None: From 45d78a48cef133a507b2acf061c2437bd368b12c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 27 Apr 2022 22:08:49 +0200 Subject: [PATCH 465/485] track HR by default every 5 minutes --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 415e48f..766703a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -60,8 +60,8 @@ _SNOOZE_TIME = const(300) _FREQ = const(5) # get accelerometer data every X seconds, but process and store them only # every _STORE_FREQ seconds (default: 5) -_HR_FREQ = const(600) -# how many seconds between heart rate data (default: 600, minimum 120) +_HR_FREQ = const(300) +# how many seconds between heart rate data (default: 300, minimum 120) _STORE_FREQ = const(300) # process data and store to file every X seconds (default: 300) _BATTERY_THRESHOLD = const(15) From dd46d36a4030c755898ddcc3ad5ab0943b4e1528 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 28 Apr 2022 15:51:58 +0200 Subject: [PATCH 466/485] fix: keep awake when ringing --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index 766703a..1547d07 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -568,6 +568,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() wasp.system.switch(self) if self._page == _RINGING: + wasp.system.keep_awake() wasp.watch.vibrator.pulse(duty=50, ms=500) elif self._track_HR_once: wasp.watch.hrs.enable() From 061ab1fe4ef695784cffc712721460d21a636ac5 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Thu, 28 Apr 2022 15:52:34 +0200 Subject: [PATCH 467/485] set default cycle length to 85 minutes for testing --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 1547d07..1c33e72 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -75,7 +75,7 @@ _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) # you more gently. (default: array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) ) _TIME_TO_FALL_ASLEEP = const(14) # minutes you take to fall asleep (default: 14, according to https://sleepyti.me/) -_CYCLE_LENGTH = const(90) +_CYCLE_LENGTH = const(85) # sleep cycle length in minutes. Currently used only to display best wake up # time but not to compute smart alarm! (default: 90 or 100, according to # https://sleepyti.me/) From 4a7330aa6fe7ec142fd662ab653ab221aefe8c10 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 29 Apr 2022 09:11:07 +0200 Subject: [PATCH 468/485] fix: stop snoozing at stop limit --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 1c33e72..0fdad19 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -148,7 +148,7 @@ class SleepTkApp(): def _try_stop_alarm(self): """If button or swipe more than _STOP_LIMIT, then stop ringing""" self._stop_trial += 1 - if self._stop_trial > _STOP_LIMIT: + if self._stop_trial >= _STOP_LIMIT: wasp.system.cancel_alarm(self._WU_t, self._activate_ticks_to_ring) self._disable_tracking() self._page = _SETTINGS1 From f048c7522e1f141041b77f5c08588f585b6b236e Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 29 Apr 2022 09:11:18 +0200 Subject: [PATCH 469/485] fix: reset values after snoozing --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 0fdad19..0f6a9c3 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -151,7 +151,7 @@ class SleepTkApp(): if self._stop_trial >= _STOP_LIMIT: wasp.system.cancel_alarm(self._WU_t, self._activate_ticks_to_ring) self._disable_tracking() - self._page = _SETTINGS1 + self.__init__() draw = wasp.watch.drawable draw.set_color(_FONT_COLOR) draw.string("{} to stop".format(_STOP_LIMIT - self._stop_trial), 0, 70) From 5e1a7358b4df61fd3d846a07034d9786b7eeb50a Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 4 May 2022 10:58:35 +0200 Subject: [PATCH 470/485] set default cycle length back to 90 --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 0f6a9c3..c3c8d41 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -75,7 +75,7 @@ _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) # you more gently. (default: array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) ) _TIME_TO_FALL_ASLEEP = const(14) # minutes you take to fall asleep (default: 14, according to https://sleepyti.me/) -_CYCLE_LENGTH = const(85) +_CYCLE_LENGTH = const(90) # sleep cycle length in minutes. Currently used only to display best wake up # time but not to compute smart alarm! (default: 90 or 100, according to # https://sleepyti.me/) From b1dccb312606c2991f5e69c69ad9325b2905abb7 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 4 May 2022 10:59:36 +0200 Subject: [PATCH 471/485] fix: allow floating point gradual wake --- SleepTk.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index c3c8d41..8d69785 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -421,7 +421,7 @@ class SleepTkApp(): # to wake up gradually if self._state_gradual_wake: for t in _GRADUAL_WAKE: - wasp.system.set_alarm(self._WU_t - t*60, self._tiny_vibration) + wasp.system.set_alarm(self._WU_t - int(t*60), self._tiny_vibration) # wake up SleepTk 2min before earliest possible wake up if self._state_smart_alarm: @@ -466,7 +466,7 @@ class SleepTkApp(): if keep_main_alarm is False: # to keep the alarm when stopping because of low battery wasp.system.cancel_alarm(self._WU_t, self._activate_ticks_to_ring) for t in _GRADUAL_WAKE: - wasp.system.cancel_alarm(self._WU_t - t*60, self._tiny_vibration) + wasp.system.cancel_alarm(self._WU_t - int(t*60), self._tiny_vibration) if self._state_smart_alarm: wasp.system.cancel_alarm(self._WU_a, self._smart_alarm_start) self._state_smart_alarm = _OFF @@ -703,9 +703,9 @@ BY MISTAKE at {:02d}h{:02d}m".format(t[3], t[4])}) # replace old gentle alarm by another one if self.sleeptk._grad_alarm_state: for t in _GRADUAL_WAKE: - wasp.system.cancel_alarm(WU_t - t*60, self.sleeptk._tiny_vibration) - if earlier + t*60 < _ANTICIPATE_ALLOWED: - wasp.system.set_alarm(WU_t - earlier - t*60, self.sleeptk._tiny_vibration) + wasp.system.cancel_alarm(WU_t - int(t*60), self.sleeptk._tiny_vibration) + if earlier + int(t*60) < _ANTICIPATE_ALLOWED: + wasp.system.set_alarm(WU_t - earlier - int(t*60), self.sleeptk._tiny_vibration) self.sleeptk._earlier = earlier self.sleeptk._page = _TRACKING From 39904313952a937601059e9fe8db125d747d7eb3 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Wed, 4 May 2022 11:00:12 +0200 Subject: [PATCH 472/485] fix: don't turn screen of immediately if interacted less than 5s ago --- SleepTk.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 8d69785..e2395ba 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -112,6 +112,7 @@ class SleepTkApp(): self._conf_view = _OFF # confirmation view self._smart_offset = _OFF # number of seconds between the alarm you set manually and the smart alarm time self._buff = array("f", [_OFF, _OFF, _OFF]) # contains accelerometer values + self._last_touch = int(wasp.watch.rtc.time()) try: shell.mkdir("logs/") @@ -160,6 +161,7 @@ class SleepTkApp(): "stop ringing alarm if pressed physical button" if not state: return + self._last_touch = int(wasp.watch.rtc.time()) mute = wasp.watch.display.mute mute(False) if self._page == _RINGING: @@ -174,6 +176,7 @@ class SleepTkApp(): "navigate between settings page" mute = wasp.watch.display.mute mute(False) + self._last_touch = int(wasp.watch.rtc.time()) if self._page == _SETTINGS1: if event[0] == wasp.EventType.LEFT: self._page = _SETTINGS2 @@ -197,6 +200,7 @@ class SleepTkApp(): draw = wasp.watch.drawable mute = wasp.watch.display.mute mute(False) + self._last_touch = int(wasp.watch.rtc.time()) if self._page == _TRACKING: if self._conf_view is _OFF: if self.btn_off.touch(event): @@ -508,10 +512,10 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.watch.rtc.time() - self._last_HR_date > _HR_FREQ and \ not self._track_HR_once: self._track_HR_once = _ON - mute = wasp.watch.display.mute - mute(True) wasp.system.wake() - mute(True) + if int(wasp.watch.rtc.time()) - self._last_touch > 5: + mute = wasp.watch.display.mute + mute(True) wasp.system.switch(self) wasp.system.request_tick(1000 // 8) @@ -556,8 +560,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() wasp.system.notify_level = self._old_notification_level # restore notification level self._page = _RINGING - mute = wasp.watch.display.mute - mute(True) + if int(wasp.watch.rtc.time()) - self._last_touch > 5: + mute = wasp.watch.display.mute + mute(True) wasp.system.wake() wasp.system.switch(self) self._draw() @@ -576,9 +581,10 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._hrdata = ppg.PPG(wasp.watch.hrs.read_hrs()) t = wasp.machine.Timer(id=1, period=8000000) t.start() - mute = wasp.watch.display.mute wasp.system.keep_awake() - mute(True) + if int(wasp.watch.rtc.time()) - self._last_touch > 5: + mute = wasp.watch.display.mute + mute(True) self._subtick(1) while t.time() < 41666: pass @@ -613,7 +619,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) self._track_HR_once = _OFF self._hrdata = None wasp.watch.hrs.disable() - wasp.system.sleep() + if int(wasp.watch.rtc.time()) - self._last_touch > 5: + wasp.system.sleep() def _subtick(self, ticks): """track heart rate at 24Hz""" @@ -623,8 +630,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """vibrate just a tiny bit before waking up, to gradually return to consciousness""" wasp.gc.collect() - mute = wasp.watch.display.mute - mute(True) + if int(wasp.watch.rtc.time()) - self._last_touch > 5: + mute = wasp.watch.display.mute + mute(True) wasp.system.wake() wasp.system.switch(self) wasp.watch.vibrator.pulse(duty=60, ms=100) From fc90854e5c0d8e14234cc63b74946d532b4bbc05 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 6 May 2022 16:59:13 +0200 Subject: [PATCH 473/485] new: write to file when screen has been turned on --- README.md | 1 - SleepTk.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6f14e6d..3804405 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,6 @@ * add a "power nap" mode that wakes you as soon as there has been no movement for 5 minutes OR (like steelball) when your heart rate drops * implement a simple Neural Network or decision tree to compute best wake up time, using one found in the scientific literature. * implement downsampling to 15 minutes precision to compute best wake up time -* if self.foreground is called, record the time. Use it to cancel smart alarm if you woke up too many times (more than 2 times in more than 20 minutes apart). * log smart alarm data to file? log user rating of how well he/she felt fresh at wake? * ability to send in real time to Bluetooth device the current sleep stage you're probably in. For use in Targeted Memory Reactivation? diff --git a/SleepTk.py b/SleepTk.py index e2395ba..992320a 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -167,6 +167,7 @@ class SleepTkApp(): if self._page == _RINGING: self._try_stop_alarm() elif self._page == _TRACKING: + self._was_touched = 1 # disable pressing to exit, use swipe up instead self._draw() else: @@ -202,6 +203,7 @@ class SleepTkApp(): mute(False) self._last_touch = int(wasp.watch.rtc.time()) if self._page == _TRACKING: + self._was_touched = 1 if self._conf_view is _OFF: if self.btn_off.touch(event): self._conf_view = widgets.ConfirmationView() @@ -396,6 +398,7 @@ class SleepTkApp(): self._last_checkpoint = 0 # to know when to save to file self._track_start_time = int(wasp.watch.rtc.time()) # makes output more compact self._last_HR_printed = "?" + self._was_touched = 0 wasp.watch.accel.reset() # create one file per recording session: @@ -525,7 +528,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) """save data to csv with row order: 1. average arm angle 2. elapsed times - 3. heart rate if present + 3/4. heart rate if present, and/or -1 if screen was woken up + by user during that time arm angle formula from https://www.nature.com/articles/s41598-018-31266-z note: math.atan() is faster than using a taylor serie """ @@ -544,7 +548,8 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) f.write("{:7f},{}{}\n".format( math.atan(buff[2] / (buff[0]**2 + buff[1]**2))*180/3.1415926535, # estimated arm angle int(wasp.watch.rtc.time() - self._track_start_time), - bpm + bpm, + ",-1" if self._was_touched else "" ).encode()) f.close() del f @@ -552,6 +557,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) buff[1] = 0 buff[2] = 0 self._last_checkpoint = self._data_point_nb + self._was_touched = 0 wasp.gc.collect() def _activate_ticks_to_ring(self): From 4201f264a999f3b3f423991236270eeaaab996bd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 6 May 2022 16:59:31 +0200 Subject: [PATCH 474/485] fix: properly reload screen after stopping alarm --- SleepTk.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SleepTk.py b/SleepTk.py index 992320a..0f7a251 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -153,6 +153,7 @@ class SleepTkApp(): wasp.system.cancel_alarm(self._WU_t, self._activate_ticks_to_ring) self._disable_tracking() self.__init__() + self.foreground() draw = wasp.watch.drawable draw.set_color(_FONT_COLOR) draw.string("{} to stop".format(_STOP_LIMIT - self._stop_trial), 0, 70) From db9350dffc3dfd7471f241587a1481475c7769bd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Fri, 6 May 2022 18:31:37 +0200 Subject: [PATCH 475/485] fix: wrong string printed at the end of ringing --- SleepTk.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 0f7a251..48ce4c4 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -148,15 +148,16 @@ class SleepTkApp(): def _try_stop_alarm(self): """If button or swipe more than _STOP_LIMIT, then stop ringing""" - self._stop_trial += 1 - if self._stop_trial >= _STOP_LIMIT: + if self._stop_trial + 1 >= _STOP_LIMIT: wasp.system.cancel_alarm(self._WU_t, self._activate_ticks_to_ring) self._disable_tracking() self.__init__() self.foreground() - draw = wasp.watch.drawable - draw.set_color(_FONT_COLOR) - draw.string("{} to stop".format(_STOP_LIMIT - self._stop_trial), 0, 70) + else: + self._stop_trial += 1 + draw = wasp.watch.drawable + draw.set_color(_FONT_COLOR) + draw.string("{} to stop".format(_STOP_LIMIT - self._stop_trial), 0, 70) def press(self, button, state): "stop ringing alarm if pressed physical button" From c78c0b12d534234ea363309d2222fb6dd3d85e26 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 8 May 2022 13:54:16 +0200 Subject: [PATCH 476/485] new: gradual wake by default over 10m instead of 15 --- SleepTk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 48ce4c4..267f85c 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -70,9 +70,9 @@ _BATTERY_THRESHOLD = const(15) _ANTICIPATE_ALLOWED = const(2400) # number of seconds SleepTk can wake you up before the alarm clock you set # only relevant if smart_alarm is enabled. (default: 2400) -_GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) +_GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) # nb of minutes before alarm to send a tiny vibration, designed to wake -# you more gently. (default: array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 15]) ) +# you more gently. (default: array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) ) _TIME_TO_FALL_ASLEEP = const(14) # minutes you take to fall asleep (default: 14, according to https://sleepyti.me/) _CYCLE_LENGTH = const(90) From e0dde35c1b6724d147ba86bf428ce7d9fe2ff8f2 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 8 May 2022 13:55:10 +0200 Subject: [PATCH 477/485] minor: renamed time_to_fall_asleep to minute_to_fall_asleep --- SleepTk.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 267f85c..919a346 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -73,8 +73,8 @@ _ANTICIPATE_ALLOWED = const(2400) _GRADUAL_WAKE = array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) # nb of minutes before alarm to send a tiny vibration, designed to wake # you more gently. (default: array("H", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) ) -_TIME_TO_FALL_ASLEEP = const(14) -# minutes you take to fall asleep (default: 14, according to https://sleepyti.me/) +_MINUTES_TO_FALL_ASLEEP = const(14) +# time you take to fall asleep (default: 14, according to https://sleepyti.me/) _CYCLE_LENGTH = const(90) # sleep cycle length in minutes. Currently used only to display best wake up # time but not to compute smart alarm! (default: 90 or 100, according to @@ -273,12 +273,12 @@ class SleepTkApp(): def _draw_duration(self, draw): draw.set_font(_FONT) if self._page == _SETTINGS1: - duration = (self._read_time(self._state_spinval_H, self._state_spinval_M) - wasp.watch.rtc.time()) / 60 - _TIME_TO_FALL_ASLEEP - assert duration >= _TIME_TO_FALL_ASLEEP + duration = (self._read_time(self._state_spinval_H, self._state_spinval_M) - wasp.watch.rtc.time()) / 60 - _MINUTES_TO_FALL_ASLEEP + assert duration >= _MINUTES_TO_FALL_ASLEEP y = 180 elif self._page == _TRACKING: draw.set_color(_FONT_COLOR) - duration = (wasp.watch.rtc.time() - self._track_start_time) / 60 - _TIME_TO_FALL_ASLEEP # time slept + duration = (wasp.watch.rtc.time() - self._track_start_time) / 60 - _MINUTES_TO_FALL_ASLEEP # time slept if duration <= 0: # don't print when not yet asleep return y = 130 @@ -346,7 +346,7 @@ class SleepTkApp(): (H, M) = wasp.watch.rtc.get_localtime()[3:5] goal_h = _SLEEP_GOAL_CYCLE * _CYCLE_LENGTH // 60 goal_m = _SLEEP_GOAL_CYCLE * _CYCLE_LENGTH % 60 - M += goal_m + _TIME_TO_FALL_ASLEEP + M += goal_m + _MINUTES_TO_FALL_ASLEEP while M % 5 != 0: M += 1 self._state_spinval_H = ((H + goal_h) % 24 + (M // 60)) % 24 From 96ab17075d1125a8d65c6fe2c15a057aa6d97c70 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sun, 8 May 2022 13:55:26 +0200 Subject: [PATCH 478/485] fix: vibration duty cycle from 50 to 25 --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 919a346..a9455e6 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -582,7 +582,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.switch(self) if self._page == _RINGING: wasp.system.keep_awake() - wasp.watch.vibrator.pulse(duty=50, ms=500) + wasp.watch.vibrator.pulse(duty=25, ms=500) elif self._track_HR_once: wasp.watch.hrs.enable() if self._hrdata is None: From f3516cc755468753cae4bbae9a8ea5a636a48a6d Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 9 May 2022 21:21:17 +0200 Subject: [PATCH 479/485] minor: smaller string --- SleepTk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index a9455e6..496f357 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -327,7 +327,7 @@ class SleepTkApp(): draw.string("No alarm set", 0, 70) draw.string("data points: {} / {}".format(str(self._data_point_nb), str(self._data_point_nb * _FREQ // _STORE_FREQ)), 0, 110) if self._track_HR_once: - draw.string("(Currently tracking HR)", 0, 170) + draw.string("(ongoing)", 0, 170) if self._state_HR_tracking: draw.string("HR:{}".format(self._last_HR_printed), 160, 170) self.btn_off = widgets.Button(x=0, y=200, w=240, h=40, label="Stop") From 3de209fa04fdf4cf5e6fb638075124d9809f03ef Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Sat, 28 May 2022 18:07:57 +0200 Subject: [PATCH 480/485] new: turn off tracking by default --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 496f357..7c04aaf 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -94,9 +94,9 @@ class SleepTkApp(): # default button state self._state_alarm = _ON - self._state_body_tracking = _ON - self._state_HR_tracking = _ON - self._state_gradual_wake = _OFF + self._state_body_tracking = _OFF + self._state_HR_tracking = _OFF + self._state_gradual_wake = _ON self._state_smart_alarm = _OFF self._state_spinval_H = _OFF self._state_spinval_M = _OFF From 1a5c552df83437a88956f3f5d17b55a687447a35 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Tue, 7 Jun 2022 12:42:04 +0200 Subject: [PATCH 481/485] feat: automatically updates hours when minutes is updated --- SleepTk.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 7c04aaf..24c670e 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -231,10 +231,18 @@ class SleepTkApp(): wasp.system.sleep() elif self._page == _SETTINGS1: if self._state_alarm and (self._spin_H.touch(event) or self._spin_M.touch(event)): - self._state_spinval_H = self._spin_H.value - self._spin_H.update() + if self._state_spinval_M == 0 and self._spin_M.value == 55: + self._spin_H.value -= 1 + elif self._state_spinval_M == 55 and self._spin_M.value == 0: + self._spin_H.value += 1 + if self._spin_H.value >= 24: + self._spin_H.value = 0 + elif self._spin_H.value <= -1: + self._spin_H.value = 23 self._state_spinval_M = self._spin_M.value self._spin_M.update() + self._state_spinval_H = self._spin_H.value + self._spin_H.update() if self._state_alarm: self._draw_duration(draw) return From f5bc059cad556ed2935c94a27fd7b01facd9e729 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 13 Jun 2022 20:51:27 +0200 Subject: [PATCH 482/485] feat: ramp up vibration to avoid waking up the gf --- SleepTk.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SleepTk.py b/SleepTk.py index 24c670e..22b6cbe 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -149,6 +149,8 @@ class SleepTkApp(): def _try_stop_alarm(self): """If button or swipe more than _STOP_LIMIT, then stop ringing""" if self._stop_trial + 1 >= _STOP_LIMIT: + self._n_vibration = 0 + del self._n_vibration wasp.system.cancel_alarm(self._WU_t, self._activate_ticks_to_ring) self._disable_tracking() self.__init__() @@ -576,6 +578,7 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.gc.collect() wasp.system.notify_level = self._old_notification_level # restore notification level self._page = _RINGING + self._n_vibration = 0 if int(wasp.watch.rtc.time()) - self._last_touch > 5: mute = wasp.watch.display.mute mute(True) @@ -590,7 +593,10 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.switch(self) if self._page == _RINGING: wasp.system.keep_awake() - wasp.watch.vibrator.pulse(duty=25, ms=500) + # in 10 vibrations, ramp up from subtle to strong: + wasp.watch.vibrator.pulse(duty=max(80 - 6 * self._n_vibration, 20), + ms=min(100 + 40 * self._n_vibration, 500)) + self._n_vibration += 1 elif self._track_HR_once: wasp.watch.hrs.enable() if self._hrdata is None: From c2994a5fa1830dd731227f7bf6686f3e51cbfbfd Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 13 Jun 2022 20:53:45 +0200 Subject: [PATCH 483/485] docs: mention time to fall asleep --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3804405..95f1277 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ * *optional: delete all the sleep data present in your watch using the script `rm_sleep_data.py`* ### Note to reader: +* Note that the watch assumes an average of 14 minutes to fall asleep. You have to take this into account if you feel super sleepy at the time of setting the alarm. * If you're interested or have any kind of things to say about this, **please** open an issue and tell me all about it :) * Status as of end of May 2022: *UI (**done**), regular alarm (**done**), heart tracking (**done** but might be a bit dodgy), smart alarm (**mostly done but untested**)* * you can download your sleep data file using the file `pull_sleep_data`. A suggested workflow to load it into [pandas](https://pypi.org/project/pandas/) can be found at the bottom of the page. From 9ca0760a3b7a26014ad15a519e07adeabc4b292c Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 13 Jun 2022 20:53:51 +0200 Subject: [PATCH 484/485] minor: typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95f1277..f36f5f9 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ * Status as of end of May 2022: *UI (**done**), regular alarm (**done**), heart tracking (**done** but might be a bit dodgy), smart alarm (**mostly done but untested**)* * you can download your sleep data file using the file `pull_sleep_data`. A suggested workflow to load it into [pandas](https://pypi.org/project/pandas/) can be found at the bottom of the page. * the notifications are set to "silent" during the tracking session and are restored to the previously used level when the alarm is ringing -* by default, the bluetooth is turned off when tracking to save battery. Wasp OS doesn't allow to turn it back on without restarting the watch. +* by default, the Bluetooth is turned off when tracking to save battery. Wasp OS doesn't allow to turn it back on without restarting the watch. * it seems the simulator is having a rough time with daylight saving mode or time management. I personnaly have a 1h offset between sleep estimation on the simulator compared to the pinetime, don't worry. # Screenshots: From 7ce31f74f19b943b43d1f65ad778f68b9820ef38 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub Date: Mon, 13 Jun 2022 21:06:11 +0200 Subject: [PATCH 485/485] new: ramp up over 60s instead of 10 --- SleepTk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SleepTk.py b/SleepTk.py index 22b6cbe..56bbf39 100644 --- a/SleepTk.py +++ b/SleepTk.py @@ -593,9 +593,9 @@ on.".format(h, m, _BATTERY_THRESHOLD)}) wasp.system.switch(self) if self._page == _RINGING: wasp.system.keep_awake() - # in 10 vibrations, ramp up from subtle to strong: - wasp.watch.vibrator.pulse(duty=max(80 - 6 * self._n_vibration, 20), - ms=min(100 + 40 * self._n_vibration, 500)) + # in 60 vibrations, ramp up from subtle to strong: + wasp.watch.vibrator.pulse(duty=max(80 - 1 * self._n_vibration, 20), + ms=min(100 + 6 * self._n_vibration, 500)) self._n_vibration += 1 elif self._track_HR_once: wasp.watch.hrs.enable()