├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── cla.yml │ ├── codeql-analysis.yml │ └── is-repo-lint.yml ├── LICENSE ├── PlayButton.jpg ├── README.md ├── Smartscaleplay.png ├── smartscale.py ├── smartscale_motivational.py └── wiiboard_test.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @RChloe 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | labels: 8 | - "gh-actions" 9 | - "dependencies" 10 | commit-message: 11 | prefix: "gh-actions" 12 | include: "scope" 13 | 14 | # Only for repos with a package.json 15 | # - package-ecosystem: npm 16 | # directory: / 17 | # schedule: 18 | # interval: weekly 19 | # labels: 20 | # - "npm" 21 | # - "dependencies" 22 | # commit-message: 23 | # prefix: "npm" 24 | # include: "scope" 25 | 26 | # - package-ecosystem: npm 27 | # directory: /lambda 28 | # schedule: 29 | # interval: weekly 30 | # labels: 31 | # - "npm" 32 | # - "dependencies" 33 | # commit-message: 34 | # prefix: "npm" 35 | # include: "scope" 36 | 37 | # other supported packages can be found here: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 38 | -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened,closed,synchronize] 7 | 8 | # select correct state for repository 9 | env: 10 | # state: private 11 | state: public 12 | 13 | jobs: 14 | public-or-private-repo: 15 | runs-on: ubuntu-latest 16 | outputs: 17 | repostate: ${{ steps.repo-state.outputs.repostate }} 18 | steps: 19 | 20 | - name: Repo state 21 | id: repo-state 22 | run: echo "repostate=${{env.state}}" >> $GITHUB_OUTPUT 23 | - name: Repo public? 24 | if: "${{ env.state == 'public' }}" 25 | run: echo "Workflow has repo set as public. If this is incorrect, uncomment line 10." 26 | - name: Repo private? 27 | if: "${{ env.state == 'private' }}" 28 | run: echo "Workflow has repo set as private. If this is incorrect, uncomment line 11." 29 | 30 | CLAssistant: 31 | needs: public-or-private-repo 32 | if: needs.public-or-private-repo.outputs.repostate == 'public' 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: "CLA Assistant" 36 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 37 | # Beta Release 38 | uses: contributor-assistant/github-action@v2.3.0 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | # the below token should have repo scope and must be manually added by you in the repository's secret 42 | PERSONAL_ACCESS_TOKEN : ${{ secrets.CLA_BOT_GH_ACCESS_TOKEN }} 43 | with: 44 | path-to-signatures: 'signatures/version1/cla.json' 45 | path-to-document: 'https://github.com/cla-assistant/github-action/blob/master/SAPCLA.md' # e.g. a CLA or a DCO document 46 | # branch should not be protected 47 | branch: 'master' 48 | allowlist: bot*, dependabot[bot], davidsulpy, rkuhlman, adametry, bborntrager, RChloe, TJBIII, JeffLoucks, krcummings1, ijavierTek 49 | 50 | #below are the optional inputs - If the optional inputs are not given, then default values will be taken 51 | remote-organization-name: 'initialstate' 52 | remote-repository-name: 'cla-signatures' 53 | #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' 54 | #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' 55 | #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' 56 | #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' 57 | #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' 58 | #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) 59 | #use-dco-flag: true - If you are using DCO instead of CLA 60 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '17 16 * * 4' 22 | 23 | # select correct state for repository 24 | env: 25 | # state: private 26 | state: public 27 | 28 | jobs: 29 | public-or-private-repo: 30 | runs-on: ubuntu-latest 31 | outputs: 32 | repostate: ${{ steps.repo-state.outputs.repostate }} 33 | steps: 34 | 35 | - name: Repo state 36 | id: repo-state 37 | run: echo "repostate=${{env.state}}" >> $GITHUB_OUTPUT 38 | - name: Repo public? 39 | if: "${{ env.state == 'public' }}" 40 | run: echo "Workflow has repo set as public. If this is incorrect, uncomment line 25." 41 | - name: Repo private? 42 | if: "${{ env.state == 'private' }}" 43 | run: echo "Workflow has repo set as private. If this is incorrect, uncomment line 26." 44 | 45 | # REMEMBER TO CHECK `LANGUAGE` MATRIX FOR CORRECT LANGUAGE SETTINGS 46 | analyze: 47 | needs: public-or-private-repo 48 | if: needs.public-or-private-repo.outputs.repostate == 'public' 49 | name: Analyze 50 | runs-on: ubuntu-latest 51 | permissions: 52 | actions: read 53 | contents: read 54 | security-events: write 55 | 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | language: [ 'python' ] 60 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 61 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 62 | 63 | steps: 64 | - name: Checkout repository 65 | uses: actions/checkout@v3 66 | 67 | # Initializes the CodeQL tools for scanning. 68 | - name: Initialize CodeQL 69 | uses: github/codeql-action/init@v2 70 | with: 71 | languages: ${{ matrix.language }} 72 | # If you wish to specify custom queries, you can do so here or in a config file. 73 | # By default, queries listed here will override any specified in a config file. 74 | # Prefix the list here with "+" to use these queries and those in the config file. 75 | 76 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 77 | # queries: security-extended,security-and-quality 78 | 79 | 80 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 81 | # If this step fails, then you should remove it and run the build manually (see below) 82 | - name: Autobuild 83 | uses: github/codeql-action/autobuild@v2 84 | 85 | # ℹ️ Command-line programs to run using the OS shell. 86 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 87 | 88 | # If the Autobuild fails above, remove it and uncomment the following three lines. 89 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 90 | 91 | # - run: | 92 | # echo "Run, Build Application using script" 93 | # ./location_of_script_within_repo/buildscript.sh 94 | 95 | - name: Perform CodeQL Analysis 96 | uses: github/codeql-action/analyze@v2 97 | with: 98 | category: "/language:${{matrix.language}}" 99 | -------------------------------------------------------------------------------- /.github/workflows/is-repo-lint.yml: -------------------------------------------------------------------------------- 1 | name: is-repo-lint 2 | on: 3 | push: 4 | branches: [ "master" ] 5 | pull_request: 6 | branches: [ "master" ] 7 | workflow_dispatch: 8 | 9 | # select correct state for repository 10 | env: 11 | # state: private 12 | state: public 13 | 14 | jobs: 15 | public-or-private-repo: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | repostate: ${{ steps.repo-state.outputs.repostate }} 19 | steps: 20 | 21 | - name: Repo state 22 | id: repo-state 23 | run: echo "repostate=${{env.state}}" >> $GITHUB_OUTPUT 24 | - name: Repo public? 25 | if: "${{ env.state == 'public' }}" 26 | run: echo "Workflow has repo set as public. If this is incorrect, uncomment line 11." 27 | - name: Repo private? 28 | if: "${{ env.state == 'private' }}" 29 | run: echo "Workflow has repo set as private. If this is incorrect, uncomment line 12." 30 | 31 | check-for-codeowners-file: 32 | runs-on: ubuntu-latest 33 | steps: 34 | 35 | - name: Checkout repo 36 | uses: actions/checkout@v3 37 | 38 | - name: Check for CODEOWNERS 39 | id: codeowners_file 40 | uses: initialstate/file-check-action@v1 41 | with: 42 | file: ".github/CODEOWNERS" 43 | 44 | - name: CODEOWNERS file Output Test 45 | run: echo ${{ steps.codeowners_file.outputs.file_exists }} 46 | 47 | - name: CODEOWNERS file exists with content 48 | if: steps.codeowners_file.outputs.file_exists == 'true' 49 | run: echo CODEOWNERS file exists! 50 | 51 | - name: CODEOWNERS file does not exist 52 | if: steps.codeowners_file.outputs.file_exists == 'false' 53 | run: echo CODEOWNERS file does not exist! 54 | 55 | check-for-readme-file: 56 | runs-on: ubuntu-latest 57 | steps: 58 | 59 | - name: Checkout repo 60 | uses: actions/checkout@v3 61 | 62 | - name: Check for README.md 63 | id: readme_file 64 | uses: initialstate/file-check-action@v1 65 | with: 66 | file: "README" 67 | 68 | - name: README file Output Test 69 | run: echo ${{ steps.readme_file.outputs.file_exists }} 70 | 71 | - name: README file exists with content 72 | if: steps.readme_file.outputs.file_exists == 'true' 73 | run: echo README file exists! 74 | 75 | - name: README file does not exist 76 | if: steps.readme_file.outputs.file_exists == 'false' 77 | run: echo README file does not exist! 78 | 79 | check-for-license: 80 | needs: public-or-private-repo 81 | if: needs.public-or-private-repo.outputs.repostate == 'public' 82 | runs-on: ubuntu-latest 83 | steps: 84 | 85 | - name: Checkout repo 86 | uses: actions/checkout@v3 87 | 88 | - name: Check for LICENSE.md 89 | id: license_file 90 | uses: initialstate/file-check-action@v1 91 | with: 92 | file: "LICENSE" 93 | 94 | - name: LICENSE file Output Test 95 | run: echo ${{ steps.license_file.outputs.file_exists }} 96 | 97 | - name: LICENSE file exists with content 98 | if: steps.license_file.outputs.file_exists == 'true' 99 | run: echo LICENSE file exists! 100 | 101 | - name: LICENSE file does not exist 102 | if: steps.license_file.outputs.file_exists == 'false' 103 | run: echo LICENSE file does not exist! 104 | 105 | check-for-dependabot-file: 106 | runs-on: ubuntu-latest 107 | steps: 108 | 109 | - name: Checkout repo 110 | uses: actions/checkout@v3 111 | 112 | - name: Check for dependabot.yml 113 | id: dependabot_file 114 | uses: initialstate/file-check-action@v1 115 | with: 116 | file: ".github/dependabot.yml" 117 | 118 | - name: dependabot.yml file Output Test 119 | run: echo ${{ steps.dependabot_file.outputs.file_exists }} 120 | 121 | - name: dependabot file exists with content 122 | if: steps.dependabot_file.outputs.file_exists == 'true' 123 | run: echo dependabot file exists! 124 | 125 | - name: dependabot file does not exist 126 | if: steps.dependabot_file.outputs.file_exists == 'false' 127 | run: echo dependabot file does not exist! 128 | 129 | check-for-codeql-file: 130 | runs-on: ubuntu-latest 131 | steps: 132 | 133 | - name: Checkout repo 134 | uses: actions/checkout@v3 135 | 136 | - name: Check for codeql-analysis.yml 137 | id: codeql-analysis_file 138 | uses: initialstate/file-check-action@v1 139 | with: 140 | file: ".github/workflows/codeql-analysis.yml" 141 | 142 | - name: codeql-analysis.yml file Output Test 143 | run: echo ${{ steps.codeql-analysis_file.outputs.file_exists }} 144 | 145 | - name: codeql-analysis file exists with content 146 | if: steps.codeql-analysis_file.outputs.file_exists == 'true' 147 | run: echo codeql-analysis file exists! 148 | 149 | - name: codeql-analysis file does not exist 150 | if: steps.codeql-analysis_file.outputs.file_exists == 'false' 151 | run: echo codeql-analysis file does not exist! 152 | 153 | check-for-cla-bot-gh-access-token: 154 | needs: public-or-private-repo 155 | if: needs.public-or-private-repo.outputs.repostate == 'public' 156 | runs-on: ubuntu-latest 157 | steps: 158 | 159 | - name: Check for missing CLA_BOT_GH_ACCESS_TOKEN 160 | env: 161 | MY_KEY: ${{ secrets.CLA_BOT_GH_ACCESS_TOKEN }} 162 | if: "${{ env.MY_KEY == '' }}" 163 | uses: actions/github-script@v6 164 | with: 165 | script: | 166 | core.setFailed('CLA_BOT_GH_ACCESS_TOKEN secret is missing. It is needed to successfully run the CLA assistant.') 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /PlayButton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/initialstate/smart-scale/40fd61349f4fdb3a26da61fa008914cddc297a86/PlayButton.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Hackable, Weight Tracking, Text Messaging Scale with a Sense of Humor 2 | 3 |  4 | 5 |  6 | 7 | Are you tired of staring at that stupid, soulless, bearer of bad news bathroom scale every morning? The one that you often say "I hate you" to as if it might hear you and give a damn. Why hasn't anyone made a scale that is actually fun to use? It is time to create a scale that is not only smart but has a bit more personality to brighten your day. We are going to build our very own hackable, weight tracking, text messaging bathroom scale that comes with a built-in sense of humor ... [Read More](https://github.com/InitialState/smart-scale/wiki) 8 | 9 | ## License 10 | 11 | This software is made available under the [Lesser GPL license](http://www.gnu.org/licenses/lgpl.html). 12 | -------------------------------------------------------------------------------- /Smartscaleplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/initialstate/smart-scale/40fd61349f4fdb3a26da61fa008914cddc297a86/Smartscaleplay.png -------------------------------------------------------------------------------- /smartscale.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import collections 5 | import time 6 | import bluetooth 7 | import sys 8 | import subprocess 9 | from ISStreamer.Streamer import Streamer 10 | from random import randint 11 | 12 | # --------- User Settings --------- 13 | BUCKET_NAME = ":apple: My Weight History" 14 | BUCKET_KEY = "weight11" 15 | ACCESS_KEY = "PLACE YOUR INITIAL STATE ACCESS KEY HERE" 16 | METRIC_UNITS = False 17 | WEIGHT_SAMPLES = 250 18 | THROWAWAY_SAMPLES = 75 19 | WEIGHT_HISTORY = 7 20 | # --------------------------------- 21 | 22 | # Wiiboard Parameters 23 | CONTINUOUS_REPORTING = "04" # Easier as string with leading zero 24 | COMMAND_LIGHT = 11 25 | COMMAND_REPORTING = 12 26 | COMMAND_REQUEST_STATUS = 15 27 | COMMAND_REGISTER = 16 28 | COMMAND_READ_REGISTER = 17 29 | INPUT_STATUS = 20 30 | INPUT_READ_DATA = 21 31 | EXTENSION_8BYTES = 32 32 | BUTTON_DOWN_MASK = 8 33 | TOP_RIGHT = 0 34 | BOTTOM_RIGHT = 1 35 | TOP_LEFT = 2 36 | BOTTOM_LEFT = 3 37 | BLUETOOTH_NAME = "Nintendo RVL-WBC-01" 38 | 39 | 40 | class EventProcessor: 41 | def __init__(self): 42 | self._measured = False 43 | self.done = False 44 | self._measureCnt = 0 45 | self._events = range(WEIGHT_SAMPLES) 46 | self._weights = range(WEIGHT_HISTORY) 47 | self._times = range(WEIGHT_HISTORY) 48 | self._unit = "lb" 49 | self._weightCnt = 0 50 | self._prevWeight = 0 51 | self._weight = 0 52 | self._weightChange = 0 53 | self.streamer = Streamer(bucket_name=BUCKET_NAME,bucket_key=BUCKET_KEY,access_key=ACCESS_KEY) 54 | 55 | def messageWeighFirst(self, weight, unit): 56 | weight = float("{0:.2f}".format(weight)) 57 | msg = [] 58 | msg.append("What do vegan zombies eat? Gggggrrrraaaaaaaiiiiinnnnnssssss❗️ You weigh " + str(weight) + " " + unit + "!") 59 | msg.append("Guys that wear skinny jeans took the phrase, getting into her pants, the wrong way. 👖 You weigh " + str(weight) + " " + unit + "!") 60 | msg.append("Why do watermelons have fancy weddings? Because they cantaloupe. 🍉 You weigh " + str(weight) + " " + unit + "!") 61 | msg.append("Why did the can crusher quit his job? Because it was soda pressing. 😜 You weigh " + str(weight) + " " + unit + "!") 62 | msg.append("My friend thinks he is smart. He told me an onion is the only food that makes you cry, so I threw a coconut at his face. You = " + str(weight) + " " + unit) 63 | msg.append("Turning vegan is a big missed steak. 😜 You weigh " + str(weight) + " " + unit + "!") 64 | msg.append("Is there anything more capitalist than a peanut with a top hat, cane, and monocle selling you other peanuts to eat? You weigh " + str(weight) + " " + unit + "!") 65 | msg.append("How has the guy who makes Capri Sun straw openings not been up for a job performance review? You weigh " + str(weight) + " " + unit + "!") 66 | msg.append("How do I like my eggs? Umm, in a cake. 🍰 You weigh " + str(weight) + " " + unit + "!") 67 | msg.append("Billy has 32 pieces of bacon and eats 28. What does he have now? Happiness. Billy has happiness. You weigh " + str(weight) + " " + unit + "!") 68 | msg.append("Diet day 1: I have removed all the bad food from the house. It was delicious. You weigh " + str(weight) + " " + unit + "!") 69 | msg.append("When I see you, I am happy, I love you not for what you look like, but for what you have inside. -Me to my fridge. You weigh " + str(weight) + " " + unit + "!") 70 | msg.append("Netflix has 7 letters. So does foooood. Coincidence? I think not. You weigh " + str(weight) + " " + unit + "!") 71 | msg.append("Studies show that if there is going to be free food, I will show up 100 percent of the time. You weigh " + str(weight) + " " + unit + "!") 72 | msg.append("I can multitask. I can eat breakfast and think about lunch at the same time. You weigh " + str(weight) + " " + unit + "!") 73 | return msg[randint(0, len(msg)-1)] 74 | 75 | def messageWeighLess(self, weight, weightChange, unit): 76 | weight = float("{0:.2f}".format(weight)) 77 | weightChange = float("{0:.2f}".format(weightChange)) 78 | msg = [] 79 | msg.append("You're getting so skinny that if someone slaps you, they'll get a paper cut. 👋 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 80 | msg.append("Wow that Lean Cuisine really filled me up - Said No One Ever. 🍲 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 81 | msg.append("Whoever said nothing tastes as good as skinny feels has clearly never had 🍕 or 🍷 or 🍰. You lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ").") 82 | msg.append("I know milk does a body good, but damn, how much have you been drinking? 😍 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 83 | msg.append("Are you from Tennessee? Because you're the only ten I see! 😍 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 84 | msg.append("If you were words on a page, you'd be what they call FINE PRINT! 📖 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 85 | msg.append("If you were a transformer, you'd be a HOT-obot, and your name would be Optimus Fine! 😍 You lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ").") 86 | msg.append("WTF! (where's the food) 🍗 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 87 | msg.append("It's a lot easier to stop eating carbs once you've come to terms with living a joyless life full of anger and sadness. U lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ")") 88 | msg.append("The Roomba just beat me to a piece of popcorn on the floor. This is how the war against the machines begins. U lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ")") 89 | msg.append("I won't be impressed with technology until I can download food. You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 90 | msg.append("I choked on a carrot today and all I could think was I bet a doughnut wouldn't have done this to me. U lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ")") 91 | msg.append("Asking me if I am hungry is like asking me if I want money. You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 92 | msg.append("I think my soulmate might be carbs. You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 93 | msg.append("Great job! We made a video about your progress. Check it out at https://youtu.be/dQw4w9WgXcQ. U lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ").") 94 | return msg[randint(0, len(msg)-1)] 95 | 96 | def messageWeighMore(self, weight, weightChange, unit): 97 | weight = float("{0:.2f}".format(weight)) 98 | weightChange = float("{0:.2f}".format(weightChange)) 99 | msg = [] 100 | msg.append("You are in shape ... round is a shape 🍩. You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 101 | msg.append("You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + "). I didn't want to sugarcoat it b/c I was afraid you would eat that too. 🍦") 102 | msg.append("You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + "). I hated telling you that b/c you apparently have enough on your plate. 🍽") 103 | msg.append("Stressed spelled backwards is desserts, but I bet you already knew that. 🍰 You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 104 | msg.append("You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + "). You probably just forgot to go to the gym. That's like what, 8 years in a row now? 🏋") 105 | msg.append("You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + "). The good news is that you are getting easier to see! 🔭") 106 | msg.append("You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + "). YOLO! (You Obviously Love Oreos) 💛") 107 | msg.append("You should name your dog Five Miles so you can honestly say you walk Five Miles every day. 🐶 You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 108 | msg.append("Instead of a John, call your bathroom a Jim so you can honstely say you go to the Jim every morning. 🚽 You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 109 | msg.append("You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + "). The good news is that there is more of you to love! 💛") 110 | msg.append("You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + "). 💩") 111 | msg.append("You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + "). I gave up desserts once. It was the worst 20 minutes of my life. 🍪 ") 112 | msg.append("When you phone dings, do people think you are backing up? 🚚 You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 113 | msg.append("Always eat alone. If people never see you eat, they might believe you when you say you have a thyroid problem. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + ")") 114 | msg.append("After exercising, I always eat a pizza ... just kidding, I don't exercise. 🍕 You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 115 | msg.append("If you are what you eat, perhaps you should eat a skinny person. 😱 You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 116 | msg.append("I never run with scissors. OK, those last two words were unnecessary. ✂️ You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 117 | msg.append("You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + "). I am sure it is all muscle. 💪 ") 118 | msg.append("Yeah, I'm into fitness... Fit'ness whole burger in my mouth. 🍔👅 You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 119 | return msg[randint(0, len(msg)-1)] 120 | 121 | def messageWeighSame(self, weight, weightChange, unit): 122 | weight = float("{0:.2f}".format(weight)) 123 | weightChange = float("{0:.2f}".format(weightChange)) 124 | msg = [] 125 | msg.append("Congratulations on nothing ... you practically weigh the same since last time. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 126 | msg.append("What do you call a fake noodle? An impasta. 🍝 Your weight didn't change much since last time. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 127 | msg.append("Bacon is low-carb and gluten-free ... just sayin'. 🐷 Your weight didn't change much since last time. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 128 | msg.append("I may look like I am deep in thought, but I'm really just thinking about what I'm going to eat later. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 129 | msg.append("I haven't eaten an apple in days. The doctors are closing in. My barricade won't last. Tell my family I love th-. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 130 | msg.append("Ban pre-shredded cheese. Make America grate again. 🧀 Your weight didn't change much since last time. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 131 | msg.append("If I share my food with you, it's either because I love you a lot or because it fell on the floor. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 132 | msg.append("The sad moment you lose a chip in the dip so you send in a recon chip and that breaks too. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 133 | msg.append("I only want two things: 1 - To lose weight. 2 - To eat. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 134 | msg.append("I enjoy long, romantic walks to the fridge. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 135 | msg.append("I just don't wanna look back and think, I could have eaten that. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 136 | msg.append("Most people want a perfect relationship. I just want a hamburger that looks like the one in commercials. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 137 | msg.append("Love is in the air ... or is that bacon? 🐷 Your weight didn't change much since last time. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 138 | msg.append("That is too much bacon. -Said No One Ever 🐷 Your weight didn't change much since last time. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 139 | return msg[randint(0, len(msg)-1)] 140 | 141 | def mass(self, event): 142 | if (event.totalWeight > 2): 143 | if self._measureCnt < WEIGHT_SAMPLES: 144 | if self._measureCnt == 1: 145 | print "Measuring ..." 146 | self.streamer.log("Update", "Measuring ...") 147 | self.streamer.flush() 148 | 149 | if METRIC_UNITS: 150 | self._events[self._measureCnt] = event.totalWeight 151 | self._unit = "kg" 152 | else: 153 | self._events[self._measureCnt] = event.totalWeight*2.20462 154 | self._unit = "lb" 155 | self._measureCnt += 1 156 | if self._measureCnt == WEIGHT_SAMPLES: 157 | 158 | # Average multiple measurements to get the weight and stream it 159 | self._prevWeight = self._weight 160 | self._sum = 0 161 | for x in range(THROWAWAY_SAMPLES, WEIGHT_SAMPLES-1): 162 | self._sum += self._events[x] 163 | self._weight = self._sum/(WEIGHT_SAMPLES-THROWAWAY_SAMPLES) 164 | if self._measured: 165 | self._weightChange = self._weight - self._prevWeight 166 | if self._weightChange < -0.4: 167 | self._msg = self.messageWeighLess(self._weight, self._weightChange, self._unit) 168 | elif self._weightChange > 0.4: 169 | self._msg = self.messageWeighMore(self._weight, self._weightChange, self._unit) 170 | else: 171 | self._msg = self.messageWeighSame(self._weight, self._weightChange, self._unit) 172 | else: 173 | self._msg = self.messageWeighFirst(self._weight, self._unit) 174 | print self._msg 175 | self.streamer.log("Update", self._msg) 176 | tmpVar = "Weight(" + self._unit + ")" 177 | self.streamer.log(str(tmpVar), float("{0:.2f}".format(self._weight))) 178 | tmpVar = time.strftime("%x %I:%M %p") 179 | self.streamer.log("Weigh Date", tmpVar) 180 | self.streamer.flush() 181 | 182 | # Store a small history of weights and overwite any measurement less than 2 hours old (7200 seconds) 183 | if self._weightCnt > 0: 184 | if (time.time() - self._times[self._weightCnt-1]) < 7200: 185 | self._tmpVar = time.time() - self._times[self._weightCnt-1] 186 | self._weightCnt -= 1 187 | self._weights[self._weightCnt] = self._weight 188 | self._times[self._weightCnt] = time.time() 189 | self._weightCnt += 1 190 | # Send an extra update at the end of WEIGHT_HISTORY 191 | if self._weightCnt == WEIGHT_HISTORY: 192 | self._weightCnt = 0 193 | self._weightChange = self._weights[WEIGHT_HISTORY-1] - self._weights[0] 194 | self._weightChange = float("{0:.2f}".format(self._weightChange)) 195 | timeChange = (self._times[WEIGHT_HISTORY-1] - self._times[0])/86400 196 | timeChange = float("{0:.1f}".format(timeChange)) 197 | if self._weightChange > 0: 198 | self._msg = "🕒 You gained " + str(self._weightChange) + " " + self._unit + " in the last " + str(timeChange) + " days!" 199 | else: 200 | self._msg = "🕒 You lost " + str(abs(self._weightChange)) + " " + self._unit + " in the last " + str(timeChange) + " days!" 201 | self.streamer.log("Update", self._msg) 202 | self.streamer.flush() 203 | 204 | # Keep track of the first complete measurement 205 | if not self._measured: 206 | self._measured = True 207 | else: 208 | self._measureCnt = 0 209 | 210 | @property 211 | def weight(self): 212 | if not self._events: 213 | return 0 214 | histogram = collections.Counter(round(num, 1) for num in self._events) 215 | return histogram.most_common(1)[0][0] 216 | 217 | 218 | class BoardEvent: 219 | def __init__(self, topLeft, topRight, bottomLeft, bottomRight, buttonPressed, buttonReleased): 220 | 221 | self.topLeft = topLeft 222 | self.topRight = topRight 223 | self.bottomLeft = bottomLeft 224 | self.bottomRight = bottomRight 225 | self.buttonPressed = buttonPressed 226 | self.buttonReleased = buttonReleased 227 | #convenience value 228 | self.totalWeight = topLeft + topRight + bottomLeft + bottomRight 229 | 230 | class Wiiboard: 231 | def __init__(self, processor): 232 | # Sockets and status 233 | self.receivesocket = None 234 | self.controlsocket = None 235 | 236 | self.processor = processor 237 | self.calibration = [] 238 | self.calibrationRequested = False 239 | self.LED = False 240 | self.address = None 241 | self.buttonDown = False 242 | for i in xrange(3): 243 | self.calibration.append([]) 244 | for j in xrange(4): 245 | self.calibration[i].append(10000) # high dummy value so events with it don't register 246 | 247 | self.status = "Disconnected" 248 | self.lastEvent = BoardEvent(0, 0, 0, 0, False, False) 249 | 250 | try: 251 | self.receivesocket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 252 | self.controlsocket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 253 | except ValueError: 254 | raise Exception("Error: Bluetooth not found") 255 | 256 | def isConnected(self): 257 | return self.status == "Connected" 258 | 259 | # Connect to the Wiiboard at bluetooth address
260 | def connect(self, address): 261 | if address is None: 262 | print "Non existant address" 263 | return 264 | self.receivesocket.connect((address, 0x13)) 265 | self.controlsocket.connect((address, 0x11)) 266 | if self.receivesocket and self.controlsocket: 267 | print "Connected to Wiiboard at address " + address 268 | self.status = "Connected" 269 | self.address = address 270 | self.calibrate() 271 | useExt = ["00", COMMAND_REGISTER, "04", "A4", "00", "40", "00"] 272 | self.send(useExt) 273 | self.setReportingType() 274 | print "Wiiboard connected" 275 | else: 276 | print "Could not connect to Wiiboard at address " + address 277 | 278 | def receive(self): 279 | while self.status == "Connected" and not self.processor.done: 280 | data = self.receivesocket.recv(25) 281 | intype = int(data.encode("hex")[2:4]) 282 | if intype == INPUT_STATUS: 283 | # TODO: Status input received. It just tells us battery life really 284 | self.setReportingType() 285 | elif intype == INPUT_READ_DATA: 286 | if self.calibrationRequested: 287 | packetLength = (int(str(data[4]).encode("hex"), 16) / 16 + 1) 288 | self.parseCalibrationResponse(data[7:(7 + packetLength)]) 289 | 290 | if packetLength < 16: 291 | self.calibrationRequested = False 292 | elif intype == EXTENSION_8BYTES: 293 | self.processor.mass(self.createBoardEvent(data[2:12])) 294 | else: 295 | print "ACK to data write received" 296 | 297 | def disconnect(self): 298 | if self.status == "Connected": 299 | self.status = "Disconnecting" 300 | while self.status == "Disconnecting": 301 | self.wait(100) 302 | try: 303 | self.receivesocket.close() 304 | except: 305 | pass 306 | try: 307 | self.controlsocket.close() 308 | except: 309 | pass 310 | print "WiiBoard disconnected" 311 | 312 | # Try to discover a Wiiboard 313 | def discover(self): 314 | print "Press the red sync button on the board now" 315 | address = None 316 | bluetoothdevices = bluetooth.discover_devices(duration=6, lookup_names=True) 317 | for bluetoothdevice in bluetoothdevices: 318 | if bluetoothdevice[1] == BLUETOOTH_NAME: 319 | address = bluetoothdevice[0] 320 | print "Found Wiiboard at address " + address 321 | if address is None: 322 | print "No Wiiboards discovered." 323 | return address 324 | 325 | def createBoardEvent(self, bytes): 326 | buttonBytes = bytes[0:2] 327 | bytes = bytes[2:12] 328 | buttonPressed = False 329 | buttonReleased = False 330 | 331 | state = (int(buttonBytes[0].encode("hex"), 16) << 8) | int(buttonBytes[1].encode("hex"), 16) 332 | if state == BUTTON_DOWN_MASK: 333 | buttonPressed = True 334 | if not self.buttonDown: 335 | print "Button pressed" 336 | self.buttonDown = True 337 | 338 | if not buttonPressed: 339 | if self.lastEvent.buttonPressed: 340 | buttonReleased = True 341 | self.buttonDown = False 342 | print "Button released" 343 | 344 | rawTR = (int(bytes[0].encode("hex"), 16) << 8) + int(bytes[1].encode("hex"), 16) 345 | rawBR = (int(bytes[2].encode("hex"), 16) << 8) + int(bytes[3].encode("hex"), 16) 346 | rawTL = (int(bytes[4].encode("hex"), 16) << 8) + int(bytes[5].encode("hex"), 16) 347 | rawBL = (int(bytes[6].encode("hex"), 16) << 8) + int(bytes[7].encode("hex"), 16) 348 | 349 | topLeft = self.calcMass(rawTL, TOP_LEFT) 350 | topRight = self.calcMass(rawTR, TOP_RIGHT) 351 | bottomLeft = self.calcMass(rawBL, BOTTOM_LEFT) 352 | bottomRight = self.calcMass(rawBR, BOTTOM_RIGHT) 353 | boardEvent = BoardEvent(topLeft, topRight, bottomLeft, bottomRight, buttonPressed, buttonReleased) 354 | return boardEvent 355 | 356 | def calcMass(self, raw, pos): 357 | val = 0.0 358 | #calibration[0] is calibration values for 0kg 359 | #calibration[1] is calibration values for 17kg 360 | #calibration[2] is calibration values for 34kg 361 | if raw < self.calibration[0][pos]: 362 | return val 363 | elif raw < self.calibration[1][pos]: 364 | val = 17 * ((raw - self.calibration[0][pos]) / float((self.calibration[1][pos] - self.calibration[0][pos]))) 365 | elif raw > self.calibration[1][pos]: 366 | val = 17 + 17 * ((raw - self.calibration[1][pos]) / float((self.calibration[2][pos] - self.calibration[1][pos]))) 367 | 368 | return val 369 | 370 | def getEvent(self): 371 | return self.lastEvent 372 | 373 | def getLED(self): 374 | return self.LED 375 | 376 | def parseCalibrationResponse(self, bytes): 377 | index = 0 378 | if len(bytes) == 16: 379 | for i in xrange(2): 380 | for j in xrange(4): 381 | self.calibration[i][j] = (int(bytes[index].encode("hex"), 16) << 8) + int(bytes[index + 1].encode("hex"), 16) 382 | index += 2 383 | elif len(bytes) < 16: 384 | for i in xrange(4): 385 | self.calibration[2][i] = (int(bytes[index].encode("hex"), 16) << 8) + int(bytes[index + 1].encode("hex"), 16) 386 | index += 2 387 | 388 | # Send to the Wiiboard 389 | # should be an array of strings, each string representing a single hex byte 390 | def send(self, data): 391 | if self.status != "Connected": 392 | return 393 | data[0] = "52" 394 | 395 | senddata = "" 396 | for byte in data: 397 | byte = str(byte) 398 | senddata += byte.decode("hex") 399 | 400 | self.controlsocket.send(senddata) 401 | 402 | #Turns the power button LED on if light is True, off if False 403 | #The board must be connected in order to set the light 404 | def setLight(self, light): 405 | if light: 406 | val = "10" 407 | else: 408 | val = "00" 409 | 410 | message = ["00", COMMAND_LIGHT, val] 411 | self.send(message) 412 | self.LED = light 413 | 414 | def calibrate(self): 415 | message = ["00", COMMAND_READ_REGISTER, "04", "A4", "00", "24", "00", "18"] 416 | self.send(message) 417 | self.calibrationRequested = True 418 | 419 | def setReportingType(self): 420 | bytearr = ["00", COMMAND_REPORTING, CONTINUOUS_REPORTING, EXTENSION_8BYTES] 421 | self.send(bytearr) 422 | 423 | def wait(self, millis): 424 | time.sleep(millis / 1000.0) 425 | 426 | 427 | def main(): 428 | processor = EventProcessor() 429 | 430 | board = Wiiboard(processor) 431 | if len(sys.argv) == 1: 432 | print "Discovering board..." 433 | address = board.discover() 434 | else: 435 | address = sys.argv[1] 436 | 437 | try: 438 | # Disconnect already-connected devices. 439 | # This is basically Linux black magic just to get the thing to work. 440 | subprocess.check_output(["bluez-test-input", "disconnect", address], stderr=subprocess.STDOUT) 441 | subprocess.check_output(["bluez-test-input", "disconnect", address], stderr=subprocess.STDOUT) 442 | except: 443 | pass 444 | 445 | print "Trying to connect..." 446 | board.connect(address) # The wii board must be in sync mode at this time 447 | board.wait(200) 448 | # Flash the LED so we know we can step on. 449 | board.setLight(False) 450 | board.wait(500) 451 | board.setLight(True) 452 | board.receive() 453 | 454 | if __name__ == "__main__": 455 | main() 456 | -------------------------------------------------------------------------------- /smartscale_motivational.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import collections 5 | import time 6 | import bluetooth 7 | import sys 8 | import subprocess 9 | from ISStreamer.Streamer import Streamer 10 | from random import randint 11 | 12 | # --------- User Settings --------- 13 | BUCKET_NAME = ":apple: My Weight History" 14 | BUCKET_KEY = "weight11" 15 | ACCESS_KEY = "PLACE YOUR INITIAL STATE ACCESS KEY HERE" 16 | METRIC_UNITS = False 17 | WEIGHT_SAMPLES = 250 18 | THROWAWAY_SAMPLES = 75 19 | WEIGHT_HISTORY = 7 20 | # --------------------------------- 21 | 22 | # Wiiboard Parameters 23 | CONTINUOUS_REPORTING = "04" # Easier as string with leading zero 24 | COMMAND_LIGHT = 11 25 | COMMAND_REPORTING = 12 26 | COMMAND_REQUEST_STATUS = 15 27 | COMMAND_REGISTER = 16 28 | COMMAND_READ_REGISTER = 17 29 | INPUT_STATUS = 20 30 | INPUT_READ_DATA = 21 31 | EXTENSION_8BYTES = 32 32 | BUTTON_DOWN_MASK = 8 33 | TOP_RIGHT = 0 34 | BOTTOM_RIGHT = 1 35 | TOP_LEFT = 2 36 | BOTTOM_LEFT = 3 37 | BLUETOOTH_NAME = "Nintendo RVL-WBC-01" 38 | 39 | 40 | class EventProcessor: 41 | def __init__(self): 42 | self._measured = False 43 | self.done = False 44 | self._measureCnt = 0 45 | self._events = range(WEIGHT_SAMPLES) 46 | self._weights = range(WEIGHT_HISTORY) 47 | self._times = range(WEIGHT_HISTORY) 48 | self._unit = "lb" 49 | self._weightCnt = 0 50 | self._prevWeight = 0 51 | self._weight = 0 52 | self._weightChange = 0 53 | self.streamer = Streamer(bucket_name=BUCKET_NAME,bucket_key=BUCKET_KEY,access_key=ACCESS_KEY) 54 | 55 | def messageWeighFirst(self, weight, unit): 56 | weight = float("{0:.2f}".format(weight)) 57 | msg = [] 58 | msg.append("Don’t let a stumble in the road be the end of your journey You weigh " + str(weight) + " " + unit + "!") 59 | msg.append("When you eat crap, you feel crap. You weigh " + str(weight) + " " + unit + "!") 60 | msg.append("Keep going. You weigh " + str(weight) + " " + unit + "!") 61 | msg.append("Take it one meal at a time. You weigh " + str(weight) + " " + unit + "!") 62 | msg.append("When you feel like quitting, think about why you started. You weigh " + str(weight) + " " + unit) 63 | msg.append("Every step is progress, no matter how small. You weigh " + str(weight) + " " + unit + "!") 64 | msg.append("You will never win if you never begin. You weigh " + str(weight) + " " + unit + "!") 65 | msg.append("The secret of change is to focus all of your energy not on fighting the old, but on building the new. You weigh " + str(weight) + " " + unit + "!") 66 | msg.append("You get what you focus on, so focus on what you want. You weigh " + str(weight) + " " + unit + "!") 67 | msg.append("Decide. Commit. Succeed. You weigh " + str(weight) + " " + unit + "!") 68 | msg.append("Keep an open mind and a closed refrigerator. You weigh " + str(weight) + " " + unit + "!") 69 | msg.append("Whatever you can do, or dream you can, begin it. Boldness has genius, power and magic in it. You weigh " + str(weight) + " " + unit + "!") 70 | return msg[randint(0, len(msg)-1)] 71 | 72 | def messageWeighLess(self, weight, weightChange, unit): 73 | weight = float("{0:.2f}".format(weight)) 74 | weightChange = float("{0:.2f}".format(weightChange)) 75 | msg = [] 76 | msg.append("One pound at a time. You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 77 | msg.append("You are your only limit. 🍲 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 78 | msg.append("Success is no accident: it is hard work and perseverance. You lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ").") 79 | msg.append("I know milk does a body good, but damn, how much have you been drinking? 😍 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 80 | msg.append("Are you from Tennessee? Because you're the only ten I see! 😍 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 81 | msg.append("If you were words on a page, you'd be what they call FINE PRINT! 📖 You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 82 | msg.append("If you were a transformer, you'd be a HOT-obot, and your name would be Optimus Fine! 😍 You lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ").") 83 | msg.append("Don’t stop until you’re proud. You lost " + str(abs(weightChange)) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 84 | msg.append("A little progress each day adds up to big results. U lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ")") 85 | msg.append("Great job! We made a video about your progress. Check it out at https://youtu.be/dQw4w9WgXcQ. U lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ").") 86 | msg.append("Don’t wait until you’ve reached your goal to be proud of yourself. Be proud of every step you take. U lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ")") 87 | msg.append("The only place where success comes before work is in the dictionary. U lost " + str(abs(weightChange)) + " " + unit + " (" + str(weight) + " " + unit + ")") 88 | return msg[randint(0, len(msg)-1)] 89 | 90 | def messageWeighMore(self, weight, weightChange, unit): 91 | weight = float("{0:.2f}".format(weight)) 92 | weightChange = float("{0:.2f}".format(weightChange)) 93 | msg = [] 94 | msg.append("With the new day comes new strength and new thoughts. You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 95 | msg.append("There’s no such thing as failure: either you win, or you learn. You gained " + str(weightChange) + " " + unit + " since last time (" + str(weight) + " " + unit + ").") 96 | msg.append("The past cannot be changed, the future is yet in your power. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 97 | msg.append("When you feel like quitting, think about why you started. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 98 | msg.append("Some people want it to happen. Some wish it would happen. Others make it happen. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 99 | msg.append("Only I can change my life. No one can do it for me. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 100 | msg.append("The greatest wealth is health. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 101 | msg.append("Do what you don’t want to do to get what you want to get. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 102 | msg.append("You can be pitiful, or you can be powerful, but you can’t be both. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 103 | msg.append("To climb steep hills requires slow pace at first. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 104 | msg.append("Take care of your body. It’s the only place you have to live. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 105 | msg.append("Success is not final, failure is not fatal: it is the courage to continue that counts. You gained " + str(weightChange) + " " + unit + " (" + str(weight) + " " + unit + ")") 106 | return msg[randint(0, len(msg)-1)] 107 | 108 | def messageWeighSame(self, weight, weightChange, unit): 109 | weight = float("{0:.2f}".format(weight)) 110 | weightChange = float("{0:.2f}".format(weightChange)) 111 | msg = [] 112 | msg.append("The groundwork of all happiness is health You weigh. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 113 | msg.append("What do you call a fake noodle? An impasta. 🍝 Your weight didn't change much since last time. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 114 | msg.append("The scale is merely a measure of my relationship with gravity. You weigh " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 115 | msg.append("Eliminate the mindset of can’t — because you can do anything. You weigh " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 116 | msg.append("If I don’t eat junk, I don’t gain weight. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 117 | msg.append("Ban pre-shredded cheese. Make America grate again. 🧀 Your weight didn't change much since last time. " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 118 | msg.append("Success is the sum of small efforts — repeated day-in and day-out. You weigh " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 119 | msg.append("The mind is everything. We become what we think about. You weigh " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 120 | msg.append("Success is nothing more than a few simple disciplines, practiced every day. You weigh " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 121 | msg.append("Whatever the mind can conceive and believe, it can achieve. You weigh " + str(weight) + " " + unit + " (" + str(weightChange) + " " + unit + " change)") 122 | return msg[randint(0, len(msg)-1)] 123 | 124 | def mass(self, event): 125 | if (event.totalWeight > 2): 126 | if self._measureCnt < WEIGHT_SAMPLES: 127 | if self._measureCnt == 1: 128 | print "Measuring ..." 129 | self.streamer.log("Update", "Measuring ...") 130 | self.streamer.flush() 131 | 132 | if METRIC_UNITS: 133 | self._events[self._measureCnt] = event.totalWeight 134 | self._unit = "kg" 135 | else: 136 | self._events[self._measureCnt] = event.totalWeight*2.20462 137 | self._unit = "lb" 138 | self._measureCnt += 1 139 | if self._measureCnt == WEIGHT_SAMPLES: 140 | 141 | # Average multiple measurements to get the weight and stream it 142 | self._prevWeight = self._weight 143 | self._sum = 0 144 | for x in range(THROWAWAY_SAMPLES, WEIGHT_SAMPLES-1): 145 | self._sum += self._events[x] 146 | self._weight = self._sum/(WEIGHT_SAMPLES-THROWAWAY_SAMPLES) 147 | if self._measured: 148 | self._weightChange = self._weight - self._prevWeight 149 | if self._weightChange < -0.4: 150 | self._msg = self.messageWeighLess(self._weight, self._weightChange, self._unit) 151 | elif self._weightChange > 0.4: 152 | self._msg = self.messageWeighMore(self._weight, self._weightChange, self._unit) 153 | else: 154 | self._msg = self.messageWeighSame(self._weight, self._weightChange, self._unit) 155 | else: 156 | self._msg = self.messageWeighFirst(self._weight, self._unit) 157 | print self._msg 158 | self.streamer.log("Update", self._msg) 159 | tmpVar = "Weight(" + self._unit + ")" 160 | self.streamer.log(str(tmpVar), float("{0:.2f}".format(self._weight))) 161 | tmpVar = time.strftime("%x %I:%M %p") 162 | self.streamer.log("Weigh Date", tmpVar) 163 | self.streamer.flush() 164 | 165 | # Store a small history of weights and overwite any measurement less than 2 hours old (7200 seconds) 166 | if self._weightCnt > 0: 167 | if (time.time() - self._times[self._weightCnt-1]) < 7200: 168 | self._tmpVar = time.time() - self._times[self._weightCnt-1] 169 | self._weightCnt -= 1 170 | self._weights[self._weightCnt] = self._weight 171 | self._times[self._weightCnt] = time.time() 172 | self._weightCnt += 1 173 | # Send an extra update at the end of WEIGHT_HISTORY 174 | if self._weightCnt == WEIGHT_HISTORY: 175 | self._weightCnt = 0 176 | self._weightChange = self._weights[WEIGHT_HISTORY-1] - self._weights[0] 177 | self._weightChange = float("{0:.2f}".format(self._weightChange)) 178 | timeChange = (self._times[WEIGHT_HISTORY-1] - self._times[0])/86400 179 | timeChange = float("{0:.1f}".format(timeChange)) 180 | if self._weightChange > 0: 181 | self._msg = "🕒 You gained " + str(self._weightChange) + " " + self._unit + " in the last " + str(timeChange) + " days!" 182 | else: 183 | self._msg = "🕒 You lost " + str(abs(self._weightChange)) + " " + self._unit + " in the last " + str(timeChange) + " days!" 184 | self.streamer.log("Update", self._msg) 185 | self.streamer.flush() 186 | 187 | # Keep track of the first complete measurement 188 | if not self._measured: 189 | self._measured = True 190 | else: 191 | self._measureCnt = 0 192 | 193 | @property 194 | def weight(self): 195 | if not self._events: 196 | return 0 197 | histogram = collections.Counter(round(num, 1) for num in self._events) 198 | return histogram.most_common(1)[0][0] 199 | 200 | 201 | class BoardEvent: 202 | def __init__(self, topLeft, topRight, bottomLeft, bottomRight, buttonPressed, buttonReleased): 203 | 204 | self.topLeft = topLeft 205 | self.topRight = topRight 206 | self.bottomLeft = bottomLeft 207 | self.bottomRight = bottomRight 208 | self.buttonPressed = buttonPressed 209 | self.buttonReleased = buttonReleased 210 | #convenience value 211 | self.totalWeight = topLeft + topRight + bottomLeft + bottomRight 212 | 213 | class Wiiboard: 214 | def __init__(self, processor): 215 | # Sockets and status 216 | self.receivesocket = None 217 | self.controlsocket = None 218 | 219 | self.processor = processor 220 | self.calibration = [] 221 | self.calibrationRequested = False 222 | self.LED = False 223 | self.address = None 224 | self.buttonDown = False 225 | for i in xrange(3): 226 | self.calibration.append([]) 227 | for j in xrange(4): 228 | self.calibration[i].append(10000) # high dummy value so events with it don't register 229 | 230 | self.status = "Disconnected" 231 | self.lastEvent = BoardEvent(0, 0, 0, 0, False, False) 232 | 233 | try: 234 | self.receivesocket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 235 | self.controlsocket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 236 | except ValueError: 237 | raise Exception("Error: Bluetooth not found") 238 | 239 | def isConnected(self): 240 | return self.status == "Connected" 241 | 242 | # Connect to the Wiiboard at bluetooth address 243 | def connect(self, address): 244 | if address is None: 245 | print "Non existant address" 246 | return 247 | self.receivesocket.connect((address, 0x13)) 248 | self.controlsocket.connect((address, 0x11)) 249 | if self.receivesocket and self.controlsocket: 250 | print "Connected to Wiiboard at address " + address 251 | self.status = "Connected" 252 | self.address = address 253 | self.calibrate() 254 | useExt = ["00", COMMAND_REGISTER, "04", "A4", "00", "40", "00"] 255 | self.send(useExt) 256 | self.setReportingType() 257 | print "Wiiboard connected" 258 | else: 259 | print "Could not connect to Wiiboard at address " + address 260 | 261 | def receive(self): 262 | while self.status == "Connected" and not self.processor.done: 263 | data = self.receivesocket.recv(25) 264 | intype = int(data.encode("hex")[2:4]) 265 | if intype == INPUT_STATUS: 266 | # TODO: Status input received. It just tells us battery life really 267 | self.setReportingType() 268 | elif intype == INPUT_READ_DATA: 269 | if self.calibrationRequested: 270 | packetLength = (int(str(data[4]).encode("hex"), 16) / 16 + 1) 271 | self.parseCalibrationResponse(data[7:(7 + packetLength)]) 272 | 273 | if packetLength < 16: 274 | self.calibrationRequested = False 275 | elif intype == EXTENSION_8BYTES: 276 | self.processor.mass(self.createBoardEvent(data[2:12])) 277 | else: 278 | print "ACK to data write received" 279 | 280 | def disconnect(self): 281 | if self.status == "Connected": 282 | self.status = "Disconnecting" 283 | while self.status == "Disconnecting": 284 | self.wait(100) 285 | try: 286 | self.receivesocket.close() 287 | except: 288 | pass 289 | try: 290 | self.controlsocket.close() 291 | except: 292 | pass 293 | print "WiiBoard disconnected" 294 | 295 | # Try to discover a Wiiboard 296 | def discover(self): 297 | print "Press the red sync button on the board now" 298 | address = None 299 | bluetoothdevices = bluetooth.discover_devices(duration=6, lookup_names=True) 300 | for bluetoothdevice in bluetoothdevices: 301 | if bluetoothdevice[1] == BLUETOOTH_NAME: 302 | address = bluetoothdevice[0] 303 | print "Found Wiiboard at address " + address 304 | if address is None: 305 | print "No Wiiboards discovered." 306 | return address 307 | 308 | def createBoardEvent(self, bytes): 309 | buttonBytes = bytes[0:2] 310 | bytes = bytes[2:12] 311 | buttonPressed = False 312 | buttonReleased = False 313 | 314 | state = (int(buttonBytes[0].encode("hex"), 16) << 8) | int(buttonBytes[1].encode("hex"), 16) 315 | if state == BUTTON_DOWN_MASK: 316 | buttonPressed = True 317 | if not self.buttonDown: 318 | print "Button pressed" 319 | self.buttonDown = True 320 | 321 | if not buttonPressed: 322 | if self.lastEvent.buttonPressed: 323 | buttonReleased = True 324 | self.buttonDown = False 325 | print "Button released" 326 | 327 | rawTR = (int(bytes[0].encode("hex"), 16) << 8) + int(bytes[1].encode("hex"), 16) 328 | rawBR = (int(bytes[2].encode("hex"), 16) << 8) + int(bytes[3].encode("hex"), 16) 329 | rawTL = (int(bytes[4].encode("hex"), 16) << 8) + int(bytes[5].encode("hex"), 16) 330 | rawBL = (int(bytes[6].encode("hex"), 16) << 8) + int(bytes[7].encode("hex"), 16) 331 | 332 | topLeft = self.calcMass(rawTL, TOP_LEFT) 333 | topRight = self.calcMass(rawTR, TOP_RIGHT) 334 | bottomLeft = self.calcMass(rawBL, BOTTOM_LEFT) 335 | bottomRight = self.calcMass(rawBR, BOTTOM_RIGHT) 336 | boardEvent = BoardEvent(topLeft, topRight, bottomLeft, bottomRight, buttonPressed, buttonReleased) 337 | return boardEvent 338 | 339 | def calcMass(self, raw, pos): 340 | val = 0.0 341 | #calibration[0] is calibration values for 0kg 342 | #calibration[1] is calibration values for 17kg 343 | #calibration[2] is calibration values for 34kg 344 | if raw < self.calibration[0][pos]: 345 | return val 346 | elif raw < self.calibration[1][pos]: 347 | val = 17 * ((raw - self.calibration[0][pos]) / float((self.calibration[1][pos] - self.calibration[0][pos]))) 348 | elif raw > self.calibration[1][pos]: 349 | val = 17 + 17 * ((raw - self.calibration[1][pos]) / float((self.calibration[2][pos] - self.calibration[1][pos]))) 350 | 351 | return val 352 | 353 | def getEvent(self): 354 | return self.lastEvent 355 | 356 | def getLED(self): 357 | return self.LED 358 | 359 | def parseCalibrationResponse(self, bytes): 360 | index = 0 361 | if len(bytes) == 16: 362 | for i in xrange(2): 363 | for j in xrange(4): 364 | self.calibration[i][j] = (int(bytes[index].encode("hex"), 16) << 8) + int(bytes[index + 1].encode("hex"), 16) 365 | index += 2 366 | elif len(bytes) < 16: 367 | for i in xrange(4): 368 | self.calibration[2][i] = (int(bytes[index].encode("hex"), 16) << 8) + int(bytes[index + 1].encode("hex"), 16) 369 | index += 2 370 | 371 | # Send to the Wiiboard 372 | # should be an array of strings, each string representing a single hex byte 373 | def send(self, data): 374 | if self.status != "Connected": 375 | return 376 | data[0] = "52" 377 | 378 | senddata = "" 379 | for byte in data: 380 | byte = str(byte) 381 | senddata += byte.decode("hex") 382 | 383 | self.controlsocket.send(senddata) 384 | 385 | #Turns the power button LED on if light is True, off if False 386 | #The board must be connected in order to set the light 387 | def setLight(self, light): 388 | if light: 389 | val = "10" 390 | else: 391 | val = "00" 392 | 393 | message = ["00", COMMAND_LIGHT, val] 394 | self.send(message) 395 | self.LED = light 396 | 397 | def calibrate(self): 398 | message = ["00", COMMAND_READ_REGISTER, "04", "A4", "00", "24", "00", "18"] 399 | self.send(message) 400 | self.calibrationRequested = True 401 | 402 | def setReportingType(self): 403 | bytearr = ["00", COMMAND_REPORTING, CONTINUOUS_REPORTING, EXTENSION_8BYTES] 404 | self.send(bytearr) 405 | 406 | def wait(self, millis): 407 | time.sleep(millis / 1000.0) 408 | 409 | 410 | def main(): 411 | processor = EventProcessor() 412 | 413 | board = Wiiboard(processor) 414 | if len(sys.argv) == 1: 415 | print "Discovering board..." 416 | address = board.discover() 417 | else: 418 | address = sys.argv[1] 419 | 420 | try: 421 | # Disconnect already-connected devices. 422 | # This is basically Linux black magic just to get the thing to work. 423 | subprocess.check_output(["bluez-test-input", "disconnect", address], stderr=subprocess.STDOUT) 424 | subprocess.check_output(["bluez-test-input", "disconnect", address], stderr=subprocess.STDOUT) 425 | except: 426 | pass 427 | 428 | print "Trying to connect..." 429 | board.connect(address) # The wii board must be in sync mode at this time 430 | board.wait(200) 431 | # Flash the LED so we know we can step on. 432 | board.setLight(False) 433 | board.wait(500) 434 | board.setLight(True) 435 | board.receive() 436 | 437 | if __name__ == "__main__": 438 | main() 439 | -------------------------------------------------------------------------------- /wiiboard_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import collections 4 | import time 5 | import bluetooth 6 | import sys 7 | import subprocess 8 | 9 | # --------- User Settings --------- 10 | WEIGHT_SAMPLES = 250 11 | # --------------------------------- 12 | 13 | # Wiiboard Parameters 14 | CONTINUOUS_REPORTING = "04" # Easier as string with leading zero 15 | COMMAND_LIGHT = 11 16 | COMMAND_REPORTING = 12 17 | COMMAND_REQUEST_STATUS = 15 18 | COMMAND_REGISTER = 16 19 | COMMAND_READ_REGISTER = 17 20 | INPUT_STATUS = 20 21 | INPUT_READ_DATA = 21 22 | EXTENSION_8BYTES = 32 23 | BUTTON_DOWN_MASK = 8 24 | TOP_RIGHT = 0 25 | BOTTOM_RIGHT = 1 26 | TOP_LEFT = 2 27 | BOTTOM_LEFT = 3 28 | BLUETOOTH_NAME = "Nintendo RVL-WBC-01" 29 | 30 | 31 | class EventProcessor: 32 | def __init__(self): 33 | self._measured = False 34 | self.done = False 35 | self._measureCnt = 0 36 | self._events = range(WEIGHT_SAMPLES) 37 | 38 | def mass(self, event): 39 | if self._measureCnt == 1: 40 | print "Measuring ..." 41 | 42 | if (event.totalWeight > 2): 43 | self._events[self._measureCnt] = event.totalWeight*2.20462 44 | self._measureCnt += 1 45 | if self._measureCnt == WEIGHT_SAMPLES: 46 | self._sum = 0 47 | for x in range(0, WEIGHT_SAMPLES-1): 48 | self._sum += self._events[x] 49 | self._weight = self._sum/WEIGHT_SAMPLES 50 | self._measureCnt = 0 51 | print str(self._weight) + " lbs" 52 | if not self._measured: 53 | self._measured = True 54 | 55 | @property 56 | def weight(self): 57 | if not self._events: 58 | return 0 59 | histogram = collections.Counter(round(num, 1) for num in self._events) 60 | return histogram.most_common(1)[0][0] 61 | 62 | 63 | class BoardEvent: 64 | def __init__(self, topLeft, topRight, bottomLeft, bottomRight, buttonPressed, buttonReleased): 65 | 66 | self.topLeft = topLeft 67 | self.topRight = topRight 68 | self.bottomLeft = bottomLeft 69 | self.bottomRight = bottomRight 70 | self.buttonPressed = buttonPressed 71 | self.buttonReleased = buttonReleased 72 | #convenience value 73 | self.totalWeight = topLeft + topRight + bottomLeft + bottomRight 74 | 75 | class Wiiboard: 76 | def __init__(self, processor): 77 | # Sockets and status 78 | self.receivesocket = None 79 | self.controlsocket = None 80 | 81 | self.processor = processor 82 | self.calibration = [] 83 | self.calibrationRequested = False 84 | self.LED = False 85 | self.address = None 86 | self.buttonDown = False 87 | for i in xrange(3): 88 | self.calibration.append([]) 89 | for j in xrange(4): 90 | self.calibration[i].append(10000) # high dummy value so events with it don't register 91 | 92 | self.status = "Disconnected" 93 | self.lastEvent = BoardEvent(0, 0, 0, 0, False, False) 94 | 95 | try: 96 | self.receivesocket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 97 | self.controlsocket = bluetooth.BluetoothSocket(bluetooth.L2CAP) 98 | except ValueError: 99 | raise Exception("Error: Bluetooth not found") 100 | 101 | def isConnected(self): 102 | return self.status == "Connected" 103 | 104 | # Connect to the Wiiboard at bluetooth address 105 | def connect(self, address): 106 | if address is None: 107 | print "Non existant address" 108 | return 109 | self.receivesocket.connect((address, 0x13)) 110 | self.controlsocket.connect((address, 0x11)) 111 | if self.receivesocket and self.controlsocket: 112 | print "Connected to Wiiboard at address " + address 113 | self.status = "Connected" 114 | self.address = address 115 | self.calibrate() 116 | useExt = ["00", COMMAND_REGISTER, "04", "A4", "00", "40", "00"] 117 | self.send(useExt) 118 | self.setReportingType() 119 | print "Wiiboard connected" 120 | else: 121 | print "Could not connect to Wiiboard at address " + address 122 | 123 | def receive(self): 124 | while self.status == "Connected" and not self.processor.done: 125 | data = self.receivesocket.recv(25) 126 | intype = int(data.encode("hex")[2:4]) 127 | if intype == INPUT_STATUS: 128 | # TODO: Status input received. It just tells us battery life really 129 | self.setReportingType() 130 | elif intype == INPUT_READ_DATA: 131 | if self.calibrationRequested: 132 | packetLength = (int(str(data[4]).encode("hex"), 16) / 16 + 1) 133 | self.parseCalibrationResponse(data[7:(7 + packetLength)]) 134 | 135 | if packetLength < 16: 136 | self.calibrationRequested = False 137 | elif intype == EXTENSION_8BYTES: 138 | self.processor.mass(self.createBoardEvent(data[2:12])) 139 | else: 140 | print "ACK to data write received" 141 | 142 | def disconnect(self): 143 | if self.status == "Connected": 144 | self.status = "Disconnecting" 145 | while self.status == "Disconnecting": 146 | self.wait(100) 147 | try: 148 | self.receivesocket.close() 149 | except: 150 | pass 151 | try: 152 | self.controlsocket.close() 153 | except: 154 | pass 155 | print "WiiBoard disconnected" 156 | 157 | # Try to discover a Wiiboard 158 | def discover(self): 159 | print "Press the red sync button on the board now" 160 | address = None 161 | bluetoothdevices = bluetooth.discover_devices(duration=6, lookup_names=True) 162 | for bluetoothdevice in bluetoothdevices: 163 | if bluetoothdevice[1] == BLUETOOTH_NAME: 164 | address = bluetoothdevice[0] 165 | print "Found Wiiboard at address " + address 166 | if address is None: 167 | print "No Wiiboards discovered." 168 | return address 169 | 170 | def createBoardEvent(self, bytes): 171 | buttonBytes = bytes[0:2] 172 | bytes = bytes[2:12] 173 | buttonPressed = False 174 | buttonReleased = False 175 | 176 | state = (int(buttonBytes[0].encode("hex"), 16) << 8) | int(buttonBytes[1].encode("hex"), 16) 177 | if state == BUTTON_DOWN_MASK: 178 | buttonPressed = True 179 | if not self.buttonDown: 180 | print "Button pressed" 181 | self.buttonDown = True 182 | 183 | if not buttonPressed: 184 | if self.lastEvent.buttonPressed: 185 | buttonReleased = True 186 | self.buttonDown = False 187 | print "Button released" 188 | 189 | rawTR = (int(bytes[0].encode("hex"), 16) << 8) + int(bytes[1].encode("hex"), 16) 190 | rawBR = (int(bytes[2].encode("hex"), 16) << 8) + int(bytes[3].encode("hex"), 16) 191 | rawTL = (int(bytes[4].encode("hex"), 16) << 8) + int(bytes[5].encode("hex"), 16) 192 | rawBL = (int(bytes[6].encode("hex"), 16) << 8) + int(bytes[7].encode("hex"), 16) 193 | 194 | topLeft = self.calcMass(rawTL, TOP_LEFT) 195 | topRight = self.calcMass(rawTR, TOP_RIGHT) 196 | bottomLeft = self.calcMass(rawBL, BOTTOM_LEFT) 197 | bottomRight = self.calcMass(rawBR, BOTTOM_RIGHT) 198 | boardEvent = BoardEvent(topLeft, topRight, bottomLeft, bottomRight, buttonPressed, buttonReleased) 199 | return boardEvent 200 | 201 | def calcMass(self, raw, pos): 202 | val = 0.0 203 | #calibration[0] is calibration values for 0kg 204 | #calibration[1] is calibration values for 17kg 205 | #calibration[2] is calibration values for 34kg 206 | if raw < self.calibration[0][pos]: 207 | return val 208 | elif raw < self.calibration[1][pos]: 209 | val = 17 * ((raw - self.calibration[0][pos]) / float((self.calibration[1][pos] - self.calibration[0][pos]))) 210 | elif raw > self.calibration[1][pos]: 211 | val = 17 + 17 * ((raw - self.calibration[1][pos]) / float((self.calibration[2][pos] - self.calibration[1][pos]))) 212 | 213 | return val 214 | 215 | def getEvent(self): 216 | return self.lastEvent 217 | 218 | def getLED(self): 219 | return self.LED 220 | 221 | def parseCalibrationResponse(self, bytes): 222 | index = 0 223 | if len(bytes) == 16: 224 | for i in xrange(2): 225 | for j in xrange(4): 226 | self.calibration[i][j] = (int(bytes[index].encode("hex"), 16) << 8) + int(bytes[index + 1].encode("hex"), 16) 227 | index += 2 228 | elif len(bytes) < 16: 229 | for i in xrange(4): 230 | self.calibration[2][i] = (int(bytes[index].encode("hex"), 16) << 8) + int(bytes[index + 1].encode("hex"), 16) 231 | index += 2 232 | 233 | # Send to the Wiiboard 234 | # should be an array of strings, each string representing a single hex byte 235 | def send(self, data): 236 | if self.status != "Connected": 237 | return 238 | data[0] = "52" 239 | 240 | senddata = "" 241 | for byte in data: 242 | byte = str(byte) 243 | senddata += byte.decode("hex") 244 | 245 | self.controlsocket.send(senddata) 246 | 247 | #Turns the power button LED on if light is True, off if False 248 | #The board must be connected in order to set the light 249 | def setLight(self, light): 250 | if light: 251 | val = "10" 252 | else: 253 | val = "00" 254 | 255 | message = ["00", COMMAND_LIGHT, val] 256 | self.send(message) 257 | self.LED = light 258 | 259 | def calibrate(self): 260 | message = ["00", COMMAND_READ_REGISTER, "04", "A4", "00", "24", "00", "18"] 261 | self.send(message) 262 | self.calibrationRequested = True 263 | 264 | def setReportingType(self): 265 | bytearr = ["00", COMMAND_REPORTING, CONTINUOUS_REPORTING, EXTENSION_8BYTES] 266 | self.send(bytearr) 267 | 268 | def wait(self, millis): 269 | time.sleep(millis / 1000.0) 270 | 271 | 272 | def main(): 273 | processor = EventProcessor() 274 | 275 | board = Wiiboard(processor) 276 | if len(sys.argv) == 1: 277 | print "Discovering board..." 278 | address = board.discover() 279 | else: 280 | address = sys.argv[1] 281 | 282 | try: 283 | # Disconnect already-connected devices. 284 | # This is basically Linux black magic just to get the thing to work. 285 | subprocess.check_output(["bluez-test-input", "disconnect", address], stderr=subprocess.STDOUT) 286 | subprocess.check_output(["bluez-test-input", "disconnect", address], stderr=subprocess.STDOUT) 287 | except: 288 | pass 289 | 290 | print "Trying to connect..." 291 | board.connect(address) # The wii board must be in sync mode at this time 292 | board.wait(200) 293 | # Flash the LED so we know we can step on. 294 | board.setLight(False) 295 | board.wait(500) 296 | board.setLight(True) 297 | board.receive() 298 | 299 | if __name__ == "__main__": 300 | main() 301 | --------------------------------------------------------------------------------