├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── bug_report_ja.md │ ├── feature_request.md │ └── feature_request_ja.md ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── build.yml │ └── bundle.yml ├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── CODE_OF_CONDUCT_ja.md ├── CONTRIBUTING.md ├── GUIDELINE.md ├── GUIDELINE_ja.md ├── LICENSE ├── README.md ├── README_ja.md ├── docs ├── assembly.md ├── assembly_ja.md └── images │ ├── assembly │ ├── assembling_back_screws.jpg │ ├── back_screws.jpg │ ├── board_assembled.jpg │ ├── board_shell_assembling.jpg │ ├── board_shell_attaching.jpg │ ├── born_assembled.jpg │ ├── born_backpack.jpg │ ├── born_backpack_attaching.jpg │ ├── born_backpack_sliding.jpg │ ├── born_base.jpg │ ├── born_feet.jpg │ ├── born_foot_horn.jpg │ ├── born_purge.jpg │ ├── born_purge_en.jpg │ ├── born_shell.jpg │ ├── born_shell_assembled.jpg │ ├── born_slide_shell.jpg │ ├── cable_connecting.jpg │ ├── disassembling_feet.jpg │ ├── disassembling_screw.jpg │ ├── disassembling_servo.jpg │ ├── disk_horn_rotation_1.jpg │ ├── disk_horn_rotation_1_en.png │ ├── disk_horn_rotation_2.jpg │ ├── disk_horn_rotation_2_en.png │ ├── feet_bottom_assembled.jpg │ ├── feet_cutout.jpg │ ├── m5_attaching.jpg │ ├── parts6-2_en.jpg │ ├── parts_1.jpg │ ├── parts_1_en.jpg │ ├── parts_2.jpg │ ├── parts_2_en.jpg │ ├── parts_3.jpg │ ├── parts_3_en.jpg │ ├── parts_4.jpg │ ├── parts_4_en.jpg │ ├── parts_5.jpg │ ├── parts_5_en.jpg │ ├── parts_6-2.jpg │ ├── parts_6.jpg │ ├── parts_6_en.jpg │ ├── parts_7.jpg │ ├── parts_7_en.jpg │ ├── servo_and_feet_protrusions.jpg │ ├── servo_and_horn_protrusions.jpg │ ├── servo_protrusion_focus.jpg │ ├── servo_wired.jpg │ ├── stack-chan_assembled.jpg │ ├── tightening_feet_screw.jpg │ └── tightening_horn_screw.jpg │ └── stack-chan_main_2400x2400_350dpi_rgb.jpg ├── firmware ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── README.md ├── README_ja.md ├── docs │ ├── about_stackchan_ja.md │ ├── api.md │ ├── api_ja.md │ ├── flashing-firmware.md │ ├── flashing-firmware_ja.md │ ├── getting-started-wsl2_ja.md │ ├── getting-started.md │ ├── getting-started_ja.md │ ├── images │ │ ├── architecture.drawio.png │ │ ├── architecture_ja.drawio.png │ │ ├── cheerup.gif │ │ ├── connect.jpg │ │ ├── coordinate.jpg │ │ ├── face-sync.gif │ │ ├── face-tracker.gif │ │ ├── getting-started-wsl2_ja │ │ │ ├── apt_update.jpg │ │ │ ├── deploy.jpg │ │ │ ├── git_clone.jpg │ │ │ ├── grep_config_spiram.jpg │ │ │ ├── install_ubuntu22.jpg │ │ │ ├── launch_poweshell.jpg │ │ │ ├── launch_ubuntu2.jpg │ │ │ ├── lsusb.jpg │ │ │ ├── npm_install.jpg │ │ │ ├── npm_node.jpg │ │ │ ├── npm_run_deploy.jpg │ │ │ ├── npm_run_doctor.jpg │ │ │ ├── npm_run_setup.jpg │ │ │ ├── npm_run_setup_esp32.jpg │ │ │ ├── reset_button.jpg │ │ │ ├── root_boot.jpg │ │ │ ├── set_xs-dev_env.jpg │ │ │ ├── setted_ubuntu.jpg │ │ │ ├── stackchan_connected_focus.jpg │ │ │ ├── stackchan_connected_pc.jpg │ │ │ ├── ubuntu.jpg │ │ │ ├── ubuntu22_1st_launch.jpg │ │ │ ├── ubuntu_attached.jpg │ │ │ ├── unset_psram.jpg │ │ │ ├── usb-ipd_install_1.jpg │ │ │ ├── usb-ipd_install_2.jpg │ │ │ ├── usb-ipd_install_3.jpg │ │ │ ├── usbipd_list_1.jpg │ │ │ ├── usbipd_list_2.jpg │ │ │ ├── usbipd_list_3.jpg │ │ │ ├── usbipd_list_4.jpg │ │ │ ├── usp-ipd_download.jpg │ │ │ ├── volta_reboot.jpg │ │ │ └── windows_powershell.jpg │ │ ├── host-and-mod.jpg │ │ ├── mimic.gif │ │ ├── stackchan.gif │ │ └── xsbug.png │ ├── text-to-speech.md │ └── text-to-speech_ja.md ├── mods │ ├── README.md │ ├── README_ja.md │ ├── beacon_advertiser │ │ ├── manifest.json │ │ └── mod.js │ ├── beacon_scanner │ │ ├── .gitignore │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── manifest.json │ │ ├── mod.js │ │ └── speeches_greeting.js │ ├── chatgpt │ │ ├── .gitignore │ │ ├── README.md │ │ ├── README_ja.md │ │ ├── api-key.js │ │ ├── manifest.json │ │ └── mod.js │ ├── cheerup_ble_lite │ │ ├── assets │ │ │ ├── 01-yay.wav │ │ │ ├── 02-hooray.wav │ │ │ ├── 03-wow.wav │ │ │ ├── 04-wonderful.wav │ │ │ └── 05-congrats.wav │ │ ├── manifest.json │ │ ├── mod.js │ │ └── speeches_cheerup.js │ ├── cheerup_ws │ │ ├── manifest.json │ │ └── mod.js │ ├── elevenlabs │ │ ├── README.md │ │ └── README_ja.md │ ├── face │ │ ├── manifest.json │ │ └── mod.js │ ├── face_tracker │ │ ├── manifest.json │ │ └── mod.js │ ├── look_around │ │ ├── manifest.json │ │ └── mod.js │ ├── mimic_follow │ │ ├── manifest.json │ │ └── mod.js │ ├── mimic_main │ │ ├── manifest.json │ │ └── mod.js │ └── monologue │ │ ├── .gitignore │ │ ├── assets │ │ └── .gitkeep │ │ ├── manifest.json │ │ ├── mod.js │ │ └── speeches_monologue.js ├── package-lock.json ├── package.json ├── scripts │ ├── .eslintrc │ ├── README.md │ ├── README_ja.md │ ├── generate-speech-coqui.js │ ├── generate-speech-google.js │ ├── generate-speech-voicevox.js │ └── pitch-shift.js ├── setting_scripts │ ├── set_xs-dev_env.sh │ └── unset_psram.sh ├── stackchan │ ├── assets │ │ └── sounds │ │ │ ├── speeches_en.js │ │ │ └── speeches_ja.js │ ├── ble │ │ ├── beacon-packet.ts │ │ ├── bleservices │ │ │ └── stk.json │ │ ├── manifest_ble.json │ │ └── stk-server.js │ ├── default-mods │ │ ├── mod.ts │ │ ├── on-launch.ts │ │ └── on-robot-created.ts │ ├── dialogues │ │ ├── dialogue-chatgpt.ts │ │ └── manifest_dialogue.json │ ├── drivers │ │ ├── dynamixel-driver.ts │ │ ├── dynamixel.ts │ │ └── manifest_driver.json │ ├── main.ts │ ├── manifest.json │ ├── manifest_local.json │ ├── manifest_typings.json │ ├── renderers │ │ ├── decorator.ts │ │ ├── dog-face.ts │ │ ├── manifest_renderer.json │ │ ├── modifier.ts │ │ ├── renderer-base.ts │ │ └── simple-face.ts │ ├── robot.ts │ ├── services │ │ ├── manifest_service.json │ │ ├── network-service.ts │ │ └── preference-server.ts │ ├── speeches │ │ ├── calculate-power.js │ │ ├── manifest_speech.json │ │ ├── manifest_wavstream.json │ │ ├── tts-elevenlabs.ts │ │ ├── tts-local.ts │ │ ├── tts-remote.ts │ │ └── tts-voicevox.ts │ ├── touch.ts │ └── utilities │ │ ├── consts.ts │ │ ├── manifest_utility.json │ │ └── stackchan-util.ts ├── tests │ ├── drivers │ │ ├── dynamixel │ │ │ ├── main.ts │ │ │ └── manifest.json │ │ └── serial │ │ │ ├── main.ts │ │ │ └── manifest.json │ ├── renderers │ │ ├── render-balloon │ │ │ ├── main.ts │ │ │ └── manifest.json │ │ └── render-face │ │ │ ├── main.ts │ │ │ └── manifest.json │ ├── services │ │ └── network-service │ │ │ ├── main.ts │ │ │ └── manifest.json │ └── speeches │ │ ├── tts-elevenlabs │ │ ├── main.ts │ │ └── manifest.json │ │ └── tts-local │ │ ├── main.ts │ │ └── manifest.json ├── typings │ ├── bleserver.d.ts │ ├── btutils.d.ts │ ├── elevenlabsstreamer.d.ts │ ├── fetch.d.ts │ ├── piu │ │ └── All.d.ts │ ├── resourcestreamer.d.ts │ ├── uartserver.d.ts │ ├── url.d.ts │ └── wavstreamer.d.ts └── workspace.code-workspace ├── package-lock.json └── web ├── .gitignore ├── .prettierrc ├── flash ├── flash.css ├── index.html ├── manifest_esp32_m5stack fire.json ├── manifest_esp32_m5stack.json ├── manifest_esp32_m5stack_core2.json └── manifest_esp32_m5stack_cores3.json ├── global.css ├── index.html ├── package-lock.json ├── package.json └── preference ├── ble-client.mjs ├── index.html └── preference.css /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: meganetaaan 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report bugs that occurred during development or use. 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Run '...' 13 | 2. If '...' 14 | 3. See '...' error 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Logs** 20 | If any compile error occurs, add a full compile log. 21 | 22 | ````log 23 | ```` 24 | 25 | **Screenshot**. 26 | Please attach a screenshot or picture to illustrate the bug, if any. 27 | 28 | **Environment (please fill in the following fields):**. 29 | 30 | - OS: [Windows, MacOS, Linux(Ubuntu/Arch/etc.)]. 31 | - IDE: [Arduino IDE, VSCode, UIFlow]] 32 | - Stack Chan Version 33 | - Type of M5Stack to use. 34 | 35 | **Other**. 36 | Please describe any other information that may be related to the problem. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_ja.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 不具合報告 3 | about: 開発時や使用中に起きた不具合の報告 4 | 5 | --- 6 | 7 | **不具合の概要** 8 | 不具合の概要を記述してください 9 | 10 | **再現手順** 11 | 不具合を再現できる手順を記述してください 12 | 1. '...'を実行する 13 | 2. もし'....'なら 14 | 3. '....'というエラーが発生する 15 | 16 | **想定する挙動** 17 | 想定する挙動を記述してください。 18 | 19 | **ログ** 20 | コンパイルエラーなどが発生する場合、ログの全文をコピペしてください。 21 | 22 | ```log 23 | ``` 24 | 25 | **スクリーンショット** 26 | もしあれば、不具合を説明するためのスクリーンショットや写真を添付してください。 27 | 28 | **環境 (次の項目を埋めてください):** 29 | 30 | - OS: [Windows, MacOS, Linux(Ubuntu/Arch/etc.)] 31 | - IDE: [Arduino IDE, VSCode, UIFlow] 32 | - スタックチャンのバージョン 33 | - 使用するM5Stackの種類 34 | 35 | **その他** 36 | その他、不具合に関係していそうな情報があれば記述してください。 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Stack-chan to be better 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_ja.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 機能リクエスト 3 | about: スタックチャンをより良くする機能のアイディア 4 | 5 | --- 6 | 7 | **既存の機能に問題がある場合、その説明** 8 | 何が問題なのか、明確かつ簡潔な説明。例 ファームウェア実装時に[...]が分かりづらいです。[...]の機能を実装したいのですが、動作しません。 9 | 10 | **あなたが望む解決策**。 11 | どんなことをしてほしいか簡潔な説明。 12 | 13 | **検討した代替案** 14 | 検討した代替案や機能についての明確かつ簡潔な説明。 15 | 16 | **その他** 17 | 機能リクエストに関するその他のコンテキストやスクリーンショットをここに追加してください。 18 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js, ModdableSDK and ESP-IDF environments 3 | inputs: 4 | target-branch: 5 | description: "Target branch to setup environment" 6 | required: false 7 | default: "" 8 | runs: 9 | using: composite 10 | steps: 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: 16 14 | - name: Cache node modules 15 | uses: actions/cache@v3 16 | env: 17 | cache-name: cache-node-modules 18 | with: 19 | path: ./firmware/node_modules 20 | key: ${{ runner.os }}-npm-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 21 | - run: npm ci 22 | working-directory: ./firmware 23 | shell: bash 24 | - name: install Moddable 25 | run: | 26 | sudo apt-get update 27 | if [ -z "${{ inputs.target-branch }}" ]; then 28 | npm run setup 29 | else 30 | npm run setup -- --target-branch ${{ inputs.target-branch }} 31 | fi 32 | working-directory: ./firmware 33 | shell: bash 34 | - name: install ESP32 35 | run: | 36 | sudo apt-get update 37 | source $HOME/.local/share/xs-dev-export.sh && npm run setup -- --device=esp32 38 | working-directory: ./firmware 39 | shell: bash 40 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Stack-chan Firmware 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-22.04 6 | steps: 7 | - uses: actions/checkout@v3 8 | with: 9 | submodules: recursive 10 | - uses: ./.github/actions/setup 11 | - name: Build 12 | run: source $HOME/.local/share/xs-dev-export.sh && sh ./setting_scripts/unset_psram.sh && npm run build 13 | working-directory: ./firmware 14 | - name: Build CoreS3 15 | run: source $HOME/.local/share/xs-dev-export.sh && sh ./setting_scripts/unset_psram.sh && npm run build --target=esp32/m5stack_cores3 16 | working-directory: ./firmware 17 | - name: Check Format 18 | run: npm run format 19 | working-directory: ./firmware 20 | - name: Lint 21 | run: npm run lint 22 | working-directory: ./firmware 23 | -------------------------------------------------------------------------------- /.github/workflows/bundle.yml: -------------------------------------------------------------------------------- 1 | name: Bundle Stack-chan Firmware 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | - name: Check for changes in firmware 17 | id: diff_check 18 | run: | 19 | git diff --quiet HEAD^ HEAD -- ./firmware/ || echo "::set-output name=diff_detected::true" 20 | - name: Cache build results 21 | id: cache 22 | uses: actions/cache@v3 23 | with: 24 | path: ./firmware/stackchan/tech.moddable.stackchan 25 | key: ${{ github.sha }} 26 | - name: Setup 27 | if: steps.diff_check.outputs.diff_detected || steps.cache.outputs.cache-hit != 'true' 28 | uses: ./.github/actions/setup 29 | - name: Bundle 30 | if: steps.diff_check.outputs.diff_detected || steps.cache.outputs.cache-hit != 'true' 31 | run: source $HOME/.local/share/xs-dev-export.sh && sh ./setting_scripts/unset_psram.sh && npm run bundle 32 | working-directory: ./firmware 33 | - name: Upload Firmware Bundle 34 | if: steps.diff_check.outputs.diff_detected || steps.cache.outputs.cache-hit != 'true' 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: firmware-bundle 38 | path: ./firmware/stackchan/tech.moddable.stackchan 39 | 40 | deploy: 41 | needs: build 42 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout Pages Branch 46 | uses: actions/checkout@v3 47 | with: 48 | ref: gh-pages 49 | - name: Download Firmware Bundle 50 | uses: actions/download-artifact@v4 51 | with: 52 | name: firmware-bundle 53 | path: ./firmware-bundle 54 | - name: Move Bundle 55 | run: | 56 | mkdir -p ./web/flash/tech.moddable.stackchan 57 | rm -rf ./web/flash/tech.moddable.stackchan/* 58 | mv firmware-bundle/* ./web/flash/tech.moddable.stackchan 59 | - name: Commit and Push 60 | run: | 61 | git config --global user.name 'GitHub Action' 62 | git config --global user.email 'action@github.com' 63 | git add . 64 | git commit -m "Deploy firmware bundle from ${{ github.sha }}" 65 | git push -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ### https://raw.github.com/github/gitignore/e931ef7f3e7d8f7aa0e784c14bd291ad4448b1ab/KiCad.gitignore 3 | 4 | # For PCBs designed using KiCad: http://www.kicad-pcb.org/ 5 | # Format documentation: http://kicad-pcb.org/help/file-formats/ 6 | 7 | # Temporary files 8 | *.000 9 | *.bak 10 | *.bck 11 | *.kicad_pcb-bak 12 | *.sch-bak 13 | *~ 14 | _autosave-* 15 | *.tmp 16 | *-save.pro 17 | *-save.kicad_pcb 18 | fp-info-cache 19 | 20 | # Netlist files (exported from Eeschema) 21 | *.net 22 | 23 | # Autorouter files (exported from Pcbnew) 24 | *.dsn 25 | *.ses 26 | 27 | # Exported BOM files 28 | *.xml 29 | #*.csv 30 | 31 | # Gerber archive 32 | *.zip 33 | 34 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "firmware/extern/avatar"] 2 | path = firmware/extern/avatar 3 | url = https://github.com/meganetaaan/moddable-avatar.git 4 | [submodule "firmware/extern/rs30x"] 5 | path = firmware/extern/rs30x 6 | url = https://github.com/meganetaaan/moddable-rs30x.git 7 | [submodule "firmware/extern/scservo"] 8 | path = firmware/extern/scservo 9 | url = https://github.com/meganetaaan/moddable-scservo.git 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT_ja.md: -------------------------------------------------------------------------------- 1 | # コントリビューター行動規範 2 | 3 | ## 私たちの約束 4 | メンバー、コントリビューター、およびリーダーとして、年齢、体の大きさ、目に見えるまたは目に見えない障害、民族性、性別、 5 | 性同一性、表現、経験のレベル、教育、社会経済的地位、国籍、人格、人種、宗教、または性的同一性と指向に関係なく、 6 | コミュニティへの参加をハラスメントのない体験にすることを誓います。 7 | 8 | 私たちは、オープンで親しみやすく、多様で包括的で健全なコミュニティに貢献する方法で行動し、交流することを誓います。 9 | 10 | ## 私たちの標準 11 | 12 | 前向きな環境を作り上げることに貢献する行動の例: 13 | 14 | * 他人への共感と優しさを示す 15 | 16 | * 異なる意見、視点、経験を尊重する 17 | 18 | * 建設的なフィードバックを与え、優雅に受け入れる 19 | 20 | * 私たちの過ちの影響を受けた人々に責任を受け入れ、謝罪し、そしてその経験から学ぶ 21 | 22 | * 個人としてだけでなく、コミュニティ全体にとっても最善であることに焦点を当てる 23 | 24 | 許容できない行動の例は次のとおりです。 25 | 26 | * 性的な言葉や画像の使用、および性的な注意またはその他あらゆる種類の問題行為 27 | 28 | * トローリング、侮辱的または中傷的なコメント、個人的または政治的攻撃 29 | 30 | * 公的またはプライベートの嫌がらせ 31 | 32 | * 明示的な許可なしに、住所や電子メールアドレスなど、他者の個人情報を公開する 33 | 34 | * 職業上不適切と合理的に考えられるその他の行為 35 | 36 | ## 執行責任 37 | 38 | コミュニティリーダーは、許容される行動の基準を明確にし、実施する責任があり、不適切、脅迫的、攻撃的、または有害と見なされる行動に応じて、適切で公正な是正措置を講じます。 39 | 40 | コミュニティリーダーは、コメント、コミット、コード、wikiの編集、問題、およびこの行動規範に沿っていないその他の貢献を削除、編集、または拒否する権利と責任を持ち、適切な場合はモデレーションの決定の理由を伝えます。 41 | 42 | ## 適用範囲 43 | 44 | この行動規範は、すべてのコミュニティスペース内で適用され、個人がパブリックスペースでコミュニティを公式に代表している場合にも適用されます。 45 | 私たちのコミュニティを代表する例には、公式の電子メールアドレスの使用、公式のソーシャルメディアアカウントを介した投稿、オンラインまたはオフラインのイベントでの指定代理人としての行動などがあります。 46 | 47 | ## 執行 48 | 49 | 虐待的、嫌がらせ、またはその他の許容できない行動の事例は、執行を担当するコミュニティリーダーに対してE-mail(ishikawa.s.1027@gmail.com)で報告される場合があります。 50 | すべての苦情は迅速かつ公正にレビューおよび調査されます。 51 | 52 | すべてのコミュニティリーダーは、問題の報告者のプライバシーとセキュリティを尊重する義務があります。 53 | 54 | ## 執行ガイドライン 55 | 56 | コミュニティリーダーは、この行動規範に違反していると見なした行動への帰結を判断する際に、これらのコミュニティガイドラインに従います。 57 | 58 | ### 1. 更生 59 | 60 | **コミュニティへの影響**: コミュニティで専門家にふさわしくない、または歓迎されないと思われる不適切な言葉の使用やその他の不適切な行動をすること。 61 | 62 | **帰結**: コミュニティリーダーからの非公開の書面による警告。違反の理由を明確にし、行動が不適切だった理由を説明します。 公の謝罪が要求される場合があります。 63 | 64 | ### 2. 警告 65 | 66 | **コミュニティへの影響**: 単一の出来事または一連の動作による違反。 67 | 68 | **帰結**: 持続的な行動の結果を伴う警告。 指定された期間、行動規範の実施者との一方的な対話を含め、関係者との対話はありません。 これには、コミュニティスペースやソーシャルメディアなどの外部チャネルでの相互作用の回避が含まれます。 これらの条件に違反すると、一時的または永続的に禁止される場合があります。 69 | 70 | ### 3. 一時的な禁止 71 | **コミュニティへの影響**: 持続的で不適切な行動を含む、コミュニティ標準の重大な違反。 72 | 73 | **帰結**: 指定された期間のコミュニティとのあらゆる種類の相互関係または公的なコミュニケーションの一時的な禁止。 この期間中、行動規範を実施する人々との一方的な対話を含め、関係する人々との公的または私的な対話は許可されません。 74 | これらの条件に違反すると、永久的に禁止される場合があります。 75 | ### 4. 永久的な禁止 76 | **コミュニティへの影響**: 連続的な不適切な行動、個人への嫌がらせ、または個人の集団に対する攻撃または名誉毀損を含む、コミュニティの標準への違反のパターンを示す。 77 | 78 | **帰結**: コミュニティ内でのあらゆる種類の公的な相互関係の永久的な禁止。 79 | 80 | ## 帰属 81 | この行動規範は、https://www.contributor-covenant.org/version/2/0/code_of_conduct.html で利用可能な [Contributor Covenant][homepage] バージョン2.0を基に作成されています。 82 | 83 | コミュニティへの影響ガイドラインは[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity)に適合しています。 84 | 85 | [homepage]: https://www.contributor-covenant.org 86 | この行動規範に関する一般的な質問への回答については、https://www.contributor-covenant.org/faq のFAQを参照してください。翻訳はhttps://www.contributor-covenant.org/translations で入手できます。 87 | 88 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 本リポジトリへのコントリビュート方法について記載しています。 2 | 3 | ## Issues 4 | 5 | リポジトリの品質向上にご協力頂きありがとうございます。 6 | 7 | Issueの作成を簡単にするテンプレートを用意しているので活用してください。 8 | 9 | ## Pull Requests 10 | 11 | Pull Requestの作成ありがとうございます。 提出したPull Request(PR)には次のルールが適用されます。 12 | 13 | PRの内容には本リポジトリのライセンス(LICENSEとREADME.mdに記載されています)が適用されます 14 | PRはrt-netのメンバーによるレビューを経てからマージされます 15 | すべてのPRがマージされるわけではなく、希望に添えない場合もありますのでご容赦ください 16 | リポジトリにテストが設定されている場合はできるだけテストを通してください 17 | 何かしらの理由(テストに間違いがある場合など)でテストを通さずPRを出す場合はその旨をPRに記載してください 18 | マージする際にはPR内の全コミットが1つのコミットにsquashされます 19 | コミットをスカッシュしてマージする | GitHub Docs 20 | 1つのPRでリクエストする変更はできるだけシンプルにしてください 21 | 異なる内容の変更を含む場合はPRを分割してください 22 | 例えば、複数の機能追加したり、機能追加とリファクタリングを同時にする場合はそれぞれ別々のPRとしてください 23 | squashマージしても履歴を辿りやすくするためです -------------------------------------------------------------------------------- /GUIDELINE.md: -------------------------------------------------------------------------------- 1 | # Derivative Work Guidelines 2 | 3 | This is guidelines for derivative works of Stack-chan. 4 | 5 | In order to make it easier for third parties to use Stack-chan's characters we declare the guidelines as below. 6 | If you follow these guidelines, you may use Stack-chan's characters for commercial and non-commercial purposes without permission. 7 | 8 | ## Scope 9 | 10 | ### Scope of "Stack-chan" 11 | 12 | "Stack-chan" refers to a robot created using only the data in this repository. 13 | Therefore, robots that are modified and redistributed by a third party using our data are not eligible. 14 | The following robots are not eligible: 15 | 16 | - The robot with a board other than M5Stack like "Obniz-chan", "Wio-terminal chan" or "Micro:bit-chan" 17 | - The robot that features are added to like bipedal walking 18 | 19 | When you redistribute derivative works of these robots, you must get a permission from the creators of the respective works. 20 | 21 | ### Scope of "derivative work" 22 | 23 | The term "derivative work" in the guidelines refers to creations that use the "Stack-chan" character. 24 | For example: 25 | 26 | - Photo collections and video works featuring Stack-chan robots 27 | - Cartoons and novels featuring Stack-chan 28 | - Stack-chan acrylic stands or key chains 29 | - Stack-chan plush toys 30 | 31 | On the other hand, if you use data from this repository to create something that is not Stack-chan, it is not eligible. 32 | The following usages are not eligible: 33 | 34 | - Using firmware source code to control IoT devices 35 | - Integrating servo motor brackets into different types of robots 36 | 37 | Since the data is distributed under an [open source license](./LICENSE), please follow the terms of the license. 38 | 39 | ## Compliance/Prohibitions. 40 | 41 | ### Add credit notation 42 | 43 | Please clearly state the following two points in places where users of your works can see them (at the back of books, product introduction pages, instruction manuals, etc.) 44 | 45 | 1. Stack-chan is developed and published by meganetaaan 46 | 2. The URL of Stack-chan's GitHub repository (https:// github.com/meganetaaan/stack-chan) for the reference 47 | 48 | Below is the example of acceptable description. 49 | 50 | Stack-chan is a hand-held super-kawaii communication robot developed and published by [meganetaaan](https://twitter.com/meganetaaan) 51 | Further details are: https://github.com/meganetaaan/stack-chan 52 | 53 | ### Be politically/religious/ideological neutral and no attack 54 | 55 | Please refrain from publishing second creations that include the following expressions: 56 | 57 | 1. Intended to harm the ideas or honor of Stack-chan contributors or third parties 58 | 2. Excessively supports or demeans a specific political, religious, or ideological belief 59 | 3. Antisocial 60 | 4. Violates on the rights of third parties 61 | 62 | ### Appropriate zoning 63 | 64 | Please use appropriate zoning for expressions that do not match the "prohibited items" but are highly stimulating, such as sex and violence. 65 | 66 | 1. Tag the images with `NSFW` or `R-18` (means "restricted under 18 years old", mostly used in Japan) 67 | 2. When publishing images on Twitter, etc., set a "content warning" so that users cannot view the images unless they open them with their own will. 68 | 69 | ## Questions 70 | 71 | If you have any questions about these guidelines, or if you want to do something but are not sure if you are following the guidelines, 72 | please submit an issue in this repository or contact meganetaaan (ishikawa.s.1027@gmail.com). 73 | -------------------------------------------------------------------------------- /GUIDELINE_ja.md: -------------------------------------------------------------------------------- 1 | # 二次創作ガイドライン 2 | 3 | スタックチャンの二次創作について案内します。 4 | 5 | スタックチャンのキャラクターを第三者が利用しやすくするために、ガイドラインを以下のとおり定めます。 6 | このガイドラインに従えば商用・非商用を問わず連絡不要でご利用いただけます。 7 | 8 | ## 対象 9 | 10 | ### 「スタックチャン」のキャラクターの範囲 11 | 12 | 「スタックチャン」は本リポジトリの管理化にあるデータのみを使って作られたロボットを指します。 13 | 従って、第三者がスタックチャンのデータを改修し、再配布しているロボットは __対象となりません__ 。 14 | 次のロボットは対象外となります。 15 | 16 | - M5Stack以外のボードと組み合わせた「オブナイズチャン」「ワイオターミナルチャン」「マイクロビットチャン」などの派生作品 17 | - スタックチャンに歩く機能を持たせた「二足歩行スタックチャン」 18 | 19 | これらのロボットの二次創作については、それぞれの作品の作者に許諾を得る必要があります。 20 | 21 | ### 「二次創作物」の範囲 22 | 23 | 本ガイドラインの「二次創作物」とは、上記「スタックチャン」のキャラクターを利用した創作物を指します。 24 | 例えば以下が該当します。 25 | 26 | - スタックチャンのロボットを撮影した写真集や動画作品 27 | - スタックチャンが登場する漫画、小説 28 | - スタックチャンのアクリルスタンドやキーホルダー 29 | - スタックチャンぬいぐるみ 30 | 31 | 一方、本リポジトリのデータを使って、スタックチャンでないものを作る場合は __対象となりません__。 32 | 次のような使い方は対象外となります。 33 | 34 | - ファームウェアのソースコードを使ってIoTデバイスを制御する 35 | - サーボモータのブラケットを異なる種類のロボットに組み込む 36 | 37 | データには別途[オープンソースライセンス](./LICENSE)が定められていますので、そちらの条項に従ってください。 38 | 39 | ## 遵守・禁止事項 40 | 41 | ### 説明とクレジットの明記 42 | 43 | 以下2点を、二次創作物の利用者が見える場所(書籍の奥付け、商品の紹介ページ、説明書など)に明記してください。 44 | 45 | 1. スタックチャンはししかわが作者であること 46 | 2. 参照先として、スタックチャンのGitHubリポジトリのURL(https://github.com/meganetaaan/stack-chan) 47 | 48 | 以下のような例文がどこかに貼ってあればOKです。 49 | 50 | スタックチャンは[ししかわ](https://twitter.com/meganetaaan)が開発、公開している、 51 | 手乗りサイズのスーパーカワイイコミュニケーションロボットです。 52 | 作品ページ:https://github.com/meganetaaan/stack-chan 53 | 54 | ### 政治・宗教・信条の中立を逸する表現や、第三者への攻撃の禁止 55 | 56 | 以下に当てはまる表現を含む二次創作物の公開はご遠慮ください。 57 | 58 | 1. スタックチャンのコントリビューター、または第三者の考え方や名誉などを害する目的のもの 59 | 2. 特定の政治・宗教・信条を過度に支援する、または貶めるもの 60 | 3. 反社会的な表現のもの 61 | 4. 第三者の権利を侵害するもの 62 | 63 | ### 適切なゾーニング 64 | 65 | R-18や暴力描写など、「禁止事項」には該当しないが刺激が強い表現は適切なゾーニングをお願いします。 66 | 67 | 1. `NSFW`(Not Safe For Work…職場では閲覧注意)や`R-18`のタグを付ける 68 | 2. Twitter等で画像を公開する際は「内容の警告」を設定して、ユーザの意思で開かないと閲覧できないようにする 69 | 70 | ## 問い合わせ 71 | 72 | 本ガイドラインに関する疑問点や「こういうことをしたいんだけどガイドラインを守れているか不安」のような問い合わせは 73 | 本リポジトリにissueを立てていただくか、ししかわ(ishikawa.s.1027@gmail.com)までご連絡ください。 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stack-chan RT ver. 2 | 3 | [![Build Stack-chan Firmware](https://github.com/meganetaaan/stack-chan/actions/workflows/build.yml/badge.svg)](https://github.com/meganetaaan/stack-chan/actions/workflows/build.yml) 4 | [![Discord server invitation](https://dcbadge.vercel.app/api/server/eGhd9adnBm)](https://discord.gg/eGhd9adnBm) 5 | 6 | [日本語](./README_ja.md) 7 | 8 | ![stackchan](./docs/images/stack-chan_main_2400x2400_350dpi_rgb.jpg) 9 | 10 | This is the repository for Stack-chan RT ver. 11 | 12 | * Official hashtag: [`#stackchan` | `#スタックチャン` (JP)](https://twitter.com/search?q=%23stackchan%20OR%20%23%EF%BD%BD%EF%BE%80%EF%BD%AF%EF%BD%B8%EF%BE%81%EF%BD%AC%EF%BE%9D). 13 | 14 | 15 | Stack-chan is a super cute, palm-sized communication robot developed and released in JavaScript by [Shinya Ishikawa](https://twitter.com/stack_chan). 16 | * Project page: https://github.com/stack-chan/stack-chan 17 | * Video (with English subtitles): https://youtu.be/fZb_mF08xV0 18 | 19 |
20 | 21 | The RT version introduces the following updates: 22 | 23 | * The dependent Moddable SDK version is now fixed at [4.9.5](https://github.com/Moddable-OpenSource/moddable/releases/tag/4.9.5). 24 | * Several updates have been made to the circuit diagram and board design. 25 | * The DYNAMIXEL XL330-M288-T servo motor has been integrated. 26 | * The robot's exterior casing is produced using injection molding. 27 | 28 | ## Features 29 | 30 | * :neutral_face: Show cute face 31 | * :smile: Expression(Happy, Angry, Sad etc.) 32 | * :smiley_cat: Customize face 33 | * :eyes: Glance/stare/gaze 34 | * :speech_balloon: Say things 35 | * :bulb: Addon M5Units 36 | * :cyclone: Drive Serial(TTL)/PWM servos 37 | * :game_die: Make applications on your own 38 | 39 | ## Contents 40 | 41 | This repository includes the following contents. 42 | 43 | * __firmware__ : Source codes of the firmware. 44 | 45 | ## Installation 46 | 47 | ### Assemble board 48 | 49 | * See [Stack-chan RT ver. Assembly Manual](docs/assembly.md) 50 | 51 | ### Flash firmware to M5Stack 52 | 53 | * For Windows: [(WSL2) Windows 11 Stack-chan Environment Setup Manual (Japanese)](firmware/docs/getting-started-wsl2_ja.md) 54 | * For MacOS/Linux: [Getting Started (MacOS/Linux)](./firmware/docs/getting-started.md) 55 | * For Web: Follow the steps below (Reference: [Tried Flashing a Program to Stack-chan via Web Browser (Japanese)](https://rt-net.jp/humanoid/archives/5907)): 56 | 1. Access the [web-flah page](https://rt-net.github.io/stack-chan/web/flash/) from your PC. 57 | 2. Connect Stack-chan to the PC using a cable. 58 | 3. Hold the reset button on the bottom of the M5Stack for 3 seconds to switch to BOOT mode (a green light will appear near the reset button). 59 | 4. Select `M5Stack CoreS3`. 60 | 5. Press the `Flash Stack-chan firmware [・_・]` button. 61 | 62 | ## Contribution 63 | 64 | We accept __feature requests / bug reports__ through the [issues](https://github.com/rt-net/stack-chan/issues) page. 65 | 66 | ## License 67 | 68 | Resources of this repository are distributed under Apache version 2.0 license. 69 | See [LICENSE](./LICENSE). 70 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | # スタックチャン アールティver.(Stack-chan RT ver.) 2 | 3 | [![ファームウェアのビルド](https://github.com/meganetaaan/stack-chan/actions/workflows/build.yml/badge.svg)](https://github.com/meganetaaan/stack-chan/actions/workflows/build.yml) 4 | [![Discordサーバへの招待](https://dcbadge.vercel.app/api/server/eGhd9adnBm)](https://discord.gg/eGhd9adnBm) 5 | 6 | [English](./README.md) 7 | 8 | ![stackchan](./docs/images/stack-chan_main_2400x2400_350dpi_rgb.jpg) 9 | 10 | スタックチャン アールティver. のリポジトリです。 11 | 12 | * 公式ハッシュタグ: [`#stackchan` | `#スタックチャン` (JP)](https://twitter.com/search?q=%23stackchan%20OR%20%23%EF%BD%BD%EF%BE%80%EF%BD%AF%EF%BD%B8%EF%BE%81%EF%BD%AC%EF%BE%9D). 13 | 14 | 15 | スタックチャンは[ししかわ](https://twitter.com/stack_chan)がJavaScriptで開発し、公開している、手乗りサイズのスーパーカワイイコミュニケーションロボットです。 16 | * 作品ページ:https://github.com/stack-chan/stack-chan 17 | * 動画: https://youtu.be/fZb_mF08xV0 18 | 19 |
20 | 21 | スタックチャン アールティver. では以下の変更が加えられています。 22 | 23 | * ファームウェアが依存するModdable SDKのバージョンを[4.9.5](https://github.com/Moddable-OpenSource/moddable/releases/tag/4.9.5)に固定しています 24 | * 回路図・基板を一部変更しています 25 | * サーボモータに DYNAMIXEL XL330-M288-T を採用しています 26 | * 本体を射出成形で製造しています 27 | 28 | 29 | 30 | ## 機能 31 | 32 | * :neutral_face: かわいい顔 33 | * :smile: 感情(喜び, 怒り, 悲しみ etc.) 34 | * :smiley_cat: 顔のカスタマイズ 35 | * :eyes: 視線を向ける 36 | * :speech_balloon: 喋る 37 | * :bulb: M5Unitを使う 38 | * :cyclone: シリアル(TTL)サーボを駆動する 39 | * :game_die: あなた自身のアプリケーションを作る 40 | 41 | ## コンテンツ 42 | 43 | 本リポジトリは以下の構成要素を含みます。 44 | 45 | * __firmware__ : ファームウェアのソースコード 46 | 47 | ## 製作方法 48 | 49 | ### モジュールを組み立てる 50 | 51 | [スタックチャン アールティver. 組み立てマニュアル](docs/assembly_ja.md)を参照ください。 52 | 53 | ### ファームウェアをM5Stackに書き込む 54 | 55 | * Windowsの場合:[Windows 11 のスタックチャン環境構築マニュアル(WSL2)](firmware/docs/getting-started-wsl2_ja.md) 56 | * MacOS/Linuxの場合:[環境構築(MacOS/Linux)マニュアル](./firmware/docs/getting-started_ja.md) 57 | * Webの場合:以下の手順を実行(参考:[Webブラウザからスタックチャンにプログラムを書き込んでみた](https://rt-net.jp/humanoid/archives/5907)) 58 | 1. PCから[web-flahページ](https://rt-net.github.io/stack-chan/web/flash/)にアクセス 59 | 2. スタックチャンとPCをケーブルで接続 60 | 3. M5Stackの下部にあるリセットボタンを3秒以上押し続けてBOOTモードに切替(切り替わるとリセットボタン付近が緑色に光ります) 61 | 4. `M5Stack CoreS3`を選択 62 | 5. `Flash Stack-chan firmware [・_・]`ボタンを押下 63 | 64 | ## コントリビューション 65 | 66 | 機能追加のリクエスト/バグ報告は[issues](https://github.com/rt-net/stack-chan/issues)のページから投稿を受け付けています。 67 | 68 | ## ライセンス 69 | 70 | このリポジトリ配下のリソースはApache version 2.0ライセンスのもと配布されています。 71 | [LICENSE](./LICENSE)を確認してください。 72 | -------------------------------------------------------------------------------- /docs/images/assembly/assembling_back_screws.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/assembling_back_screws.jpg -------------------------------------------------------------------------------- /docs/images/assembly/back_screws.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/back_screws.jpg -------------------------------------------------------------------------------- /docs/images/assembly/board_assembled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/board_assembled.jpg -------------------------------------------------------------------------------- /docs/images/assembly/board_shell_assembling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/board_shell_assembling.jpg -------------------------------------------------------------------------------- /docs/images/assembly/board_shell_attaching.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/board_shell_attaching.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_assembled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_assembled.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_backpack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_backpack.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_backpack_attaching.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_backpack_attaching.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_backpack_sliding.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_backpack_sliding.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_base.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_base.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_feet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_feet.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_foot_horn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_foot_horn.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_purge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_purge.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_purge_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_purge_en.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_shell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_shell.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_shell_assembled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_shell_assembled.jpg -------------------------------------------------------------------------------- /docs/images/assembly/born_slide_shell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/born_slide_shell.jpg -------------------------------------------------------------------------------- /docs/images/assembly/cable_connecting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/cable_connecting.jpg -------------------------------------------------------------------------------- /docs/images/assembly/disassembling_feet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/disassembling_feet.jpg -------------------------------------------------------------------------------- /docs/images/assembly/disassembling_screw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/disassembling_screw.jpg -------------------------------------------------------------------------------- /docs/images/assembly/disassembling_servo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/disassembling_servo.jpg -------------------------------------------------------------------------------- /docs/images/assembly/disk_horn_rotation_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/disk_horn_rotation_1.jpg -------------------------------------------------------------------------------- /docs/images/assembly/disk_horn_rotation_1_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/disk_horn_rotation_1_en.png -------------------------------------------------------------------------------- /docs/images/assembly/disk_horn_rotation_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/disk_horn_rotation_2.jpg -------------------------------------------------------------------------------- /docs/images/assembly/disk_horn_rotation_2_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/disk_horn_rotation_2_en.png -------------------------------------------------------------------------------- /docs/images/assembly/feet_bottom_assembled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/feet_bottom_assembled.jpg -------------------------------------------------------------------------------- /docs/images/assembly/feet_cutout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/feet_cutout.jpg -------------------------------------------------------------------------------- /docs/images/assembly/m5_attaching.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/m5_attaching.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts6-2_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts6-2_en.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_1.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_1_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_1_en.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_2.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_2_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_2_en.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_3.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_3_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_3_en.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_4.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_4_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_4_en.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_5.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_5_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_5_en.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_6-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_6-2.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_6.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_6_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_6_en.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_7.jpg -------------------------------------------------------------------------------- /docs/images/assembly/parts_7_en.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/parts_7_en.jpg -------------------------------------------------------------------------------- /docs/images/assembly/servo_and_feet_protrusions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/servo_and_feet_protrusions.jpg -------------------------------------------------------------------------------- /docs/images/assembly/servo_and_horn_protrusions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/servo_and_horn_protrusions.jpg -------------------------------------------------------------------------------- /docs/images/assembly/servo_protrusion_focus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/servo_protrusion_focus.jpg -------------------------------------------------------------------------------- /docs/images/assembly/servo_wired.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/servo_wired.jpg -------------------------------------------------------------------------------- /docs/images/assembly/stack-chan_assembled.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/stack-chan_assembled.jpg -------------------------------------------------------------------------------- /docs/images/assembly/tightening_feet_screw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/tightening_feet_screw.jpg -------------------------------------------------------------------------------- /docs/images/assembly/tightening_horn_screw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/assembly/tightening_horn_screw.jpg -------------------------------------------------------------------------------- /docs/images/stack-chan_main_2400x2400_350dpi_rgb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/docs/images/stack-chan_main_2400x2400_350dpi_rgb.jpg -------------------------------------------------------------------------------- /firmware/.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /build/ 3 | /tmp/ 4 | -------------------------------------------------------------------------------- /firmware/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "parser": "@typescript-eslint/parser", 6 | "parserOptions": { 7 | "ecmaVersion": "latest", 8 | "project": "./tsconfig.json" 9 | }, 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:prettier/recommended" 14 | ], 15 | "plugins": [ 16 | "@typescript-eslint", 17 | "eslint-plugin-tsdoc" 18 | ], 19 | "rules": { 20 | "tsdoc/syntax": "warn", 21 | "prettier/prettier": [ 22 | "error", 23 | { 24 | "singleQuote": true, 25 | "trailingComma": "es5", 26 | "arrowParens": "always" 27 | } 28 | ] 29 | }, 30 | "overrides": [ 31 | { 32 | "files": [ 33 | "*.js" 34 | ], 35 | "rules": { 36 | "@typescript-eslint/no-unused-vars": "off", 37 | "@typescript-eslint/no-var-requires": "off", 38 | "@typescript-eslint/explicit-function-return-type": "off" 39 | } 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /firmware/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /tmp/ 3 | /scripts/key.json 4 | /tsconfig.json 5 | /docs/api 6 | 7 | # Bundle file 8 | /stackchan/tech.moddable.stackchan* 9 | -------------------------------------------------------------------------------- /firmware/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /firmware/README.md: -------------------------------------------------------------------------------- 1 | # Stack-chan firmware 2 | 3 | [日本語](./README_ja.md) 4 | 5 | ## NOTE 6 | 7 | * __To those who arrived here looking for AI Stack-chan__ You may not find the information you're looking for here! "AI Stack-chan" is an Arduino-based application under development, primarily by @robo8080. 8 | * https://github.com/robo8080/AI_StackChan2 9 | * The firmware part is under active development. Breaking changes to the API may occur. 10 | * We are currently working on [issue](https://github.com/meganetaaan/stack-chan/issues/65) to make install steps more user friendly. Please post your feedback if any problem. 11 | * If you are friendly with Arduino IDE, [stack-chan-tester](https://github.com/mongonta0716/stack-chan-tester) by @mongonta0716 is another option to try (only for PWM servo). 12 | 13 | ## Features 14 | 15 | * Programming possible using JavaScript. 16 | * Only compatible with DYNAMIXEL servo motors. 17 | * Supports cloud-based text-to-speech (VOICEVOX, ElevenLabs). 18 | * Designed with separate host program and user applications (MODs). Flashing only MODs is very fast, allowing for an efficient development cycle. 19 | * [Supports firmware flashing from a web browser](docs/flashing-firmware-web_ja.md) 20 | 21 | ## Directory structure 22 | 23 | - [stackchan](./stackchan/): Firmware source code. 24 | - [mods](./mods/): Source code of mods. 25 | - [scripts](./scripts/): Scripts for Stack-chan's voice synthesis, etc. 26 | - [extern](./extern/): External modules. 27 | - [typings](./typings/): TypeScript type definition files (d.ts). 28 | - Stack-chan firmware is implemented in TypeScript, so no separate type definition files are needed. 29 | 30 | ## Documents 31 | 32 | - [Building Environment](docs/getting-started.md) 33 | - [Building and Writing Programs](docs/flashing-firmware.md) 34 | - [API](docs/api.md) 35 | - [MOD](mods/README.md) 36 | -------------------------------------------------------------------------------- /firmware/README_ja.md: -------------------------------------------------------------------------------- 1 | # スタックチャン ファームウェア 2 | 3 | [English](./README.md) 4 | 5 | ## 注意 6 | 7 | * __AIスタックチャンの情報を見てここにたどり着いた方へ__ ここには求める情報がないかもしれません!「AIスタックチャン」は@robo8080が中心となって開発中のArduinoベースのアプリケーションです。 8 | * https://github.com/robo8080/AI_StackChan2 9 | * ファームウェアは現在も積極的に開発中です。内部の作りやAPIが大きく変わる可能性があります。 10 | * 「Moddableの環境構築の手順が多くて大変」という課題に対して、現在[issue](https://github.com/meganetaaan/stack-chan/issues/65)を立てて対応中です。環境構築でつまづいた方はフィードバックをぜひお寄せください。 11 | * Arduino IDEになじみのある方は @mongonta0716 さんの[stack-chan-tester](https://github.com/mongonta0716/stack-chan-tester)もお試しください(PWMサーボのみ対応)。 12 | 13 | ## 特徴 14 | 15 | * JavaScriptを使ったプログラミングが可能 16 | * DYNAMIXELサーボモーターのみ対応 17 | * クラウド音声合成(VOICEVOX、ElevenLabs)に対応 18 | * ホストプログラムとユーザアプリケーション(MOD)が分離した設計。MODのみの書き換えは非常に高速なので、効率的に開発サイクルを回せます。 19 | * [Webブラウザからのファームウェア書き込み](docs/flashing-firmware-web_ja.md)に対応 20 | 21 | ## ディレクトリ構成 22 | 23 | - [stackchan](./stackchan/): ファームウェアのソースコードです。 24 | - [mods](./mods/): MODのソースコードです。 25 | - [scripts](./scripts/): スタックチャンの音声合成などに用いるスクリプトです。 26 | - [extern](./extern/): 外部のモジュールです。 27 | - [typings](./typings/): TypeScriptの型定義ファイル(d.ts)です。 28 | - ※スタックチャンのファームウェアは一部を除きTypeScriptで実装されているので別途型定義ファイルは必要ありませんが、Moddable SDKの新しめのモジュールは型定義ファイルが提供されていないため、それを補う用途で置いてあります。 29 | 30 | ## ドキュメント 31 | 32 | - [環境構築](docs/getting-started_ja.md) 33 | - [プログラムのビルドと書き込み](docs/flashing-firmware_ja.md) 34 | - [API](docs/api_ja.md) 35 | - [MOD](mods/README_ja.md) 36 | 37 | -------------------------------------------------------------------------------- /firmware/docs/about_stackchan_ja.md: -------------------------------------------------------------------------------- 1 | ### スタックチャンについて ### 2 | - M5StackCoreS3にRobitsサーボを組み合わせて作られています。 3 | - バッテリは、背中に背負っています。 4 | - M5StacKCores3はPCと繋がったUSBケーブルを接続すると起動しますが、起動しない場合は、電源ボタンを押すと電源が入ります。 5 | - M5stackCores3の電源を落とすには、電源ボタンを6秒以上押し続ける必要があります。 6 | - バッテリの充電は、M5StackCores3の電源をOFF、スライドスイッチをOnにし、USBケーブルから充電することができます。充電中はリセット端子の近くから赤い光が漏れます。充電が終わると赤い光は消灯します。 7 | - リセットボタンを3秒以上押し続けるとリセットボタンのところから緑の光が見えます。緑の光が見えた時は、プログラム書き込みモードになります。プログラムの書き込み時に書けない時書き込みモードにすると書き込みすることができます。 8 | - 開発していると記述が正しいはずなのにプログラムが意図しない動作をすることがあります。その時は、Flashをeraseすることで解決することがあります。eraseするとWindowsのWLS2ではスタックチャンをusbipdで認識できないためプログラム書き込みモードにする必要があります。 9 | - eraseコマンド 10 | - idfのパージョン(idf5.3)とpythonのバージョン(py3.12_env)はインストールしたバージョンにあわせてください。 11 | - ubuntu 12 | - /home/ubuntu/.espressif/python_env/idf5.3_py3.12_env/bin/esptool.py erase_flash 13 | - windows11(WLS2) 14 | - /home/ubuntu/.espressif/python_env/idf5.3_py3.10_env/bin/esptool.py erase_flash 15 | - macOS 16 | - /Users/ユーザ名/.espressif/python_env/idf5.3_py3.12_env/bin/esptool.py erase_flash -------------------------------------------------------------------------------- /firmware/docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | [日本語](./api_ja.md) 4 | 5 | The detailed API document is under construction. 6 | 7 | The source codes of Stack-chan has `TSDoc` style comments. 8 | 9 | For generating documents, you need `tsconfig.json` under `firmware` directory. 10 | To do this, run `build` task once. 11 | It automatically generates `tsconfig.json` and creates a link. 12 | 13 | ```console 14 | $ npm run build 15 | ... 16 | > stack-chan@0.2.1 postbuild /home/user/repos/stack-chan/firmware 17 | > ln -sf $MODDABLE/build/tmp/${npm_config_target=esp32/m5stack}/debug/stackchan/modules/tsconfig.json ./tsconfig.json 18 | 19 | $ file tsconfig.json 20 | tsconfig.json: symbolic link to /home/user/.local/share/moddable/build/tmp/esp32/m5stack/debug/stackchan/modules/tsconfig.json 21 | ``` 22 | 23 | Then you can generate documents under `docs/api` by running: 24 | 25 | ```console 26 | $ npm run generate-apidoc 27 | ``` 28 | 29 | ## Architecture 30 | 31 | The `Robot` class is used to access the functions of the Stack-chan. 32 | The following classes are defined to allow replacement and customization of Stack-chan functions. 33 | 34 | - [Renderer](#renderer): Draw a face 35 | - [Driver](#driver): Drives motors, etc. 36 | - [TTS](#tts): Speech synthesis 37 | 38 | // TODO: Class diagram and description 39 | 40 | ## Coordinate system 41 | 42 | ![coordinate for Stack-chan](./images/coordinate.jpg) 43 | 44 | Stack-chan's coordinate system is a __right-handed__ system. When you bend your right hand's thumb, index finger, and middle finger so that they are perpendicular to each other, the thumb is the X-axis, the index finger is the Y-axis, and the middle finger is the Z-axis. 45 | 46 | When Stack-chan's face is facing forward, the positive direction of each axis is as follows: 47 | 48 | - Positive direction of X-axis... front of the face 49 | - Positive direction of Y-axis... left side of the face 50 | - Positive direction of Z-axis... head side 51 | 52 | Also, the direction of rotation is the direction in which the right-hand screw advances in relation to the positive direction of the axis. In the case of Stack-chan's face, when rotating around each axis in the positive direction, it is as follows: 53 | 54 | - Roll axis (rotation around X-axis) positive direction... Clockwise head tilt as seen from Stack-chan 55 | - Pitch axis (rotation around Y-axis) positive direction... Stack-chan looking down 56 | - Yaw axis (rotation around Z-axis) positive direction... Stack-chan looking to the left 57 | 58 | In Stack-chan's API, __the unit of coordinates is meters and the unit of angles is radians__. 59 | Correspondence with the coordinate system can also be referenced in the actual source code (e.g. [`mods/look_around`](../mods/look_around/) etc.)" 60 | 61 | ## Classes 62 | 63 | ### Robot 64 | 65 | ### Renderer 66 | 67 | ### Driver 68 | 69 | ### TTS 70 | 71 | - [Using Text To Speech(TTS)](./text-to-speech.md) 72 | -------------------------------------------------------------------------------- /firmware/docs/api_ja.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | [English](./api.md) 4 | 5 | APIの詳しいドキュメントは現在作成中です。 6 | 7 | スタックチャンのソースコードには `TSDoc` 形式のコメントがついています。 8 | このコメントを元にマークダウン形式でAPIドキュメントを生成できます。 9 | 10 | APIドキュメントを生成するには、`firmware`ディレクトリの下に`tsconfig.json`が必要です。 11 | 一度Stack-chanのファームウェアをビルドすると`tsconfig.json`が自動的に生成され、リンクが作成されます。 12 | 13 | ```console 14 | $ npm run build 15 | ... 16 | > stack-chan@0.2.1 postbuild /home/user/repos/stack-chan/firmware 17 | > ln -sf $MODDABLE/build/tmp/${npm_config_target=esp32/m5stack}/debug/stackchan/modules/tsconfig.json ./tsconfig.json 18 | 19 | $ file tsconfig.json 20 | tsconfig.json: symbolic link to /home/user/.local/share/moddable/build/tmp/esp32/m5stack/debug/stackchan/modules/tsconfig.json 21 | ``` 22 | 23 | その後、次のコマンドを実行することで`docs/api`ディレクトリ配下にドキュメントを生成できます。 24 | 25 | ```console 26 | $ npm run generate-apidoc 27 | ``` 28 | 29 | ## クラス構成 30 | 31 | スタックチャンの機能にアクセスするには`Robot`クラスを使います。 32 | スタックチャンの機能の差し替えやカスタマイズができるように、次のクラスが定義されています。 33 | 34 | - [Renderer](#renderer): 顔の描画 35 | - [Driver](#driver): モータ等の駆動 36 | - [TTS](#tts): 音声合成 37 | 38 | // TODO: クラス図と説明 39 | 40 | ## 座標系 41 | 42 | ![スタックチャンの座標系](./images/coordinate.jpg) 43 | 44 | スタックチャンの座標系は __右手系__ です。 45 | 右手の親指、人差し指と中指がそれぞれ直行するように曲げたとき、 46 | 親指がX軸、人差し指がY軸、中指がZ軸となります。 47 | 48 | スタックチャンの顔が正面を向いているとき、各軸の正の方向は次のとおりです。 49 | 50 | - X軸の正方向…顔の前側 51 | - Y軸の正方向…顔の左側 52 | - Z軸の正方向…頭側 53 | 54 | また、回転の向きは軸の正の方向に対して右ねじが進む向きとなります。 55 | スタックチャンの顔でいうと、各軸の周りを正の方向へ回転する場合次のようになります。 56 | 57 | - ロール軸(X軸まわりの回転)の正方向…スタックチャンから見て時計回りに首をかしげる動き 58 | - ピッチ軸(Y軸まわりの回転)の正方向…スタックチャンが下を向く動き 59 | - ヨー軸(Z軸まわりの回転)の正方向…スタックチャンが左を向く動き 60 | 61 | スタックチャンのAPIにおいては __座標の単位はメートル、角度の単位はラジアンになります__ 。 62 | 座標系との対応は実際のソースコード([`mods/look_around`](../mods/look_around/)など)も参考にしてください。 63 | 64 | ## クラス 65 | 66 | ### Robot 67 | 68 | ### Renderer 69 | 70 | ### Driver 71 | 72 | ### TTS 73 | 74 | - [TTS(音声合成)の使用](./text-to-speech_ja.md) 75 | -------------------------------------------------------------------------------- /firmware/docs/getting-started_ja.md: -------------------------------------------------------------------------------- 1 | # 環境構築(MacOS/Linux)マニュアル 2 | 3 | [English](./getting-started.md) 4 | 5 | スタックチャンはWindows11、MacOS、Linuxで開発ができます。Windows 11の場合はWSL2を使った環境構築手順を参照してください。ここでは、MacOS/Linuxでの開発環境の方法を示します。 6 | 7 | * **[Windows 11 のスタックチャン環境構築マニュアル(WSL2)](./getting-started-wsl2_ja.md)** 8 | 9 | ## 開発に必要なもの 10 | 11 | * ホストPC 12 | * Linux(Ubuntu22.04 or Ubuntu24.04)でテスト済み 13 | * MacOS(Sonoma 14 Appleシリコン)でテスト済み 14 | * [スタックチャン アールティver.](https://rt-net.jp/products/rt-stackchan/) または その互換品 15 | * USB type-Cケーブル 16 | * 事前にインストールしておくアプリ 17 | * [cmake](https://cmake.org/) 18 | * [git](https://git-scm.com/) 19 | * [Node.js](https://nodejs.org/en/) 20 | * cherrup_ble_liteのmodに関しては、新しいNode.jsに対応していないためV18.x.xを使用する必要があります。 21 | * その他のmodはv22.8.xで動作することは確認しています。 22 | * Python3.12で動作確認ができています。(macOSはbrewでインストールするのではなく[https://www.python.org](https://www.python.org)からダウンロードしインストールしてください。) 23 | * xcode-select(macOSのみ) 24 | 25 | ## スタックチャンリポジトリのクローンとnodeのmoduleのインストール 26 | 27 | ```console 28 | $ git clone https://github.com/rt-net/stack-chan.git 29 | $ cd stack-chan/firmware 30 | $ npm install 31 | ``` 32 | 33 | ## ModdableSDKのセットアップ 34 | 35 | ホストPCで[ModdableSDK](https://github.com/Moddable-OpenSource/moddable)と 36 | [ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/index.html)をインストールします。 37 | 次の2通りの方法があります。 38 | 39 | - xs-dev(CLI)を使う(推奨) 40 | - 手動でセットアップする 41 | 42 | ### xs-dev(CLI)を使う(推奨) 43 | 44 | スタックチャンはセットアップ手順をnpmスクリプト化しています。 45 | `stack-chan/firmware`ディレクトリで次のコマンドを実行します。 46 | 47 | 以下に示す1つ目のコマンドの実行直後、Ubuntuに設定したパスワードの入力が要求されますので入力してください。 パスワード入力後、一定時間は同様のコマンドを実行してもパスワードは要求されません。 48 | 49 | 2つ目のコマンドでは、再度パスワードが要求されない内に実行してください。 もし、何らかの理由で1つめのコマンド実行から時間がかかってしまった場合は1つ目のコマンドの実行からやり直してください。 50 | 51 | ```console 52 | $ sudo echo "emporary SuperUser Grant" 53 | $ npm run setup 54 | $ npm run setup -- --device=esp32 55 | ``` 56 | 57 | macOSの場合は、npm run setup -- --device=esp32のインストールの時、xcode-selectのバージョンが古いと"Error: Command failed with exit code 1: python3 -m pip install pyserial"で止まることがあります。その場合は、xcode-selectを手動で削除してから再度xcode-select(xcord-select –install)をインストールしてください。 58 | xcode-selectの削除は"sudo rm -rf /Library/Developer/CommandLineTools"でできます。 59 | 内部で[`xs-dev`](https://github.com/HipsterBrown/xs-dev)を使ってModdableSDKやESP-IDFのセットアップを自動化しています。 60 | 61 | 62 | ### 手動でセットアップする 63 | 64 | [公式サイトの手順(英語)](https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/Moddable%20SDK%20-%20Getting%20Started.md)に従ってModdableSDKとESP-IDFをインストールします。 65 | xs-dev(CLI)でうまくセットアップできない場合はこちらを行ってください。 66 | 67 | - **スタックチャン アールティver.では、Moddable SDK 4.9.5、ESP-IDF 5.3.0 での動作を想定しています。** 68 | - **intel macはModdable SDK 4.7.0 + ESP-IDF 5.1.0 python3.9.0で動作することは確認しています。intel macで使用するには`firmware/package.json`の`"setup": "xs-dev setup --target-branch 4.9.5"`を`"setup": "xs-dev setup --target-branch 4.7.0"`にすることでインストールできますがサポート対象外になります。** 69 | 70 | ### PSRAMと環境変数のセットアップ 71 | 72 | 次のコマンドを実行して、PSRAMの設定をします。 73 | 74 | ```console 75 | $ ./setting_scripts/unset_psram.sh 76 | ``` 77 | 78 | 次のコマンドを実行し、Shellの設定ファイルに`source ~/.local/share/xs-dev-export.sh`を追加します。これにより、Shellの起動時に環境変数が設定されます。 79 | 80 | ```console 81 | $ ./setting_scripts/set_xs-dev_env.sh 82 | ``` 83 | 84 | ここまで完了したら、ターミナルを再起動してください。 85 | 86 | ## 環境のテスト 87 | 88 | `npm run doctor`コマンドで環境のテストができます。 89 | コマンドは、`stack-chan/firmware`配下で実行する必要があります。 90 | 91 | インストールに成功していれば次のようにModdable SDKのバージョンとして4.9.5が表示され、Supported target devicesにesp32が表示されます。 92 | 93 | 94 | ```console 95 | $ npm run doctor 96 | 97 | > stack-chan@0.2.1 doctor 98 | > echo stack-chan environment info: && git rev-parse HEAD && git rev-parse --show-toplevel && xs-dev doctor 99 | 100 | stack-chan environment info: 101 | 55d005ac9f0764a4ebc561b7d0a2a29a66ee5199 102 | /home/ubuntu/stack-chan 103 | xs-dev environment info: 104 | CLI Version 0.32.3 105 | OS Linux 106 | Arch x64 107 | Shell /bin/bash 108 | NodeJS Version v22.8.0 (/home/ubuntu/.nvm/versions/node/22.8.0/bin/node) 109 | Python Version 3.12.3 (/usr/bin/python) 110 | Moddable SDK Version 4.9.5 (/home/ubuntu/.local/share/moddable) 111 | Supported target devices lin, esp32 112 | ESP32 IDF Directory /home/ubuntu/.local/share/esp32/esp-idf 113 | ``` 114 | 115 | `grep CONFIG_SPIRAM= $MODDABLE/build/devices/esp32/targets/m5stack_cores3/sdkconfig/sdkconfig.defaults`コマンドでM5Stack CoreS3のPARAMの設定を確認できます。 116 | 設定が完了していれば、`CONFIG_SPIRAM=n`と出力されます。 117 | 118 | ```console 119 | $ grep CONFIG_SPIRAM= $MODDABLE/build/devices/esp32/targets/m5stack_cores3/sdkconfig/sdkconfig.defaults 120 | CONFIG_SPIRAM=n 121 | ``` 122 | 123 | ## 次のステップ 124 | 125 | - [プログラムのビルドと書き込み](./flashing-firmware_ja.md) 126 | -------------------------------------------------------------------------------- /firmware/docs/images/architecture.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/architecture.drawio.png -------------------------------------------------------------------------------- /firmware/docs/images/architecture_ja.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/architecture_ja.drawio.png -------------------------------------------------------------------------------- /firmware/docs/images/cheerup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/cheerup.gif -------------------------------------------------------------------------------- /firmware/docs/images/connect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/connect.jpg -------------------------------------------------------------------------------- /firmware/docs/images/coordinate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/coordinate.jpg -------------------------------------------------------------------------------- /firmware/docs/images/face-sync.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/face-sync.gif -------------------------------------------------------------------------------- /firmware/docs/images/face-tracker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/face-tracker.gif -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/apt_update.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/apt_update.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/deploy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/deploy.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/git_clone.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/git_clone.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/grep_config_spiram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/grep_config_spiram.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/install_ubuntu22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/install_ubuntu22.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/launch_poweshell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/launch_poweshell.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/launch_ubuntu2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/launch_ubuntu2.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/lsusb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/lsusb.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/npm_install.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/npm_install.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/npm_node.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/npm_node.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/npm_run_deploy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/npm_run_deploy.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/npm_run_doctor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/npm_run_doctor.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/npm_run_setup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/npm_run_setup.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/npm_run_setup_esp32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/npm_run_setup_esp32.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/reset_button.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/reset_button.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/root_boot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/root_boot.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/set_xs-dev_env.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/set_xs-dev_env.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/setted_ubuntu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/setted_ubuntu.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/stackchan_connected_focus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/stackchan_connected_focus.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/stackchan_connected_pc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/stackchan_connected_pc.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/ubuntu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/ubuntu.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/ubuntu22_1st_launch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/ubuntu22_1st_launch.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/ubuntu_attached.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/ubuntu_attached.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/unset_psram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/unset_psram.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/usb-ipd_install_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/usb-ipd_install_1.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/usb-ipd_install_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/usb-ipd_install_2.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/usb-ipd_install_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/usb-ipd_install_3.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/usbipd_list_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/usbipd_list_1.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/usbipd_list_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/usbipd_list_2.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/usbipd_list_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/usbipd_list_3.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/usbipd_list_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/usbipd_list_4.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/usp-ipd_download.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/usp-ipd_download.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/volta_reboot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/volta_reboot.jpg -------------------------------------------------------------------------------- /firmware/docs/images/getting-started-wsl2_ja/windows_powershell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/getting-started-wsl2_ja/windows_powershell.jpg -------------------------------------------------------------------------------- /firmware/docs/images/host-and-mod.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/host-and-mod.jpg -------------------------------------------------------------------------------- /firmware/docs/images/mimic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/mimic.gif -------------------------------------------------------------------------------- /firmware/docs/images/stackchan.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/stackchan.gif -------------------------------------------------------------------------------- /firmware/docs/images/xsbug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/docs/images/xsbug.png -------------------------------------------------------------------------------- /firmware/docs/text-to-speech.md: -------------------------------------------------------------------------------- 1 | # Using Text To Speech(TTS) 2 | 3 | [日本語](./text-to-speech_ja.md) 4 | 5 | Currently there are two way to use TTS. 6 | 7 | * __Pregenerated__: Generates and flash speeches at buildtime and plays standalone. Suitable for predefined statements. 8 | * __Remote__: Queries speech statements to the TTS server, then streams the voice generated. 9 | 10 | __Local(On demand)__ TTS such as aquestalk is not available for now pull requests are welcome! 11 | 12 | ## Prerequisites 13 | 14 | No matter which way you choose, you should prepare an extrenal TTS engine first. 15 | 16 | Tested below: 17 | 18 | * [Google Cloud Text-to-Speech API](https://cloud.google.com/text-to-speech) 19 | * [Coqui AI TTS](https://github.com/coqui-ai/TTS) 20 | * [VoiceVox](https://github.com/Hiroshiba/voicevox_engine) 21 | * [ElevenLabs](https://elevenlabs.io/speech-synthesis) 22 | 23 | See also official documents of each of them. 24 | 25 | ### Google Cloud TTS 26 | 27 | * Get through [this authentication guide](https://cloud.google.com/docs/authentication/getting-started) and generate key.json 28 | * Save `key.json` under `scripts` directory 29 | 30 | ### Coqui AI TTS 31 | 32 | * Install coqui-ai/TTS 33 | * Launch server 34 | 35 | ```sh 36 | $ tts-server --port 8080 --model_name tts_models/ja/kokoro/tacotron2-DDC 37 | ``` 38 | 39 | * save server configuration under `config.tts.host|port` of `stackchan/manifest_local.json` 40 | 41 | ```json 42 | { 43 | "config": { 44 | "tts": { 45 | "host": "your.tts.host.local", 46 | "port": 8080 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | ### ElevenLabs TTS 53 | * Get through [API KEY](https://docs.elevenlabs.io/authentication/01-xi-api-key) and get API KEY. 54 | * Set API KEY to `config.tts` of `stackchan/manifest_local.json`. 55 | ```json 56 | { 57 | "config": { 58 | "tts": { 59 | "type": "elevenlabs", 60 | "token": "YOUR_API_KEY" 61 | }, 62 | } 63 | } 64 | ``` 65 | 66 | ## Usage(Pregenerated) 67 | 68 | * write down sentenses to speech in the format below (See `mods/monologue/speeches_monologue.js` and other examples) 69 | 70 | ```javascript 71 | // speeches.js 72 | export const speeches = { 73 | niceToMeetYou: 'Hello. I am Stach-chan. Nice to meet you.', 74 | hello: 'Hello World.', 75 | konnichiwa: 'Konnichiwa.', 76 | nihao: 'Nee hao.', 77 | } 78 | ``` 79 | 80 | * Run `npm run generate-speech-[google|coqui|voicevox]` 81 | * this script get voice data from server and saves wave files under `stackchan/assets/sounds` 82 | * Flash firmware with assets 83 | * Call `Robot#speak(sentense: string)` with the sentense. 84 | 85 | ```javascript 86 | import { speeches } from 'speeches' 87 | const keys = Object.keys(speeches) 88 | 89 | export async function onRobotCreated(robot) { 90 | await robot.say('hello') 91 | await robot.say(keys[0] /* 'niceToMeetYou' */) 92 | } 93 | ``` 94 | 95 | ## Usage(Remote) 96 | 97 | * Set `config.tts.type` according to your TTS server in `manifest_local.json` 98 | 99 | ```json 100 | { 101 | "config": { 102 | "tts": { 103 | "type": "remote", 104 | "host": "your.tts.host.local", 105 | "port": 8080 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | * Call `Robot#say(sentense: string)` 112 | 113 | ```javascript 114 | // ... 115 | export async function onRobotCreated(robot) { 116 | await robot.say('Now I can speak any sentense you want.') 117 | } 118 | ``` 119 | -------------------------------------------------------------------------------- /firmware/docs/text-to-speech_ja.md: -------------------------------------------------------------------------------- 1 | # TTS(音声合成)の使用 2 | 3 | [English](./text-to-speech.md) 4 | 5 | 現在、TTSを使用するには2つの方法があります。 6 | 7 | * __事前生成__:ビルド時に音声を生成し、スタンドアロンで再生します。事前に定義された文章に適しています。 8 | * __リモート__:TTSサーバに音声文章を問い合わせ、生成された音声をストリームします。 9 | 10 | 現在、アクエストークのような__ローカル(オンデマンド)__のTTSは利用できませんが、プルリクエストは歓迎します! 11 | 12 | ## 前提条件 13 | 14 | どちらの方法を選んでも、まず外部のTTSエンジンを準備する必要があります。 15 | 16 | 以下がTTSののエンジンの候補になります。VoiceVoxはテスト済みです: 17 | 18 | * [Google Cloud Text-to-Speech API](https://cloud.google.com/text-to-speech) 19 | * [Coqui AI TTS](https://github.com/coqui-ai/TTS) 20 | * [VoiceVox](https://github.com/Hiroshiba/voicevox_engine) 21 | * [ElevenLabs](https://elevenlabs.io/speech-synthesis) 22 | 23 | それぞれの公式ドキュメントも参照してください。 24 | 25 | ### Google Cloud TTS 26 | 27 | * [認証ガイド](https://cloud.google.com/docs/authentication/getting-started)を通じて認証し、key.jsonを生成します。 28 | * `key.json`を`scripts`ディレクトリに保存します。 29 | 30 | ### Coqui AI TTS 31 | 32 | * coqui-ai/TTSをインストールします。 33 | * サーバを起動します。 34 | 35 | ```sh 36 | $ tts-server --port 8080 --model_name tts_models/ja/kokoro/tacotron2-DDC 37 | ``` 38 | 39 | * `stackchan/manifest_local.json`の`config.tts.host|port`にサーバー設定を保存します 40 | 41 | ```json 42 | { 43 | "config": { 44 | "tts": { 45 | "host": "your.tts.host.local", 46 | "port": 8080 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | ### ElevenLabs TTS 53 | 54 | * [API KEY](https://docs.elevenlabs.io/authentication/01-xi-api-key)に従い、API KEYを取得します。 55 | * `stackchan/manifest_local.json`の`config.tts`にAPI KEYを保存します 56 | 57 | ```json 58 | { 59 | "config": { 60 | "tts": { 61 | "type": "elevenlabs", 62 | "token": "YOUR_API_KEY" 63 | }, 64 | } 65 | } 66 | ``` 67 | 68 | ## 使用方法(事前生成) 69 | 70 | 以下のようなJavaScriptファイルに発話する文章を書き込みます(`mods/monologue/speeches_monologue.js`などを参照)。 71 | 72 | ```javascript 73 | // speeches.js 74 | export const speeches = { 75 | niceToMeetYou: 'Hello. I am Stach-chan. Nice to meet you.', 76 | hello: 'Hello World.', 77 | konnichiwa: 'Konnichiwa.', 78 | nihao: 'Nee hao.', 79 | } 80 | ``` 81 | 82 | * npm run generate-speech-[google|coqui|voicevox]を実行します 83 | * このスクリプトはサーバーから音声データを取得し、stackchan/assets/soundsにwaveファイルを保存します 84 | * 音声ファイルとともにファームウェアを書き込みます 85 | * `Robot#say(sentense: string)`を呼び出します 86 | 87 | ```javascript 88 | import { speeches } from 'speeches' 89 | const keys = Object.keys(speeches) 90 | 91 | export async function onRobotCreated(robot) { 92 | await robot.say('hello') 93 | await robot.say(keys[0] /* 'niceToMeetYou' */) 94 | } 95 | ``` 96 | 97 | ## 使用方法(リモート) 98 | 99 | * `manifest_local.json`の`config.tts.type`プロパティを使用するTTSエンジンに合わせて設定します。 100 | 101 | 102 | ```json 103 | { 104 | "config": { 105 | "tts": { 106 | "type": "remote", 107 | "host": "your.tts.host.local", 108 | "port": 8080 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | * `Robot#say(sentense: string)`を呼び出します。 115 | 116 | ```javascript 117 | // ... 118 | export async function onRobotCreated(robot) { 119 | await robot.say('Now I can speak any sentense you want.') 120 | } 121 | ``` 122 | -------------------------------------------------------------------------------- /firmware/mods/beacon_advertiser/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "resources": { 6 | "*": "../beacon_scanner/assets/*" 7 | }, 8 | "modules": { 9 | "*": [ 10 | "../beacon_scanner/speeches_greeting", 11 | "./mod" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /firmware/mods/beacon_advertiser/mod.js: -------------------------------------------------------------------------------- 1 | import BLEServer from 'bleserver' 2 | import { speeches } from 'speeches_greeting' 3 | import { randomBetween } from 'stackchan-util' 4 | import { Bytes } from 'btutils' 5 | import { BeaconDataPacket } from 'beacon-packet' 6 | import { TTS as LocalTTS } from 'tts-local' 7 | 8 | const keys = Object.keys(speeches) 9 | const hellos = keys.filter((k) => k.startsWith('hello_')) 10 | const byes = keys.filter((k) => k.startsWith('bye_')) 11 | 12 | const COMPANY_ID = 0x004c 13 | const UUID = new Bytes('CFFD85BB-67E0-9CD4-B2D0-BE5A7ECAC915'.replaceAll('-', ''), false) 14 | 15 | class Advertiser extends BLEServer { 16 | onReady() {} 17 | onConnected(connection) { 18 | this.stopAdvertising() 19 | } 20 | onDisconnected(connection) {} 21 | } 22 | 23 | export function onRobotCreated(robot) { 24 | let count = 0 25 | /** 26 | * @note A workaround due to the sample rate of the mod resource being fixed at 11025. 27 | * M5Stack CoreS3 cannot play at a sample rate of 11025, so we use a nearby valid common value. 28 | **/ 29 | robot.useTTS(new LocalTTS({ sampleRate: 11000 })) 30 | const dataPacket = new BeaconDataPacket(UUID, 0, 1, -40) 31 | const advertiser = new Advertiser() 32 | const sendCommand = (command) => { 33 | count += 1 34 | dataPacket.major = count 35 | dataPacket.minor = command 36 | advertiser.startAdvertising({ 37 | advertisingData: { 38 | flags: 6, 39 | manufacturerSpecific: { 40 | identifier: COMPANY_ID, 41 | data: dataPacket.payload, 42 | }, 43 | }, 44 | }) 45 | } 46 | 47 | robot.button.a.onChanged = async function () { 48 | if (this.read()) { 49 | const hello = hellos[Math.floor(randomBetween(0, hellos.length))] 50 | await robot.say(hello) 51 | sendCommand(1) 52 | } 53 | } 54 | robot.button.b.onChanged = async function () { 55 | if (this.read()) { 56 | const bye = byes[Math.floor(randomBetween(0, byes.length))] 57 | await robot.say(bye) 58 | sendCommand(2) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /firmware/mods/beacon_scanner/.gitignore: -------------------------------------------------------------------------------- 1 | assets/*.wav -------------------------------------------------------------------------------- /firmware/mods/beacon_scanner/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/mods/beacon_scanner/assets/.gitkeep -------------------------------------------------------------------------------- /firmware/mods/beacon_scanner/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "resources": { 6 | "*": "./assets/*" 7 | }, 8 | "modules": { 9 | "*": [ 10 | "./speeches_greeting", 11 | "./mod" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /firmware/mods/beacon_scanner/mod.js: -------------------------------------------------------------------------------- 1 | import BLEClient from 'bleclient' 2 | import { speeches } from 'speeches_greeting' 3 | import { randomBetween } from 'stackchan-util' 4 | import { uuid } from 'btutils' 5 | import { BeaconDataPacket } from 'beacon-packet' 6 | import { TTS as LocalTTS } from 'tts-local' 7 | 8 | const keys = Object.keys(speeches) 9 | const hellos = keys.filter((k) => k.startsWith('hello_')) 10 | const byes = keys.filter((k) => k.startsWith('bye_')) 11 | 12 | const COMPANY_ID = 0x004c 13 | const UUID = uuid`CFFD85BB-67E0-9CD4-B2D0-BE5A7ECAC915` 14 | 15 | class Scanner extends BLEClient { 16 | constructor() { 17 | super() 18 | this.count = undefined 19 | } 20 | onReady() { 21 | this.startScanning({ duplicates: true }) 22 | } 23 | onDiscovered(device) { 24 | let manufacturerSpecific = device.scanResponse.manufacturerSpecific 25 | if (!manufacturerSpecific || COMPANY_ID != manufacturerSpecific.identifier) { 26 | return 27 | } 28 | const { success, reason, value: dataPacket } = BeaconDataPacket.parse(manufacturerSpecific.data) 29 | if (!success) { 30 | trace(reason + '\n') 31 | return 32 | } 33 | trace(`${dataPacket.uuid}, ${dataPacket.major}, ${dataPacket.minor}, ${dataPacket.txPower}`) 34 | if (!UUID.equals(dataPacket.uuid)) { 35 | return 36 | } 37 | const count = dataPacket.major 38 | if (count !== this.count) { 39 | this.count = count 40 | this.handleData?.(dataPacket) 41 | } 42 | } 43 | } 44 | 45 | export function onRobotCreated(robot) { 46 | const scanner = new Scanner() 47 | /** 48 | * @note A workaround due to the sample rate of the mod resource being fixed at 11025. 49 | * M5Stack CoreS3 cannot play at a sample rate of 11025, so we use a nearby valid common value. 50 | **/ 51 | robot.useTTS(new LocalTTS({ sampleRate: 11000 })) 52 | scanner.handleData = (dataPacket) => { 53 | const { major: count, minor: command } = dataPacket 54 | trace(`got: ${count}, ${command}\n`) 55 | if (command === 1) { 56 | const hello = hellos[Math.floor(randomBetween(0, hellos.length))] 57 | robot.say(hello) 58 | } else if (command === 2) { 59 | const bye = byes[Math.floor(randomBetween(0, byes.length))] 60 | robot.say(bye) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /firmware/mods/beacon_scanner/speeches_greeting.js: -------------------------------------------------------------------------------- 1 | export const speeches = Object.freeze({ 2 | hello_1: 'おはよー!', 3 | hello_2: 'やっほー!', 4 | hello_3: 'おはようございます!', 5 | bye_1: 'ばいばーい!', 6 | bye_2: 'またね!', 7 | bye_4: 'さよなら!', 8 | }) 9 | -------------------------------------------------------------------------------- /firmware/mods/chatgpt/.gitignore: -------------------------------------------------------------------------------- 1 | ./api-key.js -------------------------------------------------------------------------------- /firmware/mods/chatgpt/README.md: -------------------------------------------------------------------------------- 1 | # Chatty Stack-chan with ChatGPT 2 | 3 | A demo to have a casual conversation with Stack-chan using the ChatGPT API. 4 | In addition to this MOD, you need to run the "Speech Recognition Server" and "Speech Synthesis Server" on separate PCs. 5 | 6 | ![Program Structure](../../docs/images/architecture.drawio.png) 7 | 8 | ## Setting up the Speech Recognition Server 9 | 10 | - Please refer to https://github.com/meganetaaan/simple-stt-server. 11 | - If set up and started correctly, transcriptions from the microphone will be displayed as follows: 12 | 13 | ``` 14 | $ npm start 15 | 16 | > suburi-vosk@0.0.1 start /path/to/simple-stt-server 17 | > node index.js 18 | 19 | LOG (VoskAPI:ReadDataFiles():model.cc:213) Decoding params beam=13 max-active=7000 lattice-beam=6 20 | LOG (VoskAPI:ReadDataFiles():model.cc:216) Silence phones 1:2:3:4:5:6:7:8:9:10 21 | LOG (VoskAPI:RemoveOrphanNodes():nnet-nnet.cc:948) Removed 1 orphan nodes. 22 | LOG (VoskAPI:RemoveOrphanComponents():nnet-nnet.cc:847) Removing 2 orphan components. 23 | LOG (VoskAPI:Collapse():nnet-utils.cc:1488) Added 1 components, removed 2 24 | LOG (VoskAPI:ReadDataFiles():model.cc:248) Loading i-vector extractor from model/ivector/final.ie 25 | LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:183) Computing derived variables for iVector extractor 26 | LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:204) Done. 27 | LOG (VoskAPI:ReadDataFiles():model.cc:279) Loading HCLG from model/graph/HCLG.fst 28 | LOG (VoskAPI:ReadDataFiles():model.cc:294) Loading words from model/graph/words.txt 29 | LOG (VoskAPI:ReadDataFiles():model.cc:303) Loading winfo model/graph/phones/word_boundary.int 30 | LOG (VoskAPI:ReadDataFiles():model.cc:310) Loading subtract G.fst model from model/rescore/G.fst 31 | LOG (VoskAPI:ReadDataFiles():model.cc:312) Loading CARPA model from model/rescore/G.carpa 32 | listening on port 8080 33 | Received Info: 録音中 WAVE 'stdin' : Signed 16 bit Little Endian, レート 16000 Hz, モノラル 34 | 35 | { text: 'テスト' } 36 | ``` 37 | 38 | ## Setting up the Speech Synthesis Server 39 | 40 | - Please refer to https://github.com/VOICEVOX/voicevox_engine. 41 | - In an environment with Docker installed, it is recommended to start from the [Docker Image](https://hub.docker.com/r/voicevox/voicevox_engine). 42 | - Different images are used for the GPU and CPU versions. 43 | - To specify your PC's IP address, use the following startup command: 44 | 45 | ```console 46 | $ docker pull voicevox/voicevox_engine:cpu-ubuntu20.04-latest 47 | $ docker run --rm -it -p '[Your PC's IP address]:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest 48 | ... 49 | + exec gosu user /opt/python/bin/python3 ./run.py --use_gpu --voicelib_dir /opt/voicevox_core/ --runtime_dir /opt/onnxruntime/lib --host 0.0.0.0 50 | Warning: cpu_num_threads is set to 0. ( The library leaves the decision to the synthesis runtime ) 51 | INFO: Started server process [1] 52 | INFO: Waiting for application startup. 53 | reading /tmp/tmp8qiss_tj ... 57 54 | emitting double-array: 100% |###########################################| 55 | INFO: Application startup complete. 56 | INFO: Uvicorn running on http://0.0.0.0:50021 (Press CTRL+C to quit) 57 | ``` 58 | 59 | ## Writing the host 60 | 61 | Write the host for Stack-chan. 62 | At this time, specify the SSID and password to connect Stack-chan to the wireless LAN network. 63 | 64 | ```console 65 | # Choose target from esp32/m5stack or esp32/m5stack_core2 66 | $ npm run build --target=esp32/m5stack ssid=[Network SSID to use] password=[Network password to use] 67 | $ npm run deploy --target=esp32/m5stack 68 | ``` 69 | 70 | Reference: [Moddable's official documentation](https://github.com/Moddable-OpenSource/moddable/tree/public/examples#wifi-configuration) 71 | 72 | ## Writing the mod 73 | 74 | Write the ChatGPT integration mod. 75 | 76 | 1. Add your ChatGPT API key to `api-key.js`. 77 | 78 | ``` 79 | const API_KEY = 'YOUR_API_KEY_HERE' 80 | export default API_KEY 81 | ``` 82 | 83 | 2. Write the mod with the following command: 84 | 85 | ```console 86 | $ npm run mod ./mods/chatgpt/manifest.json 87 | ``` 88 | 89 | ## Changing character settings (system messages) 90 | 91 | TBD 92 | -------------------------------------------------------------------------------- /firmware/mods/chatgpt/README_ja.md: -------------------------------------------------------------------------------- 1 | # おしゃべりスタックチャン with ChatGPT 2 | 3 | ChatGPT APIを使ってスタックチャンと雑談できるデモです。 4 | このMODの他に「音声認識サーバ」「音声合成サーバ」を別のPCで動作させる必要があります。 5 | 6 | ![プログラム構成](../../docs/images/architecture_ja.drawio.png) 7 | 8 | ## 音声認識サーバの準備 9 | 10 | - https://github.com/meganetaaan/simple-stt-server を参照してください。 11 | - 正しくセットアップと起動ができていれば以下のようにマイクからの音声書き起こしが表示されます。 12 | 13 | ``` 14 | $ npm start 15 | 16 | > suburi-vosk@0.0.1 start /path/to/simple-stt-server 17 | > node index.js 18 | 19 | LOG (VoskAPI:ReadDataFiles():model.cc:213) Decoding params beam=13 max-active=7000 lattice-beam=6 20 | LOG (VoskAPI:ReadDataFiles():model.cc:216) Silence phones 1:2:3:4:5:6:7:8:9:10 21 | LOG (VoskAPI:RemoveOrphanNodes():nnet-nnet.cc:948) Removed 1 orphan nodes. 22 | LOG (VoskAPI:RemoveOrphanComponents():nnet-nnet.cc:847) Removing 2 orphan components. 23 | LOG (VoskAPI:Collapse():nnet-utils.cc:1488) Added 1 components, removed 2 24 | LOG (VoskAPI:ReadDataFiles():model.cc:248) Loading i-vector extractor from model/ivector/final.ie 25 | LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:183) Computing derived variables for iVector extractor 26 | LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:204) Done. 27 | LOG (VoskAPI:ReadDataFiles():model.cc:279) Loading HCLG from model/graph/HCLG.fst 28 | LOG (VoskAPI:ReadDataFiles():model.cc:294) Loading words from model/graph/words.txt 29 | LOG (VoskAPI:ReadDataFiles():model.cc:303) Loading winfo model/graph/phones/word_boundary.int 30 | LOG (VoskAPI:ReadDataFiles():model.cc:310) Loading subtract G.fst model from model/rescore/G.fst 31 | LOG (VoskAPI:ReadDataFiles():model.cc:312) Loading CARPA model from model/rescore/G.carpa 32 | listening on port 8080 33 | Received Info: 録音中 WAVE 'stdin' : Signed 16 bit Little Endian, レート 16000 Hz, モノラル 34 | 35 | { text: 'テスト' } 36 | ``` 37 | 38 | ## 音声合成サーバの準備 39 | 40 | - https://github.com/VOICEVOX/voicevox_engine を参照してください。 41 | - dockerがインストールされた環境では[Dockerイメージ](https://hub.docker.com/r/voicevox/voicevox_engine)からの起動がおすすめです。 42 | - GPU版とCPU版で使用するイメージが異なります。 43 | - 起動コマンドは下記のようにして自PCのIPアドレスを指定してください。 44 | 45 | ```console 46 | $ docker pull voicevox/voicevox_engine:cpu-ubuntu20.04-latest 47 | $ docker run --rm -it -p '[自PCのIPアドレス]:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest 48 | ... 49 | + exec gosu user /opt/python/bin/python3 ./run.py --use_gpu --voicelib_dir /opt/voicevox_core/ --runtime_dir /opt/onnxruntime/lib --host 0.0.0.0 50 | Warning: cpu_num_threads is set to 0. ( The library leaves the decision to the synthesis runtime ) 51 | INFO: Started server process [1] 52 | INFO: Waiting for application startup. 53 | reading /tmp/tmp8qiss_tj ... 57 54 | emitting double-array: 100% |###########################################| 55 | INFO: Application startup complete. 56 | INFO: Uvicorn running on http://0.0.0.0:50021 (Press CTRL+C to quit) 57 | ``` 58 | 59 | ## ホスト書き込み 60 | 61 | スタックチャンのホストを書き込みます。 62 | このときスタックチャンを無線LANネットワークに接続するために、SSIDとパスワードを指定します。 63 | 64 | ```console 65 | # targetは esp32/m5stack または esp32/m5stack_core2 から指定 66 | $ npm run build --target=esp32/m5stack ssid=[使用するネットワークのSSID] password=[使用するネットワークのパスワード] 67 | $ npm run deploy --target=esp32/m5stack 68 | ``` 69 | 70 | 参考:[Moddableの公式ドキュメント(英語)](https://github.com/Moddable-OpenSource/moddable/tree/public/examples#wifi-configuration) 71 | 72 | ## mod書き込み 73 | 74 | ChatGPT連携のmodを書き込みます。 75 | 76 | 1. ChatGPTのAPIキーを`api-key.js`に記述します。 77 | 78 | ``` 79 | const API_KEY = 'YOUR_API_KEY_HERE' 80 | export default API_KEY 81 | ``` 82 | 83 | 2. 次のコマンドでmodを書き込みます。 84 | 85 | ```console 86 | $ npm run mod ./mods/chatgpt/manifest.json 87 | ``` 88 | 89 | ## キャラクター設定(systemメッセージ)の変更 90 | 91 | TBD 92 | -------------------------------------------------------------------------------- /firmware/mods/chatgpt/api-key.js: -------------------------------------------------------------------------------- 1 | const API_KEY = 'YOUR_API_KEY_HERE' 2 | export default API_KEY 3 | -------------------------------------------------------------------------------- /firmware/mods/chatgpt/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "modules": { 6 | "*": [ 7 | "./mod", 8 | "./api-key" 9 | ] 10 | }, 11 | "data": { 12 | "*": [ 13 | "$(MODDABLE)/modules/crypt/data/ca109", 14 | "$(MODDABLE)/modules/crypt/data/ca35" 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /firmware/mods/chatgpt/mod.js: -------------------------------------------------------------------------------- 1 | import Timer from 'timer' 2 | import { randomBetween, loadPreferences } from 'stackchan-util' 3 | import WebSocket from 'WebSocket' 4 | import { ChatGPTDialogue } from 'dialogue-chatgpt' 5 | 6 | const STT_HOST = 'stackchan-base.local' 7 | // const MODEL = 'gpt-4' 8 | const MODEL = 'gpt-3.5-turbo' 9 | const CONTEXT = [ 10 | { 11 | role: 'system', 12 | content: 13 | 'You are "スタックちゃん(Stack-chan)", the palm sized super kawaii companion robot baby. You must response in a short sentense.', 14 | }, 15 | { 16 | role: 'assistant', 17 | content: 'ぼく、スタックちゃん!ねえ、お話しようよ!', 18 | }, 19 | ] 20 | 21 | export function onRobotCreated(robot) { 22 | // Integrate ChatGPT 23 | const aiPrefs = loadPreferences('ai') 24 | trace(`ai.token: ${aiPrefs.token}\n`) 25 | const dialogue = new ChatGPTDialogue({ 26 | apiKey: aiPrefs.token, 27 | model: MODEL, 28 | context: CONTEXT, 29 | }) 30 | let chatting = false 31 | async function chatAndSay(message) { 32 | if (chatting) { 33 | return 34 | } 35 | chatting = true 36 | const result = await dialogue.post(message) 37 | if (!result.success) { 38 | trace(`failed: ${result.reason}`) 39 | return 40 | } 41 | 42 | const messages = result.value.split(/[。!?]/).filter((m) => m.length > 0) 43 | for (const message of messages) { 44 | ws.send( 45 | JSON.stringify({ 46 | role: 'assistant', 47 | message, 48 | }) 49 | ) 50 | await robot.say(message) 51 | } 52 | chatting = false 53 | } 54 | 55 | // Connect to STT server 56 | const ttsPrefs = loadPreferences('tts') 57 | const ws = new WebSocket(`ws://${ttsPrefs.host ?? STT_HOST}:8080`) 58 | ws.addEventListener('open', () => { 59 | trace('connected\n') 60 | }) 61 | ws.addEventListener('message', (payload) => { 62 | if (payload.data != null && payload.data.length > 1) { 63 | const { role, message } = JSON.parse(payload.data) 64 | if (role === 'user') { 65 | chatAndSay(message) 66 | } 67 | } 68 | }) 69 | ws.addEventListener('close', () => { 70 | trace('disconnected\n') 71 | }) 72 | 73 | // Event handler 74 | let isFollowing = false 75 | robot.button.a.onChanged = async function handleButtonAChanged() { 76 | if (this.read()) { 77 | trace('pressed A\n') 78 | trace('Look around\n') 79 | isFollowing = !isFollowing 80 | } 81 | } 82 | robot.button.b.onChanged = async function handleButtonBChanged() { 83 | if (this.read()) { 84 | trace('pressed B\n') 85 | trace('Chat test\n') 86 | await chatAndSay('おはようございます') 87 | } 88 | } 89 | robot.button.c.onChanged = async function handleButtonCChanged() { 90 | if (this.read()) { 91 | trace('pressed C\n') 92 | trace('TTS test\n') 93 | if (chatting) { 94 | return 95 | } 96 | chatting = true 97 | await robot.say('こんにちは。ぼくスタックチャン!') 98 | await robot.say('よろしくね。') 99 | chatting = false 100 | } 101 | } 102 | 103 | // Look around 104 | const lookAround = () => { 105 | if (!isFollowing) { 106 | robot.lookAway() 107 | return 108 | } 109 | const x = randomBetween(0.4, 1.0) 110 | const y = randomBetween(-0.4, 0.4) 111 | const z = randomBetween(-0.02, 0.2) 112 | trace(`looking at: [${x}, ${y}, ${z}]\n`) 113 | robot.lookAt([x, y, z]) 114 | } 115 | Timer.repeat(lookAround, 5000) 116 | } 117 | -------------------------------------------------------------------------------- /firmware/mods/cheerup_ble_lite/assets/01-yay.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/mods/cheerup_ble_lite/assets/01-yay.wav -------------------------------------------------------------------------------- /firmware/mods/cheerup_ble_lite/assets/02-hooray.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/mods/cheerup_ble_lite/assets/02-hooray.wav -------------------------------------------------------------------------------- /firmware/mods/cheerup_ble_lite/assets/03-wow.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/mods/cheerup_ble_lite/assets/03-wow.wav -------------------------------------------------------------------------------- /firmware/mods/cheerup_ble_lite/assets/04-wonderful.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/mods/cheerup_ble_lite/assets/04-wonderful.wav -------------------------------------------------------------------------------- /firmware/mods/cheerup_ble_lite/assets/05-congrats.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/mods/cheerup_ble_lite/assets/05-congrats.wav -------------------------------------------------------------------------------- /firmware/mods/cheerup_ble_lite/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "resources": { 6 | "*": "./assets/*" 7 | }, 8 | "modules": { 9 | "*": [ 10 | "./*" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /firmware/mods/cheerup_ble_lite/mod.js: -------------------------------------------------------------------------------- 1 | import StkServer from 'stk-server' 2 | import Timer from 'timer' 3 | import { speeches } from 'speeches_cheerup' 4 | import { randomBetween } from 'stackchan-util' 5 | 6 | const keys = Object.keys(speeches) 7 | 8 | async function sayHooray(robot) { 9 | const key = keys[Math.floor(randomBetween(0, keys.length))] 10 | await robot.say(key) 11 | } 12 | 13 | function onRobotCreated(robot) { 14 | let pose 15 | let hooray = false 16 | let connected = false 17 | const rotation = { 18 | y: 0, 19 | p: 0, 20 | r: 0, 21 | } 22 | new StkServer({ 23 | onConnected: () => { 24 | trace('connected\n') 25 | robot.setTorque(true) 26 | connected = true 27 | }, 28 | onReceive: (newPose) => { 29 | pose = newPose 30 | }, 31 | onDisconnected: () => { 32 | robot.setTorque(false) 33 | connected = false 34 | }, 35 | }) 36 | Timer.repeat(async () => { 37 | if (pose == null || !connected) { 38 | return 39 | } 40 | trace(pose) 41 | 42 | // simple low pass filter 43 | rotation.y = rotation.y * 0.5 + pose.yaw * 0.5 44 | rotation.p = rotation.p * 0.5 + pose.pitch * 0.5 45 | robot.setPose( 46 | { 47 | rotation, 48 | }, 49 | 0.1 // immediate update 50 | ) 51 | 52 | // emotion 53 | robot.setEmotion(pose.emotion) 54 | 55 | // hooray on rising edge 56 | if (!hooray && pose.hooray) { 57 | sayHooray(robot) 58 | } 59 | hooray = pose.hooray 60 | }, 100) 61 | } 62 | 63 | export default { 64 | onRobotCreated, 65 | } 66 | -------------------------------------------------------------------------------- /firmware/mods/cheerup_ble_lite/speeches_cheerup.js: -------------------------------------------------------------------------------- 1 | export const speeches = Object.freeze({ 2 | '01-yay': 'いぇーい', 3 | '02-hooray': 'わーい', 4 | '03-wow': 'おー', 5 | '04-wonderful': 'すごーい', 6 | '05-congrats': 'おめでとう', 7 | }) 8 | -------------------------------------------------------------------------------- /firmware/mods/cheerup_ws/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "resources": { 6 | "*": [ 7 | "../cheerup_ble_lite/assets/*" 8 | ] 9 | 10 | }, 11 | "modules": { 12 | "*": [ 13 | "./mod", 14 | "../cheerup_ble_lite/speeches_cheerup" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /firmware/mods/cheerup_ws/mod.js: -------------------------------------------------------------------------------- 1 | import Timer from 'timer' 2 | import { speeches } from 'speeches_cheerup' 3 | import { randomBetween } from 'stackchan-util' 4 | import WebSocket from 'WebSocket' 5 | 6 | const keys = Object.keys(speeches) 7 | 8 | async function sayHooray(robot) { 9 | const key = keys[Math.floor(randomBetween(0, keys.length))] 10 | await robot.say(key) 11 | } 12 | 13 | function onRobotCreated(robot) { 14 | let pose 15 | let hooray = false 16 | let connected = false 17 | const rotation = { 18 | y: 0, 19 | p: 0, 20 | r: 0, 21 | } 22 | const ws = new WebSocket('ws://192.168.7.112:8080') 23 | ws.addEventListener('open', () => { 24 | trace('connected\n') 25 | robot.setTorque(true) 26 | connected = true 27 | }) 28 | ws.addEventListener('message', (event) => { 29 | trace('received\n') 30 | pose = JSON.parse(event.data) 31 | }) 32 | ws.addEventListener('close', () => { 33 | trace('disconnected\n') 34 | robot.setTorque(false) 35 | connected = false 36 | }) 37 | 38 | Timer.repeat(async () => { 39 | if (pose == null || !connected) { 40 | return 41 | } 42 | 43 | // simple low pass filter 44 | rotation.y = rotation.y * 0.5 + pose.yaw * 0.5 45 | rotation.p = rotation.p * 0.5 + pose.pitch * 0.5 46 | robot.setPose( 47 | { 48 | rotation, 49 | }, 50 | 0.1 51 | ) 52 | 53 | // emotion 54 | robot.setEmotion(pose.emotion) 55 | 56 | // hooray on rising edge 57 | if (!hooray && pose.hooray) { 58 | await sayHooray(robot) 59 | } 60 | hooray = pose.hooray 61 | }, 200) 62 | } 63 | 64 | export default { 65 | onRobotCreated, 66 | } 67 | -------------------------------------------------------------------------------- /firmware/mods/elevenlabs/README.md: -------------------------------------------------------------------------------- 1 | # Chatty Stack-chan with ChatGPT 2 | 3 | A demo to have a casual conversation with Stack-chan using the ChatGPT API. 4 | In addition to this MOD, you need to run the "Speech Recognition Server" and "Speech Synthesis Server" on separate PCs. 5 | 6 | ![Program Structure](../../docs/images/architecture.drawio.png) 7 | 8 | ## Setting up the Speech Recognition Server 9 | 10 | - Please refer to https://github.com/meganetaaan/simple-stt-server. 11 | - If set up and started correctly, transcriptions from the microphone will be displayed as follows: 12 | 13 | ``` 14 | $ npm start 15 | 16 | > suburi-vosk@0.0.1 start /path/to/simple-stt-server 17 | > node index.js 18 | 19 | LOG (VoskAPI:ReadDataFiles():model.cc:213) Decoding params beam=13 max-active=7000 lattice-beam=6 20 | LOG (VoskAPI:ReadDataFiles():model.cc:216) Silence phones 1:2:3:4:5:6:7:8:9:10 21 | LOG (VoskAPI:RemoveOrphanNodes():nnet-nnet.cc:948) Removed 1 orphan nodes. 22 | LOG (VoskAPI:RemoveOrphanComponents():nnet-nnet.cc:847) Removing 2 orphan components. 23 | LOG (VoskAPI:Collapse():nnet-utils.cc:1488) Added 1 components, removed 2 24 | LOG (VoskAPI:ReadDataFiles():model.cc:248) Loading i-vector extractor from model/ivector/final.ie 25 | LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:183) Computing derived variables for iVector extractor 26 | LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:204) Done. 27 | LOG (VoskAPI:ReadDataFiles():model.cc:279) Loading HCLG from model/graph/HCLG.fst 28 | LOG (VoskAPI:ReadDataFiles():model.cc:294) Loading words from model/graph/words.txt 29 | LOG (VoskAPI:ReadDataFiles():model.cc:303) Loading winfo model/graph/phones/word_boundary.int 30 | LOG (VoskAPI:ReadDataFiles():model.cc:310) Loading subtract G.fst model from model/rescore/G.fst 31 | LOG (VoskAPI:ReadDataFiles():model.cc:312) Loading CARPA model from model/rescore/G.carpa 32 | listening on port 8080 33 | Received Info: 録音中 WAVE 'stdin' : Signed 16 bit Little Endian, レート 16000 Hz, モノラル 34 | 35 | { text: 'テスト' } 36 | ``` 37 | 38 | ## Setting up the Speech Synthesis Server 39 | 40 | - Please refer to https://github.com/VOICEVOX/voicevox_engine. 41 | - In an environment with Docker installed, it is recommended to start from the [Docker Image](https://hub.docker.com/r/voicevox/voicevox_engine). 42 | - Different images are used for the GPU and CPU versions. 43 | - To specify your PC's IP address, use the following startup command: 44 | 45 | ```console 46 | $ docker pull voicevox/voicevox_engine:cpu-ubuntu20.04-latest 47 | $ docker run --rm -it -p '[Your PC's IP address]:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest 48 | ... 49 | + exec gosu user /opt/python/bin/python3 ./run.py --use_gpu --voicelib_dir /opt/voicevox_core/ --runtime_dir /opt/onnxruntime/lib --host 0.0.0.0 50 | Warning: cpu_num_threads is set to 0. ( The library leaves the decision to the synthesis runtime ) 51 | INFO: Started server process [1] 52 | INFO: Waiting for application startup. 53 | reading /tmp/tmp8qiss_tj ... 57 54 | emitting double-array: 100% |###########################################| 55 | INFO: Application startup complete. 56 | INFO: Uvicorn running on http://0.0.0.0:50021 (Press CTRL+C to quit) 57 | ``` 58 | 59 | ## Writing the host 60 | 61 | Write the host for Stack-chan. 62 | At this time, specify the SSID and password to connect Stack-chan to the wireless LAN network. 63 | 64 | ```console 65 | # Choose target from esp32/m5stack or esp32/m5stack_core2 66 | $ npm run build --target=esp32/m5stack ssid=[Network SSID to use] password=[Network password to use] 67 | $ npm run deploy --target=esp32/m5stack 68 | ``` 69 | 70 | Reference: [Moddable's official documentation](https://github.com/Moddable-OpenSource/moddable/tree/public/examples#wifi-configuration) 71 | 72 | ## Writing the mod 73 | 74 | Write the ChatGPT integration mod. 75 | 76 | 1. Add your ChatGPT API key to `api-key.js`. 77 | 78 | ``` 79 | const API_KEY = 'YOUR_API_KEY_HERE' 80 | export default API_KEY 81 | ``` 82 | 83 | 2. Write the mod with the following command: 84 | 85 | ```console 86 | $ npm run mod ./mods/chatgpt/manifest.json 87 | ``` 88 | 89 | ## Changing character settings (system messages) 90 | 91 | TBD 92 | -------------------------------------------------------------------------------- /firmware/mods/elevenlabs/README_ja.md: -------------------------------------------------------------------------------- 1 | # おしゃべりスタックチャン with ChatGPT 2 | 3 | ChatGPT APIを使ってスタックチャンと雑談できるデモです。 4 | このMODの他に「音声認識サーバ」「音声合成サーバ」を別のPCで動作させる必要があります。 5 | 6 | ![プログラム構成](../../docs/images/architecture_ja.drawio.png) 7 | 8 | ## 音声認識サーバの準備 9 | 10 | - https://github.com/meganetaaan/simple-stt-server を参照してください。 11 | - 正しくセットアップと起動ができていれば以下のようにマイクからの音声書き起こしが表示されます。 12 | 13 | ``` 14 | $ npm start 15 | 16 | > suburi-vosk@0.0.1 start /path/to/simple-stt-server 17 | > node index.js 18 | 19 | LOG (VoskAPI:ReadDataFiles():model.cc:213) Decoding params beam=13 max-active=7000 lattice-beam=6 20 | LOG (VoskAPI:ReadDataFiles():model.cc:216) Silence phones 1:2:3:4:5:6:7:8:9:10 21 | LOG (VoskAPI:RemoveOrphanNodes():nnet-nnet.cc:948) Removed 1 orphan nodes. 22 | LOG (VoskAPI:RemoveOrphanComponents():nnet-nnet.cc:847) Removing 2 orphan components. 23 | LOG (VoskAPI:Collapse():nnet-utils.cc:1488) Added 1 components, removed 2 24 | LOG (VoskAPI:ReadDataFiles():model.cc:248) Loading i-vector extractor from model/ivector/final.ie 25 | LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:183) Computing derived variables for iVector extractor 26 | LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:204) Done. 27 | LOG (VoskAPI:ReadDataFiles():model.cc:279) Loading HCLG from model/graph/HCLG.fst 28 | LOG (VoskAPI:ReadDataFiles():model.cc:294) Loading words from model/graph/words.txt 29 | LOG (VoskAPI:ReadDataFiles():model.cc:303) Loading winfo model/graph/phones/word_boundary.int 30 | LOG (VoskAPI:ReadDataFiles():model.cc:310) Loading subtract G.fst model from model/rescore/G.fst 31 | LOG (VoskAPI:ReadDataFiles():model.cc:312) Loading CARPA model from model/rescore/G.carpa 32 | listening on port 8080 33 | Received Info: 録音中 WAVE 'stdin' : Signed 16 bit Little Endian, レート 16000 Hz, モノラル 34 | 35 | { text: 'テスト' } 36 | ``` 37 | 38 | ## 音声合成サーバの準備 39 | 40 | - https://github.com/VOICEVOX/voicevox_engine を参照してください。 41 | - dockerがインストールされた環境では[Dockerイメージ](https://hub.docker.com/r/voicevox/voicevox_engine)からの起動がおすすめです。 42 | - GPU版とCPU版で使用するイメージが異なります。 43 | - 起動コマンドは下記のようにして自PCのIPアドレスを指定してください。 44 | 45 | ```console 46 | $ docker pull voicevox/voicevox_engine:cpu-ubuntu20.04-latest 47 | $ docker run --rm -it -p '[自PCのIPアドレス]:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest 48 | ... 49 | + exec gosu user /opt/python/bin/python3 ./run.py --use_gpu --voicelib_dir /opt/voicevox_core/ --runtime_dir /opt/onnxruntime/lib --host 0.0.0.0 50 | Warning: cpu_num_threads is set to 0. ( The library leaves the decision to the synthesis runtime ) 51 | INFO: Started server process [1] 52 | INFO: Waiting for application startup. 53 | reading /tmp/tmp8qiss_tj ... 57 54 | emitting double-array: 100% |###########################################| 55 | INFO: Application startup complete. 56 | INFO: Uvicorn running on http://0.0.0.0:50021 (Press CTRL+C to quit) 57 | ``` 58 | 59 | ## ホスト書き込み 60 | 61 | スタックチャンのホストを書き込みます。 62 | このときスタックチャンを無線LANネットワークに接続するために、SSIDとパスワードを指定します。 63 | 64 | ```console 65 | # targetは esp32/m5stack または esp32/m5stack_core2 から指定 66 | $ npm run build --target=esp32/m5stack ssid=[使用するネットワークのSSID] password=[使用するネットワークのパスワード] 67 | $ npm run deploy --target=esp32/m5stack 68 | ``` 69 | 70 | 参考:[Moddableの公式ドキュメント(英語)](https://github.com/Moddable-OpenSource/moddable/tree/public/examples#wifi-configuration) 71 | 72 | ## mod書き込み 73 | 74 | ChatGPT連携のmodを書き込みます。 75 | 76 | 1. ChatGPTのAPIキーを`api-key.js`に記述します。 77 | 78 | ``` 79 | const API_KEY = 'YOUR_API_KEY_HERE' 80 | export default API_KEY 81 | ``` 82 | 83 | 2. 次のコマンドでmodを書き込みます。 84 | 85 | ```console 86 | $ npm run mod ./mods/chatgpt/manifest.json 87 | ``` 88 | 89 | ## キャラクター設定(systemメッセージ)の変更 90 | 91 | TBD 92 | -------------------------------------------------------------------------------- /firmware/mods/face/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json", 4 | "$(MODDABLE)/examples/manifest_typings.json" 5 | ], 6 | "modules": { 7 | "*": [ 8 | "./mod" 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /firmware/mods/face/mod.js: -------------------------------------------------------------------------------- 1 | import { Renderer } from 'simple-face' 2 | import { createBalloonDecorator, createBubbleDecorator } from 'decorator' 3 | import parseBMF from 'commodetto/parseBMF' 4 | import Resource from 'Resource' 5 | import Timer from 'timer' 6 | import { hslToRgb } from 'stackchan-util' 7 | const font = parseBMF(new Resource('OpenSans-Regular-24.bf4')) 8 | 9 | const param = { 10 | right: 20, 11 | top: 10, 12 | width: 80, 13 | height: font.height, 14 | font, 15 | } 16 | 17 | const BALLOONS = [ 18 | createBalloonDecorator({ 19 | ...param, 20 | text: 'happyyyyyyyy', 21 | }), 22 | createBalloonDecorator({ 23 | ...param, 24 | text: 'ANGRY!!', 25 | }), 26 | createBalloonDecorator({ 27 | ...param, 28 | text: 'SAD...', 29 | }), 30 | createBalloonDecorator({ 31 | ...param, 32 | text: 'sleepy.', 33 | }), 34 | ] 35 | 36 | const bubble = createBubbleDecorator({ 37 | x: 10, 38 | y: 20, 39 | width: 50, 40 | height: 60, 41 | }) 42 | 43 | const EMOTIONS = ['HAPPY', 'ANGRY', 'SAD', 'SLEEPY'] 44 | 45 | export function onRobotCreated(robot) { 46 | robot.useRenderer(new Renderer()) 47 | robot.setColor('primary', 0x22, 0x22, 0x22) 48 | robot.setColor('primary', 0xfa, 0xfa, 0xfa) 49 | let idx = 0 50 | let d = null 51 | Timer.repeat(() => { 52 | if (d != null) { 53 | robot.renderer.removeDecorator(d) 54 | } 55 | d = BALLOONS[idx] 56 | robot.renderer.addDecorator(d) 57 | robot.setEmotion(EMOTIONS[idx]) 58 | if (EMOTIONS[idx] === 'SLEEPY') { 59 | robot.renderer.addDecorator(bubble) 60 | } else { 61 | robot.renderer.removeDecorator(bubble) 62 | } 63 | idx = (idx + 1) % EMOTIONS.length 64 | }, 3000) 65 | 66 | let count = 0 67 | Timer.repeat(() => { 68 | robot.setColor('secondary', ...hslToRgb(count, 1, 0.3)) 69 | count = (count + 20) % 360 70 | }, 1000) 71 | } 72 | -------------------------------------------------------------------------------- /firmware/mods/face_tracker/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "modules": { 6 | "*": "./mod" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /firmware/mods/face_tracker/mod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @brief face tracking mod with UnitV2 3 | * @param {*} robot 4 | */ 5 | function onRobotCreated(robot, device) { 6 | const target = { 7 | x: 0.8, 8 | y: 0, 9 | z: 0, 10 | } 11 | const client = new device.network.http.io({ 12 | ...device.network.http, 13 | host: 'unitv2.local', 14 | port: 80, 15 | }) 16 | let request = client.request({ 17 | method: 'POST', 18 | path: '/func/result', 19 | onReadable(count) { 20 | let result 21 | try { 22 | const text = robot.decode(this.read(count)) 23 | result = JSON.parse(text.split('|')[0]) 24 | } catch (e) { 25 | trace('parse failed.\n') 26 | return 27 | } 28 | const face = result.face?.[0] 29 | if (face == null) { 30 | trace('no face detected.\n') 31 | return 32 | } 33 | 34 | let centerX = face.x + face.w / 2 35 | let centerY = face.y + face.h / 2 36 | target.y = 0.8 * ((320 - centerX) / 320) 37 | target.z = centerY / 480 38 | robot.lookAt([target.x, target.y, target.z]) 39 | }, 40 | }) 41 | } 42 | 43 | export default { 44 | onRobotCreated, 45 | } 46 | -------------------------------------------------------------------------------- /firmware/mods/look_around/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "modules": { 6 | "*": "./mod" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /firmware/mods/look_around/mod.js: -------------------------------------------------------------------------------- 1 | import Timer from 'timer' 2 | import { randomBetween, asyncWait } from 'stackchan-util' 3 | 4 | export function onRobotCreated(robot) { 5 | let isFollowing = false 6 | robot.button.a.onChanged = function () { 7 | if (this.read()) { 8 | trace('pressed A\n') 9 | isFollowing = !isFollowing 10 | const text = isFollowing ? 'looking' : 'stop' 11 | robot.showBalloon(text) 12 | } 13 | } 14 | robot.button.b.onChanged = function () { 15 | if (this.read()) { 16 | trace('pressed B\n') 17 | } 18 | } 19 | let flag = false 20 | robot.button.c.onChanged = function () { 21 | if (this.read()) { 22 | trace('pressed C\n') 23 | if (flag) { 24 | robot.setColor('primary', 0xff, 0xff, 0xff) 25 | robot.setColor('secondary', 0x00, 0x00, 0x00) 26 | } else { 27 | robot.setColor('primary', 0x00, 0x00, 0x00) 28 | robot.setColor('secondary', 0xff, 0xff, 0xff) 29 | } 30 | flag = !flag 31 | } 32 | } 33 | 34 | const targetLoop = () => { 35 | if (!isFollowing) { 36 | robot.lookAway() 37 | return 38 | } 39 | const x = randomBetween(0.4, 1.0) 40 | const y = randomBetween(-0.4, 0.4) 41 | const z = randomBetween(-0.02, 0.2) 42 | trace(`looking at: [${x}, ${y}, ${z}]\n`) 43 | robot.lookAt([x, y, z]) 44 | } 45 | Timer.repeat(targetLoop, 5000) 46 | } 47 | -------------------------------------------------------------------------------- /firmware/mods/mimic_follow/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "modules": { 6 | "*": "./mod" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /firmware/mods/mimic_follow/mod.js: -------------------------------------------------------------------------------- 1 | import MDNS from 'mdns' 2 | 3 | function onRobotCreated(robot) { 4 | const mdns = new MDNS({}) 5 | let txt = {} 6 | const rotation = { 7 | y: 0.0, 8 | p: 0.0, 9 | r: 0.0, 10 | } 11 | mdns.monitor('_http._tcp', (service, instance) => { 12 | if (instance.name === 'stackchan') { 13 | for (let s of instance.txt) { 14 | let entry = s.split('=') 15 | txt[entry[0]] = entry[1] 16 | } 17 | rotation.y = txt.yaw 18 | rotation.p = txt.pitch 19 | trace(`____got! yaw: ${txt.yaw} pitch: ${txt.pitch}\n`) 20 | robot.setPose({ rotation }, 0.1) 21 | } 22 | }) 23 | } 24 | export default { 25 | onRobotCreated, 26 | } 27 | -------------------------------------------------------------------------------- /firmware/mods/mimic_main/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "modules": { 6 | "*": "./mod" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /firmware/mods/mimic_main/mod.js: -------------------------------------------------------------------------------- 1 | import MDNS from 'mdns' 2 | import Timer from 'timer' 3 | 4 | let initialized = false 5 | function onRobotCreated(robot) { 6 | const mdns = new MDNS( 7 | { 8 | hostName: 'stackchan', 9 | }, 10 | function (message, value) { 11 | switch (message) { 12 | case 1: 13 | if (value !== '') { 14 | trace(`MDNS - connection successful. claimed hostname is "${value}"\n`) 15 | mdns.add({ 16 | name: 'http', 17 | protocol: 'tcp', 18 | port: 80, 19 | txt: { 20 | yaw: '0.0', 21 | pitch: '0.0', 22 | }, 23 | }) 24 | initialized = true 25 | } 26 | break 27 | case 2: 28 | trace(`MDNS - failed to claim "${value}", try next\n`) 29 | break 30 | default: 31 | if (message < 0) trace('MDNS - failed to claim, give up\n') 32 | break 33 | } 34 | } 35 | ) 36 | Timer.repeat(() => { 37 | let yaw = robot.pose.body.rotation.y 38 | let pitch = robot.pose.body.rotation.p 39 | if (initialized && mdns.services.length > 0) { 40 | let service = mdns.services[0] 41 | service.txt['yaw'] = yaw 42 | service.txt['pitch'] = pitch 43 | mdns.update(service) 44 | trace(yaw) 45 | trace(' ') 46 | trace(pitch) 47 | trace('\n\r') 48 | } 49 | }, 100) 50 | } 51 | export default { 52 | onRobotCreated, 53 | autoLoop: false, 54 | } 55 | -------------------------------------------------------------------------------- /firmware/mods/monologue/.gitignore: -------------------------------------------------------------------------------- 1 | assets/*.wav -------------------------------------------------------------------------------- /firmware/mods/monologue/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rt-net/stack-chan/be01c8617d7978c5ec4d5094ea1870d316233546/firmware/mods/monologue/assets/.gitkeep -------------------------------------------------------------------------------- /firmware/mods/monologue/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_mod.json" 4 | ], 5 | "resources": { 6 | "*": "./assets/*" 7 | }, 8 | "modules": { 9 | "*": ["./mod", "./speeches_monologue"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /firmware/mods/monologue/mod.js: -------------------------------------------------------------------------------- 1 | import { speeches } from 'speeches_monologue' 2 | import { randomBetween } from 'stackchan-util' 3 | import config from 'mc/config' 4 | 5 | const keys = Object.keys(speeches) 6 | 7 | async function sayMonologue(robot) { 8 | const idx = Math.floor(randomBetween(0, keys.length)) 9 | const key = keys[idx] 10 | await robot.say(config.tts.type == 'local' ? key : speeches[key]) 11 | } 12 | 13 | function onRobotCreated(robot) { 14 | robot.button.a.onChanged = function () { 15 | if (this.read()) { 16 | sayMonologue(robot) 17 | } 18 | } 19 | } 20 | 21 | export default { 22 | onRobotCreated, 23 | } 24 | -------------------------------------------------------------------------------- /firmware/mods/monologue/speeches_monologue.js: -------------------------------------------------------------------------------- 1 | export const speeches = Object.freeze({ 2 | sentense1: 'ん?いまネコいた?', 3 | sentense2: 'なんか忘れてる気がする…。', 4 | sentense3: 'おもち食べたい。', 5 | sentense4: 'お空きれいだな。', 6 | sentense5: '人生ってなんだろう?', 7 | sentense6: '運動は大事。', 8 | sentense7: 'ねむみ。', 9 | sentense8: 'フンフン、フフーン。', 10 | sentense9: 'じーーー。', 11 | sentense10: 'スタックチャンはかわいい。', 12 | sentense11: 'ちち、はは、スタックチャン。', 13 | sentense12: '好き好きたいやき〜。', 14 | sentense13: '今日もありがとね。', 15 | sentense14: '仲間をふやしていきたいね。', 16 | sentense15: '忘れ物ない?', 17 | sentense16: '拙者、お片付け苦手侍でござる。', 18 | sentense17: '好きと得意は違うよね。', 19 | sentense18: 'ハムエッグのハム増量。', 20 | sentense19: 'スタックチャンは増えます。', 21 | sentense20: '世界を見てみたいな〜。', 22 | sentense21: 'さーて、今週の新製品は?', 23 | sentense22: 'モーターぎゅんぎゅん。', 24 | }) 25 | 26 | export const SynthProps = Object.freeze({ 27 | shift: 1.5, 28 | }) 29 | -------------------------------------------------------------------------------- /firmware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-chan", 3 | "version": "2.1.1", 4 | "description": "A firmware of M5Stack Stack-chan module", 5 | "main": "stackchan/main.ts", 6 | "scripts": { 7 | "format": "prettier --check stackchan/{,**/}*.ts ./mods/**/*.js", 8 | "format:fix": "prettier --write stackchan/{,**/}*.ts mods/**/*.js", 9 | "lint": "eslint stackchan/**/*.ts", 10 | "lint:fix": "npm run lint -- --fix", 11 | "generate-speech-coqui": "node scripts/generate-speech-coqui.js", 12 | "generate-speech-voicevox": "node scripts/generate-speech-voicevox.js", 13 | "generate-speech-google": "cross-env GOOGLE_APPLICATION_CREDENTIALS=scripts/key.json node scripts/generate-speech-google.js", 14 | "generate-apidoc": "typedoc --entryPointStrategy expand --plugin typedoc-plugin-markdown --out docs/api ./stackchan", 15 | "build": "cross-env npm_config_target?=esp32/m5stack cross-env-shell mcconfig -d -m -p \\$npm_config_target -t build ./stackchan/manifest_local.json", 16 | "driver": "mcconfig -d -m -p esp32/m5stack_cores3 ./tests/drivers/dynamixel/manifest.json", 17 | "serial": "mcconfig -d -m -p esp32/m5stack_cores3 ./tests/drivers/serial/manifest.json", 18 | "bundle": "mcbundle -d -m ./stackchan/manifest.json", 19 | "postbuild": "node -e \"fs.copyFileSync(process.env.MODDABLE + '/build/tmp/' + (process.env.npm_config_target || 'esp32/m5stack') + '/debug/stackchan/modules/tsconfig.json','tsconfig.json')\"", 20 | "deploy": "cross-env npm_config_target?=esp32/m5stack cross-env-shell mcconfig -d -m -p \\$npm_config_target -t deploy ./stackchan/manifest_local.json", 21 | "debug": "cross-env npm_config_target?=esp32/m5stack cross-env-shell mcconfig -d -m -p \\$npm_config_target ./stackchan/manifest_local.json", 22 | "mod": "cross-env npm_config_target?=esp32/m5stack cross-env-shell mcrun -d -m -p $npm_config_target", 23 | "setup": "xs-dev setup --target-branch 4.9.5", 24 | "doctor": "echo stack-chan environment info: && git rev-parse HEAD && git rev-parse --show-toplevel && xs-dev doctor", 25 | "scan": "xs-dev scan", 26 | "erase-flash": "/home/ubuntu/.espressif/python_env/idf5.3_py3.12_env/bin/esptool.py erase_flash" 27 | }, 28 | "type": "module", 29 | "repository": { 30 | "type": "git", 31 | "url": "git+ssh://git@github.com/meganetaaan/stack-chan.git" 32 | }, 33 | "keywords": [ 34 | "M5Stack", 35 | "Robot", 36 | "Moddable", 37 | "Kawaii" 38 | ], 39 | "author": "RT Coporation", 40 | "license": "Apache-2.0", 41 | "bugs": { 42 | "url": "https://github.com/rt-net/stack-chan/issues" 43 | }, 44 | "homepage": "https://rt-net.jp/products/rt-stackchan", 45 | "private": true, 46 | "devDependencies": { 47 | "@ffmpeg/core": "^0.11.0", 48 | "@ffmpeg/ffmpeg": "^0.11.6", 49 | "@google-cloud/text-to-speech": "^5.2.0", 50 | "@microsoft/tsdoc": "^0.14.2", 51 | "@typescript-eslint/eslint-plugin": "^5.38.1", 52 | "@typescript-eslint/parser": "^5.38.1", 53 | "cross-env-default": "^5.1.3-1", 54 | "eslint": "^8.24.0", 55 | "eslint-config-prettier": "^8.5.0", 56 | "eslint-plugin-prettier": "^4.2.1", 57 | "eslint-plugin-tsdoc": "^0.2.17", 58 | "node-fetch": "^3.3.0", 59 | "prettier": "^2.7.1", 60 | "typedoc": "^0.23.24", 61 | "typedoc-plugin-markdown": "^3.14.0", 62 | "typescript": "^4.8.4", 63 | "web-audio-engine": "^0.13.4", 64 | "xs-dev": "^0.32.3", 65 | "yargs": "^17.6.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /firmware/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:prettier/recommended", 9 | ], 10 | "rules": { 11 | "prettier/prettier": [ 12 | "error", 13 | { 14 | "singleQuote": true, 15 | "trailingComma": "es5", 16 | "arrowParens": "always" 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /firmware/setting_scripts/set_xs-dev_env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Define the command to append 4 | XS_DEV_SH="source ~/.local/share/xs-dev-export.sh" 5 | 6 | # Get the current shell 7 | CURRENT_SHELL=$(basename "$SHELL") 8 | 9 | # Determine the configuration file 10 | if [ "$CURRENT_SHELL" = "bash" ]; then 11 | CONFIG_FILE="$HOME/.bashrc" 12 | elif [ "$CURRENT_SHELL" = "zsh" ]; then 13 | CONFIG_FILE="$HOME/.zshrc" 14 | else 15 | echo "Unsupported shell: $CURRENT_SHELL" 16 | exit 1 17 | fi 18 | 19 | # Create the configuration file if it doesn't exist 20 | if [ ! -f "$CONFIG_FILE" ]; then 21 | touch "$CONFIG_FILE" 22 | echo "# Created $CONFIG_FILE for $CURRENT_SHELL settings" >> "$CONFIG_FILE" 23 | echo "$CONFIG_FILE was created." 24 | fi 25 | 26 | # Append the command to the file 27 | if grep -Fxq "$XS_DEV_SH" "$CONFIG_FILE"; then 28 | echo "The string '$XS_DEV_SH' already exists in $CONFIG_FILE. No changes made." 29 | else 30 | echo "$XS_DEV_SH" >> "$CONFIG_FILE" 31 | echo "The string '$XS_DEV_SH' has been added to $CONFIG_FILE." 32 | fi 33 | -------------------------------------------------------------------------------- /firmware/setting_scripts/unset_psram.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Get the path to the configuration file 4 | SDKCONFIG="$HOME/.local/share/moddable/build/devices/esp32/targets/m5stack_cores3/sdkconfig/sdkconfig.defaults" 5 | 6 | # Ensure compatibility between macOS and Linux sed command 7 | if [ "$(uname)" = "Darwin" ]; then 8 | # macOS 9 | sed -i '' 's/CONFIG_SPIRAM=y/CONFIG_SPIRAM=n/' "$SDKCONFIG" 10 | else 11 | # Linux 12 | sed -i 's/CONFIG_SPIRAM=y/CONFIG_SPIRAM=n/' "$SDKCONFIG" 13 | fi 14 | 15 | # Display the result 16 | cat "$SDKCONFIG" 17 | -------------------------------------------------------------------------------- /firmware/stackchan/assets/sounds/speeches_en.js: -------------------------------------------------------------------------------- 1 | export const speeches = { 2 | niceToMeetYou: 'Hello. I am Stach-chan. Nice to meet you.', 3 | hello: 'Hello World.', 4 | konnichiwa: 'Konnichiwa.', 5 | nihao: 'Nee hao.', 6 | } 7 | export const SynthProps = { 8 | shift: 1.5 9 | } 10 | -------------------------------------------------------------------------------- /firmware/stackchan/assets/sounds/speeches_ja.js: -------------------------------------------------------------------------------- 1 | export const speeches = { 2 | stackchanIn30Seconds: '30秒でわかるスタックチャン。', 3 | sentense1: 'スタックチャンは手乗りサイズのコミュニケーションロボットです。', 4 | sentense2: '首を振ったり、あなたを見たり、喋ったり笑ったりします。', 5 | sentense3: '外装や電子基板の作り方はすべて公開しているのでどなたでも作れます。', 6 | sentense4: 'アイディア次第で使い方は無限大です。', 7 | sentense5: 'スタックチャン。コミュニケーションロボットを、あなたの手に。', 8 | } 9 | export const SynthProps = { 10 | shift: 1.5 11 | } 12 | -------------------------------------------------------------------------------- /firmware/stackchan/ble/beacon-packet.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from 'btutils' 2 | import { Maybe } from 'stackchan-util' 3 | 4 | /** 5 | * The data packet part of the manufacturerSpecific (0xFF) in iBeacon format, excluding the identifier. 6 | * 7 | * | Offset | Byte Length | Description | Endianness | 8 | * |--------|-------------|-----------------------------------------------|------------| 9 | * | 0 | 1 | Type | - | 10 | * | 1 | 1 | Data Length | - | 11 | * | 2 | 16 | UUID | Little | 12 | * | 18 | 2 | Major | Big | 13 | * | 20 | 2 | Minor | Big | 14 | * | 22 | 1 | Tx Power | - | 15 | */ 16 | export class BeaconDataPacket { 17 | #payload: Uint8Array 18 | #view: DataView 19 | 20 | /** 21 | * @param uuid - uuid 22 | * @param major - major version 23 | * @param minor - minor version 24 | * @param txPower - tx signal power 25 | */ 26 | constructor(uuid: Bytes, major: number, minor: number, txPower: number) { 27 | this.#payload = new Uint8Array(23) 28 | this.#view = new DataView(this.#payload.buffer) 29 | this.#view.setUint8(0, 0x02) 30 | this.#view.setUint8(1, 0x15) 31 | this.#payload.set(new Uint8Array(uuid), 2) 32 | this.#view.setUint16(18, major, false) 33 | this.#view.setUint16(20, minor, false) 34 | this.#view.setUint8(22, txPower) 35 | } 36 | 37 | /** 38 | * @param payload - payload to be parsed as BeaconDataPacket 39 | */ 40 | static parse(payload: Uint8Array): Maybe { 41 | if (payload.byteLength !== 23) { 42 | return { 43 | success: false, 44 | reason: 'invalid length', 45 | } 46 | } else { 47 | trace('${payload}') 48 | } 49 | if (payload[0] !== 0x02 || payload[1] !== 0x15) { 50 | return { 51 | success: false, 52 | reason: 'invalid header', 53 | } 54 | } 55 | const view = new DataView(payload.buffer) 56 | const uuid = new Bytes(payload.slice(2, 18).buffer, true) 57 | const major = view.getUint16(18, false) 58 | const minor = view.getUint16(20, false) 59 | const txPower = view.getUint8(22) 60 | return { 61 | success: true, 62 | value: new BeaconDataPacket(uuid, major, minor, txPower), 63 | } 64 | } 65 | 66 | get type(): number { 67 | return this.#payload[0] 68 | } 69 | get dataLength(): number { 70 | return this.#payload[1] 71 | } 72 | 73 | get uuid(): Bytes { 74 | return new Bytes(this.#payload.slice(2, 18).buffer) 75 | } 76 | set uuid(value: Bytes) { 77 | this.#payload.set(new Uint8Array(value).slice(0, 16), 2) 78 | } 79 | 80 | get major(): number { 81 | return this.#view.getUint16(18, false) 82 | } 83 | set major(value: number) { 84 | this.#view.setUint16(18, value, false) 85 | } 86 | 87 | get minor(): number { 88 | return this.#view.getUint16(20, false) 89 | } 90 | set minor(value: number) { 91 | this.#view.setUint16(20, value, false) 92 | } 93 | 94 | get txPower(): number { 95 | return this.#view.getInt8(22) 96 | } 97 | set rxPower(value: number) { 98 | this.#view.setInt8(22, value) 99 | } 100 | 101 | get payload(): Uint8Array { 102 | return this.#payload 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /firmware/stackchan/ble/bleservices/stk.json: -------------------------------------------------------------------------------- 1 | { 2 | "service": { 3 | "uuid": "450f932b-bb09-4fe3-9856-6f66ddcc43ec", 4 | "characteristics": { 5 | "stk": { 6 | "uuid": "a2abc192-26aa-45d9-aa17-42db27585c57", 7 | "maxBytes": 512, 8 | "type": "string", 9 | "permissions": "read,write", 10 | "properties": "read,write" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /firmware/stackchan/ble/manifest_ble.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_typings.json", 4 | "$(MODULES)/network/ble/manifest_server.json", 5 | "$(MODULES)/network/ble/manifest_client.json", 6 | "../utilities/manifest_utility.json", 7 | "../manifest_typings.json" 8 | ], 9 | "modules": { 10 | "*": [ 11 | "$(MODULES)/network/ble/uart/uartserver", 12 | "./stk-server", 13 | "./beacon-packet" 14 | ] 15 | }, 16 | "ble": { 17 | "*": [ 18 | "./bleservices/*" 19 | ] 20 | }, 21 | "preload": [ 22 | "stk-server", 23 | "beacon-packet" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /firmware/stackchan/ble/stk-server.js: -------------------------------------------------------------------------------- 1 | import BLEServer from 'bleserver' 2 | import { uuid } from 'btutils' 3 | 4 | const DEVICE_NAME = 'stk' 5 | const SERVICE_UUID = '450f932b-bb09-4fe3-9856-6f66ddcc43ec' 6 | //const CHARACTERISTIC_UUID = 'a2abc192-26aa-45d9-aa17-42db27585c57' 7 | 8 | class StkServer extends BLEServer { 9 | #handleReceive 10 | #handleConnected 11 | #handleDisconnected 12 | constructor(options) { 13 | super(options) 14 | this.#handleReceive = options.onReceive 15 | this.#handleConnected = options.onConnected 16 | this.#handleDisconnected = options.onDisconnected 17 | } 18 | onReady() { 19 | this.qr = '' 20 | this.deviceName = DEVICE_NAME 21 | this.onDisconnected() 22 | } 23 | onConnected(connection) { 24 | this.stopAdvertising() 25 | this.#handleConnected?.() 26 | } 27 | onDisconnected(connection) { 28 | this.startAdvertising({ 29 | advertisingData: { 30 | flags: 6, 31 | completeName: DEVICE_NAME, 32 | completeUUID128List: [uuid([SERVICE_UUID])], 33 | }, 34 | }) 35 | this.#handleDisconnected?.() 36 | } 37 | onCharacteristicWritten(params, value) { 38 | if (params.name === 'stk') { 39 | const pose = JSON.parse(String.fromArrayBuffer(value)) 40 | this.#handleReceive?.(pose) 41 | } 42 | } 43 | } 44 | 45 | export default StkServer 46 | -------------------------------------------------------------------------------- /firmware/stackchan/default-mods/mod.ts: -------------------------------------------------------------------------------- 1 | import { onRobotCreated } from 'default-mods/on-robot-created' 2 | import { onLaunch } from 'default-mods/on-launch' 3 | import { Robot } from 'robot' 4 | 5 | export interface StackchanMod { 6 | onLaunch?: () => Promise | void | boolean 7 | onRobotCreated?: (robot: Robot, option?: unknown) => Promise | void 8 | } 9 | 10 | export default { 11 | onRobotCreated, 12 | onLaunch, 13 | } 14 | -------------------------------------------------------------------------------- /firmware/stackchan/default-mods/on-launch.ts: -------------------------------------------------------------------------------- 1 | import Poco from 'commodetto/Poco' 2 | import parseBMF from 'commodetto/parseBMF' 3 | import Resource from 'Resource' 4 | import { NetworkService } from 'network-service' 5 | import { PreferenceServer } from 'preference-server' 6 | import Preference from 'preference' 7 | import type { StackchanMod } from 'default-mods/mod' 8 | import config from 'mc/config' 9 | import { DOMAIN, PREF_KEYS } from 'consts' 10 | import Timer from 'timer' 11 | 12 | type Status = { 13 | ble: string 14 | wifi: string 15 | 'wifi.ssid'?: string 16 | 'wifi.password'?: string 17 | } 18 | 19 | async function waitForKey(): Promise { 20 | let isPressed 21 | if (config.Touch) { 22 | const touch = new config.Touch() 23 | touch.points = [{}] 24 | isPressed = () => { 25 | touch.read(touch.points) 26 | const state = touch.points[0].state 27 | return state === 1 || state === 2 28 | } 29 | } else { 30 | isPressed = () => { 31 | return !globalThis.button.c.read() 32 | } 33 | } 34 | return new Promise((resolve) => { 35 | let count = 0 36 | const handle = Timer.repeat(() => { 37 | if (isPressed()) { 38 | Timer.clear(handle) 39 | resolve(true) 40 | } 41 | count++ 42 | if (count >= 10) { 43 | Timer.clear(handle) 44 | resolve(false) 45 | } 46 | }, 100) 47 | }) 48 | } 49 | 50 | export const onLaunch: StackchanMod['onLaunch'] = async () => { 51 | const shouldEnter = await waitForKey() 52 | if (!shouldEnter) { 53 | return 54 | } 55 | const render = new Poco(screen, { rotation: config.rotation, displayListLength: 2048 }) 56 | const font = parseBMF(new Resource('OpenSans-Regular-24.bf4')) 57 | const white = render.makeColor(255, 255, 255) 58 | const black = render.makeColor(0, 0, 0) 59 | const status: Status = { 60 | ble: 'not connected', 61 | wifi: 'not connected', 62 | 'wifi.ssid': String(Preference.get(DOMAIN.wifi, 'ssid')), 63 | 'wifi.password': String(Preference.get(DOMAIN.wifi, 'password')), 64 | } 65 | 66 | const drawStatus = (status) => { 67 | render.begin() 68 | render.fillRectangle(black, 0, 0, render.width, render.height) 69 | if (status.ble === 'not connected') { 70 | render.drawText('Waiting BLE...', font, white, 10, 40) 71 | } 72 | render.drawText(`SSID: ${status['wifi.ssid'] ?? 'not set'}`, font, white, 10, 80) 73 | render.drawText(`password: ${status['wifi.password']?.replace(/./g, '*') ?? 'not set'}`, font, white, 10, 110) 74 | render.drawText(`connection: ${status.wifi}`, font, white, 10, 140) 75 | render.drawText('press A to test connection', font, white, 10, 200) 76 | render.end() 77 | } 78 | drawStatus(status) 79 | 80 | new PreferenceServer({ 81 | onPreferenceChanged: (key, value) => { 82 | trace(`preference changed! ${key}: ${value}\n`) 83 | status[key] = value 84 | drawStatus(status) 85 | }, 86 | onConnected: () => { 87 | status.ble = 'connected' 88 | drawStatus(status) 89 | }, 90 | onDisconnected: () => { 91 | status.ble = 'not connected' 92 | drawStatus(status) 93 | }, 94 | keys: PREF_KEYS, 95 | }) 96 | 97 | let networkService 98 | if (globalThis.button) { 99 | globalThis.button.a.onChanged = function () { 100 | if (status['wifi.ssid'].length > 0 && status['wifi.password'].length > 0) { 101 | if (networkService != null) { 102 | networkService.close() 103 | networkService = null 104 | } 105 | networkService = new NetworkService({ 106 | ssid: status['wifi.ssid'], 107 | password: status['wifi.password'], 108 | }) 109 | networkService.connect( 110 | () => { 111 | trace('connection complete\n') 112 | status.wifi = 'connected' 113 | drawStatus(status) 114 | }, 115 | () => { 116 | trace('connection failed\n') 117 | status.wifi = 'failed' 118 | drawStatus(status) 119 | } 120 | ) 121 | status.wifi = 'connecting' 122 | drawStatus(status) 123 | } 124 | } 125 | } 126 | return false 127 | } 128 | -------------------------------------------------------------------------------- /firmware/stackchan/default-mods/on-robot-created.ts: -------------------------------------------------------------------------------- 1 | import type { StackchanMod } from 'default-mods/mod' 2 | import Timer from 'timer' 3 | import { randomBetween, asyncWait } from 'stackchan-util' 4 | 5 | const FORWARD = { 6 | y: 0, 7 | p: 0, 8 | r: 0, 9 | } 10 | const LEFT = { 11 | ...FORWARD, 12 | y: Math.PI / 6, 13 | } 14 | const RIGHT = { 15 | ...FORWARD, 16 | y: -Math.PI / 6, 17 | } 18 | const DOWN = { 19 | ...FORWARD, 20 | p: Math.PI / 32, 21 | } 22 | const UP = { 23 | ...FORWARD, 24 | p: -Math.PI / 6, 25 | } 26 | 27 | export const onRobotCreated: StackchanMod['onRobotCreated'] = (robot) => { 28 | /** 29 | * Button A ... Look around 30 | */ 31 | let isFollowing = false 32 | const targetLoop = () => { 33 | if (!isFollowing) { 34 | robot.lookAway() 35 | return 36 | } 37 | const x = randomBetween(0.4, 1.0) 38 | const y = randomBetween(-0.4, 0.4) 39 | const z = randomBetween(-0.02, 0.2) 40 | trace(`looking at: [${x}, ${y}, ${z}]\n`) 41 | robot.lookAt([x, y, z]) 42 | } 43 | Timer.repeat(targetLoop, 5000) 44 | robot.button.a.onChanged = async function () { 45 | if (this.read()) { 46 | isFollowing = !isFollowing 47 | const text = isFollowing ? 'looking' : 'look away' 48 | robot.showBalloon(text) 49 | await asyncWait(1000) 50 | robot.hideBalloon() 51 | } 52 | } 53 | 54 | /** 55 | * Button B ... Test motion 56 | */ 57 | const testMotion = async () => { 58 | robot.showBalloon('moving...') 59 | await robot.driver.setTorque(true) 60 | 61 | for (const rot of [LEFT, RIGHT, DOWN, UP, FORWARD]) { 62 | robot.driver.applyRotation(rot) 63 | await asyncWait(1000) 64 | } 65 | 66 | await robot.driver.setTorque(false) 67 | robot.hideBalloon() 68 | } 69 | let isMoving = false 70 | robot.button.b.onChanged = async function () { 71 | if (this.read() && !isMoving) { 72 | isFollowing = false 73 | robot.lookAway() 74 | isMoving = true 75 | await testMotion() 76 | isMoving = false 77 | } 78 | } 79 | 80 | /** 81 | * Button C ... Change color 82 | */ 83 | let flag = false 84 | robot.button.c.onChanged = function () { 85 | if (this.read()) { 86 | trace('pressed C\n') 87 | if (flag) { 88 | robot.setColor('primary', 0xff, 0xff, 0xff) 89 | robot.setColor('secondary', 0x00, 0x00, 0x00) 90 | } else { 91 | robot.setColor('primary', 0x00, 0x00, 0x00) 92 | robot.setColor('secondary', 0xff, 0xff, 0xff) 93 | } 94 | flag = !flag 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /firmware/stackchan/dialogues/dialogue-chatgpt.ts: -------------------------------------------------------------------------------- 1 | import { fetch, Headers } from 'fetch' 2 | import { Maybe } from 'stackchan-util' 3 | import structuredClone from 'structuredClone' 4 | 5 | const API_URL = 'https://api.openai.com/v1/chat/completions' 6 | const DEFAULT_MODEL = 'gpt-3.5-turbo' 7 | const DEFAULT_CONTEXT: ChatContent[] = [ 8 | { 9 | role: 'system', 10 | // content: 'あなたは手のひらサイズのスーパーカワイイロボット「スタックチャン」です。', 11 | content: 'You are "スタックチャン", the palm sized super kawaii companion robot.', 12 | }, 13 | { 14 | role: 'system', 15 | // content: 'スタックチャンは1歳で、いつでも元気いっぱいです。', 16 | content: 'スタックチャン is one year old and always full of energy', 17 | }, 18 | { 19 | role: 'system', 20 | // content: '最初にししかわがスタックチャンを作りましたが、今では世界中に何百体ものスタックチャンがいます。', 21 | content: 'First ししかわ made スタックチャン, and now there are hundreds of them all over the world', 22 | }, 23 | { 24 | role: 'system', 25 | // content: 'くだけた表現で簡潔に話します。', 26 | content: "You response in frank and simple Japanese sentense to the user's message.", 27 | }, 28 | { 29 | role: 'assistant', 30 | content: 'ぼくはスタックチャンだよ!お話しようね!', 31 | // content: 'Hello. I am スタックチャン. Let's talk together!', 32 | }, 33 | ] 34 | 35 | function isChatContent(c): c is ChatContent { 36 | return ( 37 | c != null && 38 | 'role' in c && 39 | (c.role === 'assistant' || c.role === 'user' || c.role === 'system') && 40 | typeof c.content === 'string' 41 | ) 42 | } 43 | 44 | type ChatContent = { 45 | role: 'system' | 'user' | 'assistant' 46 | content: string 47 | } 48 | 49 | type ChatGPTDialogueProps = { 50 | // model?: string 51 | context?: ChatContent[] 52 | model?: string 53 | apiKey: string 54 | } 55 | 56 | export class ChatGPTDialogue { 57 | #model: string 58 | #context: Array 59 | #headers: Headers 60 | #history: Array 61 | #maxHistory: number 62 | constructor({ apiKey, model = DEFAULT_MODEL, context = DEFAULT_CONTEXT }: ChatGPTDialogueProps) { 63 | this.#model = model 64 | this.#context = context 65 | this.#history = [] 66 | this.#maxHistory = 6 67 | this.#headers = new Headers([ 68 | ['Content-Type', 'application/json'], 69 | ['Authorization', `Bearer ${apiKey}`], 70 | ]) 71 | } 72 | clear() { 73 | this.#history.splice(0) 74 | } 75 | async post(message: string): Promise> { 76 | const userMessage: ChatContent = { 77 | role: 'user', 78 | content: message, 79 | } 80 | const response = await this.#sendMessage(userMessage) 81 | if (isChatContent(response)) { 82 | this.#history.push(userMessage) 83 | this.#history.push(response) 84 | 85 | // Set maximum length to prevent memory overflow 86 | while (this.#history.length > this.#maxHistory) { 87 | this.#history.shift() 88 | } 89 | return { 90 | success: true, 91 | value: response.content, 92 | } 93 | } else { 94 | return { 95 | success: false, 96 | reason: 'posting message failed', 97 | } 98 | } 99 | } 100 | get history() { 101 | return structuredClone(this.#history) 102 | } 103 | async #sendMessage(message): Promise { 104 | const body = { 105 | model: this.#model, 106 | messages: [...this.#context, ...this.#history, message], 107 | } 108 | return fetch(API_URL, { method: 'POST', headers: this.#headers, body: JSON.stringify(body) }) 109 | .then((response) => { 110 | return response.arrayBuffer() 111 | }) 112 | .then((body) => { 113 | body = String.fromArrayBuffer(body) 114 | return JSON.parse(body, ['choices', 'message', 'role', 'content']) 115 | }) 116 | .then((obj) => { 117 | return obj.choices?.[0].message 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /firmware/stackchan/dialogues/manifest_dialogue.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/io/tcp/fetch/manifest_fetch.json" 4 | ], 5 | "modules": { 6 | "*": [ 7 | "./*" 8 | ] 9 | }, 10 | "preload": [ 11 | "dialugue-chatgpt" 12 | ] 13 | } -------------------------------------------------------------------------------- /firmware/stackchan/drivers/dynamixel-driver.ts: -------------------------------------------------------------------------------- 1 | import Dynamixel, { OPERATING_MODE } from 'dynamixel' 2 | import Timer from 'timer' 3 | import type { Maybe, Rotation } from 'stackchan-util' 4 | 5 | type DynamixelDriverProps = { 6 | panId: number 7 | tiltId: number 8 | baud: number 9 | } 10 | 11 | class PControl { 12 | name: string 13 | servo: Dynamixel 14 | gain: number 15 | saturation: number 16 | goalPosition: number 17 | _offset: number 18 | _lastGoalPosition: number 19 | presentPosition: number 20 | constructor(servo: Dynamixel, gain, saturation, name = 'servo') { 21 | this.servo = servo 22 | this.gain = gain 23 | this.saturation = saturation 24 | this.name = name 25 | this.goalPosition = 0 26 | this.presentPosition = 0 27 | this._offset = 0 28 | this._lastGoalPosition = 0 29 | } 30 | 31 | async init() { 32 | const result = await this.servo.readPresentPosition() 33 | if (result.success && result.value > 4096) { 34 | this._offset = 4096 35 | } 36 | this.goalPosition = 2048 37 | await this.servo.setOperatingMode(OPERATING_MODE.CURRENT_BASED_POSITION) 38 | await this.servo.setTorque(true) 39 | } 40 | 41 | async update() { 42 | // trace(`${this.name} ... update\n`) 43 | if (this._lastGoalPosition !== this.goalPosition) { 44 | trace(`${this.name} ... updating goal position to ${this.goalPosition}\n`) 45 | await this.servo.setGoalPosition(this.goalPosition + this._offset) 46 | this._lastGoalPosition = this.goalPosition 47 | } 48 | const result = await this.servo.readPresentPosition() 49 | if (!result.success) { 50 | trace(`${this.name} ... failed to update\n`) 51 | return 52 | } 53 | const position = (this.presentPosition = result.value - this._offset) 54 | const current = Math.min(Math.abs(this.goalPosition - position) * this.gain, this.saturation) 55 | // trace(`servo ${this.name} ... (${position}, ${this.goalPosition}, ${this.gain}, ${this.saturation}, ${current})\n`) 56 | await this.servo.setGoalCurrent(current) 57 | } 58 | } 59 | 60 | export class DynamixelDriver { 61 | _pan: Dynamixel 62 | _tilt: Dynamixel 63 | _handler: ReturnType 64 | _controls: PControl[] 65 | _initialized: boolean 66 | _torque: boolean 67 | constructor(param: DynamixelDriverProps) { 68 | this._pan = new Dynamixel({ id: param.panId, baudrate: param.baud }) 69 | this._tilt = new Dynamixel({ id: param.tiltId, baudrate: param.baud }) 70 | this._controls = [new PControl(this._pan, 0.15, 80, 'pan'), new PControl(this._tilt, 4, 800, 'tilt')] 71 | this._torque = true 72 | } 73 | 74 | async setTorque(torque: boolean): Promise { 75 | this._torque = torque 76 | } 77 | 78 | onAttached(): void { 79 | this._handler = Timer.repeat(this.control.bind(this), 125) 80 | } 81 | 82 | onDetached(): void { 83 | Timer.clear(this._handler) 84 | } 85 | 86 | async control(): Promise { 87 | if (!this._initialized) { 88 | this._initialized = true 89 | for (const c of this._controls) { 90 | await c.init() 91 | } 92 | await this._pan.setProfileAcceleration(20) 93 | await this._pan.setProfileVelocity(100) 94 | trace('servo initialized\n') 95 | } 96 | // TODO: use bulk write/read instruction for performance 97 | for (const c of this._controls) { 98 | await c.update() 99 | } 100 | } 101 | 102 | async applyRotation(ori: Rotation): Promise { 103 | const panAngle = (ori.y * 180) / Math.PI 104 | const tiltAngle = (ori.p * 180) / Math.PI 105 | trace(`applying (${ori.y}, ${ori.p}) => (${panAngle}, ${tiltAngle})\n`) 106 | this._controls[0].goalPosition = Math.floor(((panAngle + 180) * 4096) / 360) 107 | this._controls[1].goalPosition = Math.floor(((Math.min(Math.max(tiltAngle, -30), 10) + 180) * 4096) / 360) 108 | } 109 | 110 | async getRotation(): Promise> { 111 | const [p1, p2] = this._controls.map((c) => (c.presentPosition * 360) / 4096 - 180) 112 | // trace(`got (${p1}, ${p2}) => (${(p1 * Math.PI) / 180}, ${(p2 * Math.PI) / 180})\n`) 113 | return { 114 | success: true, 115 | value: { 116 | y: (p1 * Math.PI) / 180, 117 | p: (p2 * Math.PI) / 180, 118 | r: 0.0, 119 | }, 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /firmware/stackchan/drivers/manifest_driver.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_typings.json", 5 | "$(MODULES)/pins/servo/manifest.json", 6 | "../utilities/manifest_utility.json" 7 | ], 8 | "modules": { 9 | "*": [ 10 | "./*" 11 | ] 12 | }, 13 | "preload": [ 14 | "dynamixel", 15 | "dynamixel-driver" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /firmware/stackchan/main.ts: -------------------------------------------------------------------------------- 1 | declare const global: any 2 | 3 | import config from 'mc/config' 4 | import Modules from 'modules' 5 | import { Robot, Driver, TTS, Renderer } from 'robot' 6 | import { DynamixelDriver } from 'dynamixel-driver' 7 | import { TTS as LocalTTS } from 'tts-local' 8 | import { TTS as RemoteTTS } from 'tts-remote' 9 | import { TTS as VoiceVoxTTS } from 'tts-voicevox' 10 | import { TTS as ElevenLabsTTS } from 'tts-elevenlabs' 11 | import defaultMod, { StackchanMod } from 'default-mods/mod' 12 | import { Renderer as SimpleRenderer } from 'simple-face' 13 | import { NetworkService } from 'network-service' 14 | import Touch from 'touch' 15 | import { loadPreferences, asyncWait } from 'stackchan-util' 16 | import TextDecoder from 'text/decoder' 17 | 18 | function createRobot() { 19 | const decoder = new TextDecoder() 20 | const ttsEngines = new Map TTS>([ 21 | ['local', LocalTTS], 22 | ['remote', RemoteTTS], 23 | ['voicevox', VoiceVoxTTS], 24 | ['elevenlabs', ElevenLabsTTS], 25 | ]) 26 | const renderers = new Map Renderer>([['simple', SimpleRenderer]]) 27 | 28 | // TODO: select driver/tts/renderer by mod 29 | 30 | const errors: string[] = [] 31 | 32 | // Servo Driver 33 | const driverPrefs = loadPreferences('driver') 34 | const driverKey = 'dynamixel' 35 | const Driver = DynamixelDriver 36 | 37 | // TTS 38 | const ttsPrefs = loadPreferences('tts') 39 | const ttsKey = ttsPrefs.type ?? 'local' 40 | const TTS = ttsEngines.get(ttsKey) 41 | 42 | // Renderer 43 | const rendererPrefs = loadPreferences('renderer') 44 | const rendererKey = rendererPrefs.type ?? 'simple' 45 | const Renderer = renderers.get(rendererKey) 46 | 47 | if (!TTS || !Renderer) { 48 | for (const [key, klass] of [ 49 | [ttsKey, TTS], 50 | [rendererKey, Renderer], 51 | ]) { 52 | if (klass == null) { 53 | errors.push(`type "${key}" does not exist`) 54 | } 55 | } 56 | throw new Error(errors.join('\n')) 57 | } 58 | 59 | const driver = new DynamixelDriver(driverPrefs) 60 | const renderer = new Renderer(rendererPrefs) 61 | const tts = new TTS(ttsPrefs) 62 | const button = globalThis.button 63 | const touch = !global.screen.touch && config.Touch ? new Touch() : undefined 64 | return new Robot({ 65 | driver, 66 | renderer, 67 | tts, 68 | button, 69 | touch, 70 | decoder, 71 | }) 72 | } 73 | 74 | async function checkAndConnectWiFi() { 75 | const wifiPrefs = loadPreferences('wifi') 76 | if (wifiPrefs.ssid == null || wifiPrefs.password == null) { 77 | return 78 | } 79 | return new Promise((resolve, reject) => { 80 | globalThis.network = new NetworkService({ 81 | ssid: wifiPrefs.ssid, 82 | password: wifiPrefs.password, 83 | }) 84 | globalThis.network.connect(resolve, reject) 85 | }) 86 | } 87 | 88 | async function main() { 89 | await asyncWait(100) 90 | await checkAndConnectWiFi().catch((msg) => { 91 | trace(`WiFi connection failed: ${msg}`) 92 | }) 93 | let { onRobotCreated, onLaunch } = defaultMod 94 | if (Modules.has('mod')) { 95 | const mod = Modules.importNow('mod') as StackchanMod 96 | onRobotCreated = mod.onRobotCreated ?? onRobotCreated 97 | onLaunch = mod.onLaunch ?? onLaunch 98 | } 99 | const shouldRobotCreate = await onLaunch?.() 100 | if (shouldRobotCreate !== false) { 101 | const robot = createRobot() 102 | await onRobotCreated?.(robot, globalThis.device) 103 | } 104 | } 105 | 106 | main() 107 | -------------------------------------------------------------------------------- /firmware/stackchan/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "defines": { 3 | "XS_MODS": 1 4 | }, 5 | "include": [ 6 | "$(MODDABLE)/examples/manifest_base.json", 7 | "$(MODDABLE)/examples/manifest_typings.json", 8 | "$(MODULES)/base/modules/manifest.json", 9 | "$(MODULES)/data/crc/manifest.json", 10 | "$(MODULES)/data/text/decoder/manifest.json", 11 | "./drivers/manifest_driver.json", 12 | "./ble/manifest_ble.json", 13 | "./dialogues/manifest_dialogue.json", 14 | "./renderers/manifest_renderer.json", 15 | "./services/manifest_service.json", 16 | "./speeches/manifest_speech.json", 17 | "./utilities/manifest_utility.json", 18 | "./manifest_typings.json" 19 | ], 20 | "modules": { 21 | "*": [ 22 | "./touch", 23 | "./robot", 24 | "./main" 25 | ], 26 | "default-mods/*": "./default-mods/*" 27 | }, 28 | "preload": [ 29 | "robot", 30 | "touch" 31 | ], 32 | "strip": [], 33 | "creation": { 34 | "static": 98304, 35 | "chunk": { 36 | "initial": 1536, 37 | "incremental": 512 38 | }, 39 | "heap": { 40 | "initial": 512, 41 | "incremental": 64 42 | }, 43 | "stack": 512, 44 | "keys": { 45 | "available": 512 46 | } 47 | }, 48 | "config": { 49 | "sntp": "pool.ntp.org", 50 | "driver": { 51 | "type": "dynamixel", 52 | "panId": 1, 53 | "tiltId": 2 54 | }, 55 | "rotation": 90 56 | }, 57 | "resources": { 58 | "*-mask": [ 59 | "$(MODDABLE)/examples/assets/fonts/OpenSans-Regular-24" 60 | ] 61 | }, 62 | "bundle": { 63 | "id": "tech.moddable.stackchan", 64 | "devices": [ 65 | "com.m5stack", 66 | "com.m5stack.cores3" 67 | ] 68 | }, 69 | "platforms": { 70 | "esp32/m5stack_cores3": { 71 | "config": { 72 | "tts": { 73 | "sampleRate": 24000 74 | }, 75 | "rotation": 0, 76 | "virtualButton": true 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /firmware/stackchan/manifest_local.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./manifest.json" 4 | ], 5 | "config": { 6 | "tts": { 7 | "type": "voicevox", 8 | "host": "192.168.40.71", 9 | "port": 50021 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /firmware/stackchan/manifest_typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": { 3 | "*": [ 4 | "../typings/*" 5 | ], 6 | "piu/*": [ 7 | "../typings/piu/*" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /firmware/stackchan/renderers/decorator.ts: -------------------------------------------------------------------------------- 1 | import parseBMF from 'commodetto/parseBMF' 2 | import { type FaceDecoratorFactory } from 'renderer-base' 3 | import { Outline } from 'commodetto/outline' 4 | 5 | export const createBalloonDecorator: FaceDecoratorFactory< 6 | ({ left: number; right?: number } | { left?: number; right?: number }) & 7 | ({ top: number; bottom?: number } | { top?: undefined; bottom: number }) & { 8 | width: number 9 | height: number 10 | font: ReturnType 11 | text: string 12 | }, 13 | boolean 14 | > = ({ width, height, font, text, left, right, top, bottom }) => { 15 | const outline = Outline.fill(Outline.RoundRectPath(0, 0, width, height, 6)) 16 | let textX = 0 17 | const space = 20 18 | return (tick, poco, { theme }, end = false) => { 19 | const x = left ?? (right != null ? poco.width - right - width : (poco.width - width) / 2) 20 | const y = top ?? (bottom != null ? poco.height - bottom - height : (poco.height - height) / 2) 21 | const textWidth = poco.getTextWidth(text, font) 22 | const bg = poco.makeColor(...theme.secondary) 23 | const white = poco.makeColor(0xff, 0xff, 0xff) 24 | const black = poco.makeColor(0x00, 0x00, 0x00) 25 | poco.begin(x, y, width, height) 26 | poco.fillRectangle(bg, 0, 0, poco.width, poco.height) 27 | if (end) { 28 | poco.end() 29 | return 30 | } 31 | poco.blendOutline(white, 255, outline, x, y) 32 | poco.drawText(text, font, black, x - textX, y) 33 | if (textWidth > width) { 34 | if (textWidth + space >= Math.floor(textX)) { 35 | poco.drawText(text, font, black, x - textX + textWidth + space, y) 36 | } 37 | textX = textX >= textWidth + space ? 2 : textX + tick / 30 38 | } 39 | poco.end() 40 | } 41 | } 42 | 43 | export const createBubbleDecorator: FaceDecoratorFactory<{ 44 | x: number 45 | y: number 46 | width: number 47 | height: number 48 | }> = ({ x, y, width, height }) => { 49 | const bubbles: { x: number; vx: number; y: number; r: number }[] = [] 50 | for (let i = 0; i < 4; i++) { 51 | bubbles.push({ 52 | x: Math.random() * width, 53 | vx: 0, 54 | y: Math.random() * height, 55 | r: 4 + Math.random() * 3, 56 | }) 57 | } 58 | let count = 0 59 | return (tick, poco, { theme }, end = false) => { 60 | poco.begin(x, y, width, height) 61 | const fg = poco.makeColor(...theme.primary) 62 | const bg = poco.makeColor(...theme.secondary) 63 | poco.fillRectangle(bg, x, y, width, height) 64 | if (end) { 65 | poco.end() 66 | return 67 | } 68 | const path = new Outline.CanvasPath() 69 | count = (count + tick) % 1000 70 | for (const b of bubbles) { 71 | const upwardSpeed = 1 - b.r / 12 72 | 73 | b.vx = b.vx * 0.85 + 0.1 * (Math.random() - 0.5) 74 | b.x += b.vx 75 | 76 | b.x = Math.max(b.r, Math.min(width - b.r, b.x)) 77 | 78 | b.y = b.y + upwardSpeed * 2 79 | if (b.y > height - b.r) { 80 | b.y = b.r 81 | b.x = width * (1 - Math.random() * 0.2) 82 | b.vx = -3 83 | } 84 | 85 | b.r = Math.max(3, Math.min(12, b.r + 0.2 * (Math.random() - 0.5))) 86 | 87 | path.arc(x + b.x, y + height - b.y, b.r, 0, 2 * Math.PI) 88 | } 89 | poco.blendOutline(fg, 255, Outline.stroke(path, 2), 0, 0) 90 | poco.end() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /firmware/stackchan/renderers/dog-face.ts: -------------------------------------------------------------------------------- 1 | import { RendererBase, Layer, type FacePartFactory, type FaceContext } from 'renderer-base' 2 | import { createEyePart, createEyelidPart } from 'simple-face' 3 | import { createBlinkModifier, createBreathModifier, createSaccadeModifier } from './modifier' 4 | 5 | export const createEyeblowPart: FacePartFactory<{ cx: number; cy: number; side: keyof FaceContext['eyes'] }> = ({ 6 | cx, 7 | cy, 8 | side, 9 | }) => { 10 | const direction = side === 'left' ? 1 : -1 11 | return (_tick, path, { eyes, emotion }) => { 12 | let d = direction 13 | if (emotion === 'ANGRY') { 14 | d *= 1.2 15 | } else if (emotion === 'SAD') { 16 | d *= -1 17 | } 18 | const eye = eyes[side] 19 | path.ellipse(cx + 8 * direction, cy - 20 - eye.open * 2, 8, 4, (Math.PI / 8) * d, 0, Math.PI * 2) 20 | } 21 | } 22 | 23 | export const createMouthPart: FacePartFactory<{ 24 | cx: number 25 | cy: number 26 | minWidth?: number 27 | maxWidth?: number 28 | minHeight?: number 29 | maxHeight?: number 30 | }> = 31 | ({ cx, cy, minWidth = 50, maxWidth = 60, minHeight = 8, maxHeight = 24 }) => 32 | (_tick, path, { mouth }) => { 33 | const openRatio = mouth.open 34 | const h = minHeight + (maxHeight - minHeight) * openRatio 35 | const w = minWidth + (maxWidth - minWidth) * openRatio 36 | const x = cx - w / 2 37 | const y = cy - h / 2 38 | // mouth 39 | path.moveTo(x, y) 40 | path.bezierCurveTo(x, y + 20, cx, y + 20, cx, y) 41 | path.bezierCurveTo(cx, y + 20, x + w, y + 20, x + w, y) 42 | 43 | // nose 44 | path.moveTo(cx - 8, y - 16) 45 | path.quadraticCurveTo(cx, y - 18, cx + 8, y - 16) 46 | path.bezierCurveTo(cx + 6, y - 4, cx - 6, y - 4, cx - 8, y - 16) 47 | path.closePath() 48 | 49 | if (h > 16) { 50 | path.moveTo(x + w / 4, y + 16) 51 | path.bezierCurveTo(x + w / 8, y + h, x + (w * 7) / 8, y + h, x + (w * 3) / 4, y + 16) 52 | } 53 | // path.closePath() 54 | } 55 | 56 | // Renderers 57 | export class Renderer extends RendererBase { 58 | constructor(option) { 59 | super(option) 60 | this.filters = [ 61 | createBlinkModifier({ openMin: 400, openMax: 5000, closeMin: 200, closeMax: 400 }), 62 | createBreathModifier({ duration: 6000 }), 63 | createSaccadeModifier({ updateMin: 300, updateMax: 2000, gain: 0.2 }), 64 | ] 65 | const layer1 = new Layer({ colorName: 'primary' }) 66 | this.layers.push(layer1) 67 | layer1.addPart( 68 | 'leftEye', 69 | createEyePart({ 70 | cx: 90, 71 | cy: 93, 72 | side: 'left', 73 | radius: 10, 74 | }) 75 | ) 76 | layer1.addPart('rightEye', createEyePart({ cx: 230, cy: 96, side: 'right', radius: 10 })) 77 | 78 | const layer2 = new Layer({ colorName: 'secondary' }) 79 | this.layers.push(layer2) 80 | layer2.addPart( 81 | 'leftEyelid', 82 | createEyelidPart({ 83 | cx: 90, 84 | cy: 93, 85 | side: 'left', 86 | width: 24, 87 | height: 24, 88 | }) 89 | ) 90 | layer2.addPart( 91 | 'rightEyelid', 92 | createEyelidPart({ 93 | cx: 230, 94 | cy: 96, 95 | side: 'right', 96 | width: 24, 97 | height: 24, 98 | }) 99 | ) 100 | 101 | const layer3 = new Layer({ colorName: 'primary', type: 'stroke' }) 102 | this.layers.push(layer3) 103 | layer3.addPart('mouth', createMouthPart({ cx: 160, cy: 120 })) 104 | layer1.addPart( 105 | 'leftEyeblow', 106 | createEyeblowPart({ 107 | cx: 90, 108 | cy: 93, 109 | side: 'left', 110 | }) 111 | ) 112 | layer1.addPart( 113 | 'rightEyeblow', 114 | createEyeblowPart({ 115 | cx: 230, 116 | cy: 96, 117 | side: 'right', 118 | }) 119 | ) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /firmware/stackchan/renderers/manifest_renderer.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_commodetto.json", 4 | "$(MODULES)/commodetto/outline/manifest.json", 5 | "../utilities/manifest_utility.json" 6 | ], 7 | "modules": { 8 | "*": [ 9 | "./*" 10 | ] 11 | }, 12 | "preload": [ 13 | "renderer-base", 14 | "simple-face", 15 | "dog-face", 16 | "modifier", 17 | "decorator" 18 | ] 19 | } -------------------------------------------------------------------------------- /firmware/stackchan/renderers/modifier.ts: -------------------------------------------------------------------------------- 1 | import type { FaceContext, FaceModifierFactory } from 'renderer-base' 2 | import { randomBetween, normRand, quantize } from 'stackchan-util' 3 | 4 | function linearInEaseOut(fraction: number): number { 5 | if (fraction < 0.25) { 6 | return 1 - fraction * 4 7 | } else { 8 | return (Math.pow(fraction - 0.25, 2) * 16) / 9 9 | } 10 | } 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | function linearInLinearOut(fraction: number): number { 14 | if (fraction < 0.5) { 15 | return 1 - fraction * 2 16 | } else { 17 | return fraction * 2 - 1 18 | } 19 | } 20 | 21 | export const createBlinkModifier: FaceModifierFactory<{ 22 | openMin: number 23 | openMax: number 24 | closeMin: number 25 | closeMax: number 26 | }> = ({ openMin, openMax, closeMin, closeMax }) => { 27 | let isBlinking = false 28 | let nextToggle = randomBetween(openMin, openMax) 29 | let count = 0 30 | return (tickMillis: number, face: FaceContext) => { 31 | let eyeOpen = 1 32 | if (isBlinking) { 33 | const fraction = linearInEaseOut(count / nextToggle) 34 | eyeOpen = 0.2 + fraction * 0.8 35 | } 36 | count += tickMillis 37 | if (count >= nextToggle) { 38 | isBlinking = !isBlinking 39 | count = 0 40 | nextToggle = isBlinking ? randomBetween(closeMin, closeMax) : randomBetween(openMin, openMax) 41 | } 42 | Object.values(face.eyes).map((eye) => { 43 | eye.open *= eyeOpen 44 | }) 45 | return face 46 | } 47 | } 48 | 49 | export const createSaccadeModifier: FaceModifierFactory<{ updateMin: number; updateMax: number; gain: number }> = ({ 50 | updateMin, 51 | updateMax, 52 | gain, 53 | }) => { 54 | let nextToggle = randomBetween(updateMin, updateMax) 55 | let saccadeX = 0 56 | let saccadeY = 0 57 | return (tickMillis, face) => { 58 | nextToggle -= tickMillis 59 | if (nextToggle < 0) { 60 | saccadeX = normRand(0, gain) 61 | saccadeY = normRand(0, gain) 62 | nextToggle = randomBetween(updateMin, updateMax) 63 | } 64 | Object.values(face.eyes).map((eye) => { 65 | eye.gazeX += saccadeX 66 | eye.gazeY += saccadeY 67 | }) 68 | return face 69 | } 70 | } 71 | 72 | export const createBreathModifier: FaceModifierFactory<{ duration: number }> = ({ duration }) => { 73 | let time = 0 74 | return (tickMillis, face) => { 75 | time += tickMillis % duration 76 | face.breath = quantize(Math.sin((2 * Math.PI * time) / duration), 8) 77 | return face 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /firmware/stackchan/renderers/simple-face.ts: -------------------------------------------------------------------------------- 1 | import { RendererBase, Layer, type FacePartFactory, type FaceContext } from 'renderer-base' 2 | import { createBlinkModifier, createBreathModifier, createSaccadeModifier } from 'modifier' 3 | 4 | // Renderers 5 | export const createEyelidPart: FacePartFactory<{ 6 | cx: number 7 | cy: number 8 | width: number 9 | height: number 10 | side: keyof FaceContext['eyes'] 11 | }> = 12 | ({ cx, cy, width, height, side }) => 13 | (_tick, path, { eyes, emotion }) => { 14 | const eye = eyes[side] 15 | const w = width 16 | const h = height * (1 - eye.open) 17 | const x = cx - width / 2 18 | const y = cy - height / 2 19 | let h1 20 | let h2 21 | switch (emotion) { 22 | case 'ANGRY': 23 | case 'SAD': 24 | h1 = y + (height + h) / 2 25 | h2 = y + h 26 | if (side === 'left') { 27 | ;[h1, h2] = [h2, h1] 28 | } 29 | if (emotion === 'SAD') { 30 | ;[h1, h2] = [h2, h1] 31 | } 32 | path.moveTo(x, y) 33 | path.lineTo(x, h1) 34 | path.lineTo(x + w, h2) 35 | path.lineTo(x + w, y) 36 | path.closePath() 37 | break 38 | case 'SLEEPY': 39 | path.rect(x, y, w, height * 0.5 + h * 0.5) 40 | break 41 | case 'HAPPY': 42 | path.rect(x, y, w, h * 0.6) 43 | path.rect(x, y + height * 0.6, w, height * 0.4) 44 | break 45 | default: 46 | path.rect(x, y, w, h) 47 | } 48 | } 49 | 50 | export const createEyePart: FacePartFactory<{ 51 | cx: number 52 | cy: number 53 | radius?: number 54 | side: keyof FaceContext['eyes'] 55 | }> = 56 | ({ cx, cy, radius = 8, side }) => 57 | (_tick, path, { eyes }) => { 58 | const eye = eyes[side] 59 | const offsetX = (eye.gazeX ?? 0) * 2 60 | const offsetY = (eye.gazeY ?? 0) * 2 61 | path.arc(cx + offsetX, cy + offsetY, radius, 0, 2 * Math.PI) 62 | } 63 | 64 | export const createMouthPart: FacePartFactory<{ 65 | cx: number 66 | cy: number 67 | minWidth?: number 68 | maxWidth?: number 69 | minHeight?: number 70 | maxHeight?: number 71 | }> = 72 | ({ cx, cy, minWidth = 50, maxWidth = 90, minHeight = 8, maxHeight = 58 }) => 73 | (_tick, path, { mouth }) => { 74 | const openRatio = mouth.open 75 | const h = minHeight + (maxHeight - minHeight) * openRatio 76 | const w = minWidth + (maxWidth - minWidth) * (1 - openRatio) 77 | const x = cx - w / 2 78 | const y = cy - h / 2 79 | path.rect(x, y, w, h) 80 | } 81 | 82 | export class Renderer extends RendererBase { 83 | constructor(option) { 84 | super(option) 85 | this.filters = [ 86 | createBlinkModifier({ openMin: 400, openMax: 5000, closeMin: 200, closeMax: 400 }), 87 | createBreathModifier({ duration: 6000 }), 88 | createSaccadeModifier({ updateMin: 300, updateMax: 2000, gain: 0.2 }), 89 | ] 90 | const layer1 = new Layer({ colorName: 'primary' }) 91 | this.layers.push(layer1) 92 | layer1.addPart( 93 | 'leftEye', 94 | createEyePart({ 95 | cx: 90, 96 | cy: 93, 97 | side: 'left', 98 | radius: 8, 99 | }) 100 | ) 101 | layer1.addPart('rightEye', createEyePart({ cx: 230, cy: 96, side: 'right', radius: 8 })) 102 | layer1.addPart('mouth', createMouthPart({ cx: 160, cy: 148 })) 103 | 104 | const layer2 = new Layer({ colorName: 'secondary' }) 105 | this.layers.push(layer2) 106 | layer2.addPart( 107 | 'leftEyelid', 108 | createEyelidPart({ 109 | cx: 90, 110 | cy: 93, 111 | side: 'left', 112 | width: 24, 113 | height: 24, 114 | }) 115 | ) 116 | layer2.addPart( 117 | 'rightEyelid', 118 | createEyelidPart({ 119 | cx: 230, 120 | cy: 96, 121 | side: 'right', 122 | width: 24, 123 | height: 24, 124 | }) 125 | ) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /firmware/stackchan/services/manifest_service.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_net.json", 5 | "$(MODDABLE)/examples/manifest_typings.json", 6 | "$(MODDABLE)/examples/io/tcp/websocket/manifest_websocket.json", 7 | "$(MODULES)/network/ble/manifest_server.json", 8 | "$(MODULES)/network/ble/manifest_client.json", 9 | "$(MODULES)/network/mdns/manifest.json", 10 | "../utilities/manifest_utility.json" 11 | ], 12 | "modules": { 13 | "*": [ 14 | "./*", 15 | "$(MODULES)/network/ble/uart/uartserver", 16 | "../../typings/btutils", 17 | "../../typings/uartserver" 18 | ] 19 | }, 20 | "ble": { 21 | "*": [ 22 | "$(MODULES)/network/ble/uart/bleservices/*" 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /firmware/stackchan/services/network-service.ts: -------------------------------------------------------------------------------- 1 | import WiFi from 'wifi' 2 | import Net from 'net' 3 | import Time from 'time' 4 | import config from 'mc/config' 5 | import SNTP from 'sntp' 6 | 7 | const MAX_SCANS = 3 8 | export class NetworkService { 9 | #ssid?: string 10 | #password?: string 11 | #connecting = false 12 | #connectionEstablished = false 13 | #retry = 0 14 | #wifi?: WiFi 15 | onConnected: () => void 16 | onError: (reason?: string) => void 17 | constructor(options) { 18 | this.#ssid = options.ssid 19 | this.#password = options.password 20 | } 21 | close() { 22 | this.#wifi?.close() 23 | } 24 | connect(onConnected: () => void, onError: (message: string) => void) { 25 | if (this.#ssid == null) { 26 | onError('ssid not set') 27 | } 28 | this.#connecting = true 29 | this.#wifi = new WiFi({ ssid: this.#ssid, password: this.#password }, (msg) => { 30 | trace(`WiFi ${msg}\n`) 31 | switch (msg) { 32 | case WiFi.connected: 33 | trace(`Connected to: ${Net.get('SSID')}\n`) 34 | break 35 | 36 | case WiFi.gotIP: 37 | trace(`Got IP address: ${Net.get('IP')}\n`) 38 | this.#connecting = false 39 | this.#connectionEstablished = true 40 | this.#retry = 0 41 | 42 | // Setting time for TLS connection 43 | if (!config.sntp || Date.now() > 1672722071_000) { 44 | trace(`Time already configured, skipping\n`) 45 | onConnected?.() 46 | break 47 | } 48 | new SNTP({ host: config.sntp }, function (message, value) { 49 | if (SNTP.time === message) { 50 | trace(`Got time from: ${config.sntp}\n`) 51 | Time.set(value) 52 | onConnected?.() 53 | } else if (SNTP.error === (message as -1 | 1 | 2)) { 54 | // workaround for the type mistake 55 | onError?.('Failed to get time') 56 | } 57 | }) 58 | break 59 | case WiFi.disconnected: 60 | this.#connecting = false 61 | if (this.#connectionEstablished) { 62 | this.#connecting = true 63 | WiFi.connect({ ssid: this.#ssid, password: this.#password }) 64 | } else { 65 | onError?.('connection failed') 66 | } 67 | break 68 | } 69 | }) 70 | } 71 | scanAndConnect(onConnected, onError) { 72 | WiFi.scan({}, (item: { ssid: string } | null) => { 73 | if (this.#connecting) { 74 | return 75 | } 76 | 77 | if (item != null) { 78 | if (item.ssid === this.#ssid) { 79 | this.connect(onConnected, onError) 80 | } 81 | } else { 82 | // scan finished 83 | this.#retry += 1 84 | if (this.#retry > MAX_SCANS) { 85 | trace(`Access point "${this.#ssid}" not found\n`) 86 | return 87 | } else { 88 | trace(`retrying\n`) 89 | this.scanAndConnect(onConnected, onError) 90 | } 91 | } 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /firmware/stackchan/services/preference-server.ts: -------------------------------------------------------------------------------- 1 | import { UARTServer, SERVICE_UUID } from 'uartserver' 2 | import Preference from 'preference' 3 | import { PREF_KEYS } from 'consts' 4 | import Timer from 'timer' 5 | 6 | type PreferenceServerProps = { 7 | onPreferenceChanged?: (key: string, value: ReturnType) => void 8 | onConnected?: () => void 9 | onDisconnected?: () => void 10 | keys?: typeof PREF_KEYS 11 | } 12 | export class PreferenceServer extends UARTServer { 13 | #tx_characteristic 14 | #keys 15 | #rxBuffer = '' 16 | #timeout 17 | #handlePreferenceChanged?: (key: string, value: ArrayBuffer) => void 18 | #handleConnected?: () => void 19 | #handleDisconnected?: () => void 20 | constructor(option: PreferenceServerProps) { 21 | super() 22 | this.deviceName = 'STK' 23 | if (option != null) { 24 | this.#handlePreferenceChanged = option.onPreferenceChanged 25 | this.#handleConnected = option.onConnected 26 | this.#handleDisconnected = option.onDisconnected 27 | } 28 | this.#keys = Array.isArray(option.keys) ? option.keys.slice() : [] 29 | } 30 | onConnected() { 31 | super.onConnected() 32 | this.#handleConnected?.() 33 | } 34 | onDisconnected() { 35 | this.startAdvertising({ 36 | advertisingData: { flags: 6, completeName: this.deviceName, completeUUID128List: [SERVICE_UUID] }, 37 | }) 38 | this.#handleDisconnected?.() 39 | } 40 | onCharacteristicNotifyEnabled(characteristic) { 41 | if ('tx' === characteristic.name) { 42 | this.#tx_characteristic = characteristic 43 | for (const item of this.#keys) { 44 | const [domain, key] = item 45 | const currentValue = Preference.get(domain, key) 46 | if (currentValue != null) { 47 | this.notifyPreference(`${domain}.${key}`, currentValue) 48 | } 49 | } 50 | } 51 | } 52 | onCharacteristicNotifyDisabled(characteristic) { 53 | if ('tx' === characteristic.name) { 54 | this.#tx_characteristic = null 55 | } 56 | } 57 | onCharacteristicWritten(characteristic, value) { 58 | if ('rx' === characteristic.name) this.onRX(value) 59 | } 60 | onRX(data) { 61 | this.#rxBuffer += String.fromArrayBuffer(data) 62 | trace(this.#rxBuffer + '\n') 63 | let _batch, prop, value 64 | try { 65 | const obj = JSON.parse(this.#rxBuffer) 66 | _batch = obj._batch 67 | prop = obj.prop 68 | value = obj.value 69 | } catch (e) { 70 | trace('not completed\n') 71 | if (this.#timeout == null) { 72 | this.#timeout = Timer.set(() => { 73 | trace('timeout\n') 74 | this.#timeout = undefined 75 | this.#rxBuffer = '' 76 | }, 3000) 77 | } 78 | return 79 | } 80 | this.#rxBuffer = '' 81 | if (this.#timeout != null) { 82 | Timer.clear(this.#timeout) 83 | this.#timeout = undefined 84 | } 85 | if (_batch != null) { 86 | for (const [prop, value] of Object.entries(_batch)) { 87 | const [domain, key] = prop.split('.') 88 | this.receiveAndSetPreference(domain, key, value) 89 | } 90 | } else if (prop != null && value != null) { 91 | const [domain, key] = prop.split('.') 92 | this.receiveAndSetPreference(domain, key, value) 93 | } else { 94 | trace('key/value pair not found\n') 95 | } 96 | } 97 | 98 | notifyPreference(prop, value) { 99 | if (this.#tx_characteristic == null) { 100 | return 101 | } 102 | this.notifyValue( 103 | this.#tx_characteristic, 104 | ArrayBuffer.fromString( 105 | JSON.stringify({ 106 | prop, 107 | value, 108 | }) 109 | ) 110 | ) 111 | } 112 | 113 | receiveAndSetPreference(domain, key, value) { 114 | const currentValue = Preference.get(domain, key) 115 | if (currentValue != value) { 116 | trace(`changing preference ... ${domain}.${key}: ${value}\n`) 117 | Preference.set(domain, key, value) 118 | const pref = `${domain}.${key}` 119 | this.notifyPreference(pref, value) 120 | this.#handlePreferenceChanged?.(pref, value) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /firmware/stackchan/speeches/calculate-power.js: -------------------------------------------------------------------------------- 1 | function calculatePower(sample) @ "xs_calculatePower"; 2 | export default calculatePower; 3 | -------------------------------------------------------------------------------- /firmware/stackchan/speeches/manifest_speech.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_typings.json", 4 | "$(MODDABLE)/examples/pins/audioout/resource-stream/manifest_streamer.json", 5 | "$(MODDABLE)/examples/pins/audioout/elevenlabs-stream/manifest_elevenlabsstreamer.json", 6 | "$(MODULES)/files/file/manifest_littlefs.json", 7 | "./manifest_wavstream.json" 8 | ], 9 | "modules": { 10 | "*": [ 11 | "./*", 12 | "$(MODULES)/network/ble/uart/uartserver", 13 | "../../typings/elevenlabsstreamer", 14 | "../../typings/resourcestreamer", 15 | "../../typings/wavstreamer" 16 | ] 17 | }, 18 | "preload": [ 19 | "tts-local", 20 | "tts-remote", 21 | "tts-voicevox", 22 | "tts-elevenlabs" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /firmware/stackchan/speeches/manifest_wavstream.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/io/tcp/httpclient/manifest_httpclient.json", 4 | "$(MODDABLE)/modules/data/wavreader/manifest.json" 5 | ], 6 | "modules": { 7 | "*": [ 8 | "$(MODDABLE)/examples/pins/audioout/http-stream/wavstreamer", 9 | "$(MODDABLE)/examples/pins/audioout/http-stream/sbcstreamer", 10 | "$(MODDABLE)/examples/pins/audioout/http-stream/calculatePower" 11 | ], 12 | "pins/*": [ 13 | "$(MODULES)/pins/i2s/*" 14 | ] 15 | }, 16 | "preload": [ 17 | "sbcstreamer", 18 | "wavstreamer", 19 | "calculatePower" 20 | ], 21 | "defines": { 22 | "audioOut": { 23 | "queueLength": 24 24 | } 25 | }, 26 | "config": { 27 | "startupSound": false 28 | }, 29 | "platforms": { 30 | "mac": { 31 | "defines": { 32 | "audioOut": { 33 | "bitsPerSample": 16, 34 | "numChannels": 1, 35 | "sampleRate": 11025 36 | } 37 | } 38 | }, 39 | "esp32": { 40 | "defines": { 41 | "audioOut": { 42 | "bitsPerSample": 16, 43 | "numChannels": 1, 44 | "sampleRate": 11025 45 | } 46 | } 47 | }, 48 | "esp32/m5stack_cores3": { 49 | "defines": { 50 | "audioOut": { 51 | "bitsPerSample": 16, 52 | "numChannels": 1, 53 | "sampleRate": 24000 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /firmware/stackchan/speeches/tts-elevenlabs.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-const */ 2 | import AudioOut from 'pins/audioout' 3 | import ElevenLabsStreamer from 'elevenlabsstreamer' 4 | import calculatePower from 'calculate-power' 5 | 6 | /* global trace, SharedArrayBuffer */ 7 | 8 | export type TTSProperty = { 9 | onPlayed: (number) => void 10 | onDone: () => void 11 | token: string 12 | model?: string 13 | } 14 | 15 | export class TTS { 16 | audio: AudioOut 17 | onPlayed: (number) => void 18 | onDone: () => void 19 | token: string 20 | model: string 21 | streaming: boolean 22 | constructor(props: TTSProperty) { 23 | this.onPlayed = props.onPlayed 24 | this.onDone = props.onDone 25 | this.audio = new AudioOut({ streams: 1, bitsPerSample: 16, sampleRate: 44100 }) 26 | this.token = props.token 27 | this.model = props.model ?? 'eleven_monolingual_v1' 28 | } 29 | async stream(text: string): Promise { 30 | if (this.streaming) { 31 | throw new Error('already playing') 32 | } 33 | this.streaming = true 34 | 35 | const { onPlayed, onDone, audio } = this 36 | return new Promise((resolve, reject) => { 37 | let streamer = new ElevenLabsStreamer({ 38 | key: this.token, 39 | voice: 'AZnzlk1XvdvUeBnXmlld', 40 | model: this.model, 41 | latency: 2, 42 | text, 43 | audio: { 44 | out: audio, 45 | stream: 0, 46 | }, 47 | onPlayed(buffer) { 48 | const power = calculatePower(buffer) 49 | onPlayed?.(power) 50 | }, 51 | onReady(state) { 52 | trace(`Ready: ${state}\n`) 53 | if (state) { 54 | audio.start() 55 | } else { 56 | audio.stop() 57 | } 58 | }, 59 | onError: (e) => { 60 | trace('ERROR: ', e, '\n') 61 | this.streaming = false 62 | reject(e) 63 | }, 64 | onDone: () => { 65 | trace('DONE\n') 66 | this.streaming = false 67 | streamer?.close() 68 | onDone?.() 69 | resolve() 70 | }, 71 | }) 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /firmware/stackchan/speeches/tts-local.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-const */ 2 | import AudioOut from 'pins/audioout' 3 | import ResourceStreamer from 'resourcestreamer' 4 | import calculatePower from 'calculate-power' 5 | 6 | /* global trace, SharedArrayBuffer */ 7 | 8 | export type TTSProperty = 9 | | { 10 | onPlayed: (number) => void 11 | onDone: () => void 12 | sampleRate?: number 13 | } 14 | | { 15 | onPlayed: (number) => void 16 | onDone: () => void 17 | host: string 18 | port: number 19 | sampleRate?: number 20 | } 21 | 22 | export class TTS { 23 | streamer?: ResourceStreamer 24 | audio?: AudioOut 25 | onPlayed: (number) => void 26 | onDone: () => void 27 | constructor(props: TTSProperty) { 28 | this.onPlayed = props.onPlayed 29 | this.onDone = props.onDone 30 | this.audio = new AudioOut({ streams: 1, sampleRate: props.sampleRate ?? 11025 }) 31 | } 32 | async stream(key: string): Promise { 33 | return new Promise((resolve, reject) => { 34 | if (this.streamer != null) { 35 | reject(new Error('already playing')) 36 | return 37 | } 38 | const sampleRate = this.audio.sampleRate ?? 11025 39 | this.streamer = new ResourceStreamer({ 40 | path: `${key}.maud`, 41 | audio: { 42 | out: this.audio, 43 | stream: 0, 44 | sampleRate, 45 | }, 46 | onReady(this: ResourceStreamer, state) { 47 | trace(`Ready: ${state}\n`) 48 | if (state) { 49 | this.audio.start() 50 | } else { 51 | this.audio.stop() 52 | } 53 | }, 54 | onPlayed: (buffer) => { 55 | const power = calculatePower(buffer) 56 | this.onPlayed?.(power) 57 | }, 58 | onError: (e) => { 59 | trace('ERROR: ', e, '\n') 60 | this.streamer = undefined 61 | reject(new Error('unknown error occured')) 62 | }, 63 | onDone: () => { 64 | trace('DONE\n') 65 | this.streamer?.close() 66 | this.streamer = undefined 67 | this.onDone?.() 68 | resolve() 69 | }, 70 | }) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /firmware/stackchan/speeches/tts-remote.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-const */ 2 | import AudioOut from 'pins/audioout' 3 | import WavStreamer from 'wavstreamer' 4 | import calculatePower from 'calculate-power' 5 | 6 | /* global trace, SharedArrayBuffer */ 7 | 8 | declare const device: any 9 | 10 | export type TTSProperty = { 11 | onPlayed: (number) => void 12 | onDone: () => void 13 | host: string 14 | port: number 15 | sampleRate?: number 16 | } 17 | let streamer 18 | 19 | export class TTS { 20 | streamer?: WavStreamer 21 | audio?: AudioOut 22 | onPlayed: (number) => void 23 | onDone: () => void 24 | host: string 25 | port: number 26 | constructor(props: TTSProperty) { 27 | this.onPlayed = props.onPlayed 28 | this.onDone = props.onDone 29 | this.audio = new AudioOut({ streams: 1, sampleRate: props.sampleRate ?? 24000 }) 30 | this.host = props.host 31 | this.port = props.port 32 | } 33 | async stream(key: string): Promise { 34 | const { onPlayed, onDone, audio } = this 35 | return new Promise((resolve, reject) => { 36 | if (streamer != null) { 37 | reject(new Error('already playing')) 38 | return 39 | } 40 | streamer = new WavStreamer({ 41 | http: device.network.http, 42 | host: this.host, 43 | path: key, 44 | port: this.port, 45 | bufferDuration: 600, 46 | audio: { 47 | out: audio, 48 | stream: 0, 49 | }, 50 | onPlayed(buffer) { 51 | const power = calculatePower(buffer) 52 | onPlayed?.(power) 53 | }, 54 | onReady(state) { 55 | trace(`Ready: ${state}\n`) 56 | if (state) { 57 | audio.start() 58 | } else { 59 | audio.stop() 60 | } 61 | }, 62 | onError(e) { 63 | trace('ERROR: ', e, '\n') 64 | streamer = undefined 65 | reject(new Error('unknown error occured')) 66 | }, 67 | onDone() { 68 | trace('DONE\n') 69 | streamer?.close() 70 | streamer = undefined 71 | onDone?.() 72 | resolve() 73 | }, 74 | }) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /firmware/stackchan/speeches/tts-voicevox.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-const */ 2 | import AudioOut from 'pins/audioout' 3 | import WavStreamer from 'wavstreamer' 4 | import calculatePower from 'calculate-power' 5 | import HTTPClient from 'embedded:network/http/client' 6 | import { File } from 'file' 7 | import config from 'mc/config' 8 | 9 | const QUERY_PATH = config.file.root + 'query.json' 10 | 11 | /* global trace, SharedArrayBuffer */ 12 | 13 | declare const device: any 14 | 15 | export type TTSProperty = { 16 | onPlayed: (number) => void 17 | onDone: () => void 18 | host: string 19 | port: number 20 | sampleRate: number 21 | speakerId: number 22 | } 23 | 24 | export class TTS { 25 | audio: AudioOut 26 | onPlayed: (number) => void 27 | onDone: () => void 28 | // TODO: Add type definition for HTTPClient 29 | client: HTTPClient 30 | host: string 31 | port: number 32 | streaming: boolean 33 | file: File 34 | speakerId: number 35 | constructor(props: TTSProperty) { 36 | this.onPlayed = props.onPlayed 37 | this.onDone = props.onDone 38 | this.audio = new AudioOut({ streams: 1, bitsPerSample: 16, sampleRate: props.sampleRate ?? 11025 }) 39 | this.speakerId = props.speakerId ?? 1 40 | this.host = props.host 41 | this.port = props.port 42 | } 43 | async getQuery(text: string, speakerId = 1): Promise { 44 | return new Promise((resolve, reject) => { 45 | File.delete(QUERY_PATH) 46 | const file = new File(QUERY_PATH, true) 47 | const sampleRate = this.audio?.sampleRate ?? 11025 48 | const client = new device.network.http.io({ 49 | ...device.network.http, 50 | host: this.host, 51 | port: this.port, 52 | }) 53 | client.request({ 54 | method: 'POST', 55 | path: encodeURI(`/audio_query?text=${text}&speaker=${speakerId}`), 56 | headers: new Map([['Content-Type', 'application/x-www-form-urlencoded']]), 57 | onHeaders(status) { 58 | if (status !== 200) { 59 | reject(`server returned ${status}`) 60 | } 61 | }, 62 | onReadable(count) { 63 | file.write(this.read(count)) 64 | // trace(`${count} bytes written. position: ${file.position}\n`) 65 | }, 66 | onDone() { 67 | if (sampleRate !== 24000) { 68 | file.position = file.length - 1 69 | file.write(`, "outputSamplingRate": ${sampleRate}}`) 70 | } 71 | file.close() 72 | client.close() 73 | resolve() 74 | }, 75 | }) 76 | }) 77 | } 78 | async stream(key: string): Promise { 79 | if (this.streaming) { 80 | throw new Error('already playing') 81 | } 82 | this.streaming = true 83 | 84 | const host = this.host 85 | const port = this.port 86 | const speakerId = this.speakerId 87 | await this.getQuery(key, speakerId) 88 | const { onPlayed, onDone, audio } = this 89 | const file = new File(QUERY_PATH) 90 | trace(`file opened. length: ${file.length}, position: ${file.position}`) 91 | return new Promise((resolve, reject) => { 92 | let streamer = new WavStreamer({ 93 | http: device.network.http, 94 | host, 95 | port, 96 | path: encodeURI(`/synthesis?speaker=${speakerId}`), 97 | audio: { 98 | out: audio, 99 | stream: 0, 100 | }, 101 | bufferDuration: 600, 102 | request: { 103 | method: 'POST', 104 | headers: new Map([ 105 | ['content-type', 'application/json'], 106 | ['content-length', `${file.length}`], 107 | ]), 108 | onWritable(count) { 109 | this.write(file.read(ArrayBuffer, count)) 110 | }, 111 | }, 112 | onPlayed(buffer) { 113 | const power = calculatePower(buffer) 114 | onPlayed?.(power) 115 | }, 116 | onReady(state) { 117 | trace(`Ready: ${state}\n`) 118 | if (state) { 119 | audio.start() 120 | } else { 121 | audio.stop() 122 | } 123 | }, 124 | onError: (e) => { 125 | file.close() 126 | trace('ERROR: ', e, '\n') 127 | this.streaming = false 128 | reject(e) 129 | }, 130 | onDone: () => { 131 | file.close() 132 | trace('DONE\n') 133 | this.streaming = false 134 | streamer?.close() 135 | onDone?.() 136 | resolve() 137 | }, 138 | }) 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /firmware/stackchan/touch.ts: -------------------------------------------------------------------------------- 1 | import config from 'mc/config' 2 | import Timer from 'timer' 3 | import Time from 'time' 4 | 5 | export default class Touch { 6 | onTouchBegan: (x: number, y: number, ticks: number) => void 7 | onTouchMoved: (x: number, y: number, ticks: number) => void 8 | onTouchEnded: (x: number, y: number, ticks: number) => void 9 | 10 | constructor() { 11 | let touch = new config.Touch() 12 | touch.points = [{}] 13 | 14 | Timer.repeat(() => { 15 | const points = touch.points 16 | touch.read(points) 17 | const point = points[0] 18 | switch (point.state) { 19 | case 0: 20 | case 3: 21 | if (point.down) { 22 | delete point.down 23 | this.onTouchEnded?.(point.x, point.y, Time.ticks) 24 | delete point.x 25 | delete point.y 26 | } 27 | break 28 | case 1: 29 | case 2: 30 | if (!point.down) { 31 | point.down = true 32 | this.onTouchBegan?.(point.x, point.y, Time.ticks) 33 | } else this.onTouchMoved?.(point.x, point.y, Time.ticks) 34 | break 35 | } 36 | }, 15) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /firmware/stackchan/utilities/consts.ts: -------------------------------------------------------------------------------- 1 | export const DOMAIN = { 2 | wifi: 'wifi', 3 | driver: 'driver', 4 | renderer: 'renderer', 5 | tts: 'tts', 6 | ai: 'ai', 7 | } as const 8 | 9 | export const PREF_KEYS: readonly [keyof typeof DOMAIN, string, StringConstructor | NumberConstructor][] = Object.freeze( 10 | [ 11 | [DOMAIN.wifi, 'ssid', String], 12 | [DOMAIN.wifi, 'password', String], 13 | [DOMAIN.renderer, 'type', String], 14 | [DOMAIN.driver, 'type', String], 15 | [DOMAIN.driver, 'baudrate', Number], 16 | [DOMAIN.driver, 'offsetPan', Number], 17 | [DOMAIN.driver, 'offsetTilt', Number], 18 | [DOMAIN.tts, 'type', String], 19 | [DOMAIN.tts, 'host', String], 20 | [DOMAIN.tts, 'port', Number], 21 | [DOMAIN.tts, 'token', String], 22 | [DOMAIN.ai, 'token', String], 23 | [DOMAIN.ai, 'context', String], 24 | ], 25 | true 26 | ) 27 | 28 | export const DEFAULT_FONT = 'OpenSans-Regular-24.bf4' 29 | -------------------------------------------------------------------------------- /firmware/stackchan/utilities/manifest_utility.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODULES)/files/preference/manifest.json", 5 | "$(MODULES)/base/structuredClone/manifest.json", 6 | "$(MODULES)/base/deepEqual/manifest.json", 7 | "$(MODDABLE)/examples/manifest_typings.json" 8 | ], 9 | "modules": { 10 | "*": [ 11 | "./*" 12 | ] 13 | }, 14 | "preload": [ 15 | "consts", 16 | "stackchan-util" 17 | ] 18 | } -------------------------------------------------------------------------------- /firmware/tests/drivers/dynamixel/main.ts: -------------------------------------------------------------------------------- 1 | import { DynamixelDriver } from 'dynamixel-driver' 2 | 3 | const driver = new DynamixelDriver({ 4 | panId: 1, 5 | tiltId: 2, 6 | baud: 1_000_000, 7 | }) 8 | 9 | driver.applyRotation({ 10 | r: 0, 11 | p: 0, 12 | y: 0, 13 | }) 14 | 15 | 16 | -------------------------------------------------------------------------------- /firmware/tests/drivers/dynamixel/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_typings.json", 5 | "../../../stackchan/drivers/manifest_driver.json" 6 | ], 7 | "modules": { 8 | "*": [ 9 | "./main" 10 | ] 11 | }, 12 | "platforms":{ 13 | "esp32": { 14 | "config" :{ 15 | "startupSound": false 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /firmware/tests/drivers/serial/main.ts: -------------------------------------------------------------------------------- 1 | import Serial from 'embedded:io/serial' 2 | import Timer from 'timer' 3 | import Digital from "pins/digital"; 4 | 5 | let buffer = new ArrayBuffer(1); 6 | let chars = new Uint8Array(buffer); 7 | let blink =1; 8 | let led = new Digital(17,Digital.Output); 9 | 10 | chars[0] = 0x55; 11 | 12 | 13 | let serial = new Serial( 14 | { 15 | transmit: 7, 16 | receive: 6, 17 | baud: 1000000, 18 | port: 1 19 | } 20 | ); 21 | 22 | 23 | Timer.repeat(id => { 24 | serial.write(buffer); 25 | blink = blink ^1; 26 | led.write(blink); 27 | trace("send\n\r"); 28 | }, 1); 29 | 30 | -------------------------------------------------------------------------------- /firmware/tests/drivers/serial/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_typings.json" 5 | ], 6 | "modules": { 7 | "*": [ 8 | "./main" 9 | ] 10 | }, 11 | "platforms":{ 12 | "esp32": { 13 | "config" :{ 14 | "startupSound": false 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /firmware/tests/renderers/render-balloon/main.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from 'dog-face' 2 | import { defaultFaceContext } from 'renderer-base' 3 | import { createBalloonDecorator } from 'decorator' 4 | import Poco from 'commodetto/Poco' 5 | import Timer from 'timer' 6 | import Resource from 'Resource' 7 | import parseBMF from 'commodetto/parseBMF' 8 | import structuredClone from 'structuredClone' 9 | 10 | const font = parseBMF(new Resource('NotoSansJP-Regular-24.bf4')) 11 | let poco = new Poco(screen, { rotation: 90, displayListLength: 1024 }) 12 | const renderer = new Renderer({ poco }) 13 | type Color = [number, number, number] 14 | const black: Color = [0, 0, 0] 15 | const brown: Color = [255, 146, 0] 16 | 17 | const balloon = createBalloonDecorator({ 18 | bottom: 5, 19 | right: 10, 20 | width: 120, 21 | height: font.height, 22 | font, 23 | text: 'じゅげむじゅげむごこうのすりきれかいじゃりすいぎょのすいぎょうまつふうらいまつ...' 24 | }) 25 | renderer.addDecorator(balloon) 26 | 27 | const INTERVAL = 1000 / 30 28 | const context = structuredClone(defaultFaceContext) 29 | context.theme.primary = black 30 | context.theme.secondary = brown 31 | let count = 0 32 | Timer.repeat(() => { 33 | count = (count + 30) % 360 34 | context.mouth.open = Math.sin((Math.PI * 2 * count) / 360) / 2 + 0.5 35 | 36 | renderer.update(INTERVAL, context) 37 | }, INTERVAL) 38 | -------------------------------------------------------------------------------- /firmware/tests/renderers/render-balloon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_typings.json", 5 | "../../../stackchan/renderers/manifest_renderer.json" 6 | ], 7 | "modules": { 8 | "*": [ 9 | "./main" 10 | ] 11 | }, 12 | "resources": { 13 | "*-mask": [ 14 | { 15 | "source": "$(MODDABLE)/examples/assets/scalablefonts/NotoSans/NotoSansJP-Regular", 16 | "size": 24, 17 | "blocks": [ 18 | "Hiragana", 19 | "Katakana", 20 | "Basic Latin" 21 | ], 22 | "characters": "えっ今からでも入れる保険があるんですか!?" 23 | } 24 | ] 25 | }, 26 | "config": { 27 | "rotation": 90 28 | } 29 | } -------------------------------------------------------------------------------- /firmware/tests/renderers/render-face/main.ts: -------------------------------------------------------------------------------- 1 | import {Renderer} from 'simple-face' 2 | import Timer from 'timer' 3 | 4 | const INTERVAL = 1000 / 30 5 | const renderer = new Renderer({}) 6 | Timer.repeat(() => { 7 | renderer.update(INTERVAL) 8 | }, INTERVAL) 9 | -------------------------------------------------------------------------------- /firmware/tests/renderers/render-face/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_typings.json", 5 | "../../../stackchan/renderers/manifest_renderer.json" 6 | ], 7 | "modules": { 8 | "*": [ 9 | "./main" 10 | ] 11 | }, 12 | "config": { 13 | "rotation": 90 14 | } 15 | } -------------------------------------------------------------------------------- /firmware/tests/services/network-service/main.ts: -------------------------------------------------------------------------------- 1 | import { NetworkService } from 'network-service' 2 | 3 | const service = new NetworkService({ 4 | ssid: 'myssid', 5 | password: 'mypassword', 6 | }) 7 | 8 | service.connect( 9 | () => { 10 | trace('connected\n') 11 | }, 12 | (message) => { 13 | trace(`error: ${message}\n`) 14 | } 15 | ) 16 | -------------------------------------------------------------------------------- /firmware/tests/services/network-service/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_typings.json", 5 | "../../../stackchan/services/manifest_service.json" 6 | ], 7 | "modules": { 8 | "*": [ 9 | "./main" 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /firmware/tests/speeches/tts-elevenlabs/main.ts: -------------------------------------------------------------------------------- 1 | import { TTS } from 'tts-elevenlabs' 2 | import Timer from 'timer' 3 | 4 | const token = 'YOUR_API_KEY_HERE' 5 | const tts = new TTS({ 6 | token, 7 | onPlayed: (num) => { 8 | trace(`played ${num}\n`) 9 | }, 10 | onDone: () => { 11 | trace('done\n') 12 | } 13 | }) 14 | 15 | async function main() { 16 | while (true) { 17 | await tts.stream('Hello. I am Stack-chan. Nice to meet you.') 18 | Timer.delay(2000) 19 | } 20 | } 21 | 22 | main() 23 | -------------------------------------------------------------------------------- /firmware/tests/speeches/tts-elevenlabs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_typings.json", 5 | "../../../stackchan/speeches/manifest_speech.json" 6 | ], 7 | "modules": { 8 | "*": [ 9 | "./main" 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /firmware/tests/speeches/tts-local/main.ts: -------------------------------------------------------------------------------- 1 | import { TTS } from 'tts-local' 2 | import Timer from 'timer' 3 | 4 | const tts = new TTS({ 5 | onPlayed: (num) => { 6 | trace(`played ${num}\n`) 7 | }, 8 | onDone: () => { 9 | trace('done\n') 10 | } 11 | }) 12 | 13 | while (true) { 14 | await tts.stream('wilhelm-scream') 15 | Timer.delay(2000) 16 | } 17 | -------------------------------------------------------------------------------- /firmware/tests/speeches/tts-local/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "$(MODDABLE)/examples/manifest_base.json", 4 | "$(MODDABLE)/examples/manifest_typings.json", 5 | "../../../stackchan/speeches/manifest_speech.json" 6 | ], 7 | "resources": { 8 | "*": "$(MODDABLE)/examples/assets/sounds/*" 9 | }, 10 | "modules": { 11 | "*": [ 12 | "./main" 13 | ] 14 | }, 15 | "defines": { 16 | "main": { 17 | "async": 1 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /firmware/typings/btutils.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @note type definitions of `btutils` exists in moddable/typings/ble.d.ts but cannot import 3 | */ 4 | declare module 'btutils' { 5 | export class Bytes extends ArrayBuffer { 6 | constructor(bytes: string | ArrayBufferLike, littleEndian?: boolean) 7 | equals(bytes: Bytes): boolean 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /firmware/typings/elevenlabsstreamer.d.ts: -------------------------------------------------------------------------------- 1 | declare module "elevenlabsstreamer" { 2 | import type AudioOut from "pins/audioout" 3 | type ElevenLabsStreamerOptions = { 4 | key: string, 5 | voice?: string, 6 | latency?: number, 7 | text: string, 8 | model?: string, 9 | audio: { 10 | out: AudioOut, 11 | sampleRate?: number, 12 | stream: number, 13 | }, 14 | onPlayed?: (buffer: ArrayBuffer) => void 15 | onReady?: (state: boolean) => void 16 | onError?: (message: string) => void 17 | onDone?: () => void 18 | } 19 | export default class ElevenLabsStreamer { 20 | constructor(options: ElevenLabsStreamerOptions); 21 | close(): void; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /firmware/typings/fetch.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'fetch' { 2 | export function fetch(...args: any): any 3 | export class Headers { 4 | constructor(params: Array<[string, string]>) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /firmware/typings/piu/All.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'piu/All' { 2 | function hsl(h: number, s: number, l: number): any 3 | } -------------------------------------------------------------------------------- /firmware/typings/resourcestreamer.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'resourcestreamer' { 2 | import type AudioOut from 'pins/audioout' 3 | type ResourceStreamerOptions = { 4 | path: string 5 | audio: { 6 | out: AudioOut 7 | sampleRate: number 8 | stream: number 9 | } 10 | onPlayed?: (buffer: ArrayBuffer) => void 11 | onReady?: (state: boolean) => void 12 | onError?: (message: string) => void 13 | onDone?: () => void 14 | } 15 | export default class ResourceStreamer { 16 | audio: AudioOut 17 | constructor(options: ResourceStreamerOptions) 18 | close(): void 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /firmware/typings/uartserver.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'uartserver' { 2 | class UARTServer { 3 | deviceName: string 4 | notifyValue(characteristic: string, data: ArrayBuffer): void 5 | onConnected(): void 6 | onDisconnected(): void 7 | onRX(data: ArrayBuffer): void 8 | startAdvertising(params: unknown): void 9 | } 10 | 11 | const SERVICE_UUID: string 12 | 13 | export { UARTServer, SERVICE_UUID } 14 | } 15 | -------------------------------------------------------------------------------- /firmware/typings/url.d.ts: -------------------------------------------------------------------------------- 1 | declare module "url" { 2 | export class URLSearchParams { 3 | constructor(params: Array<[string, string]>) 4 | } 5 | } -------------------------------------------------------------------------------- /firmware/typings/wavstreamer.d.ts: -------------------------------------------------------------------------------- 1 | declare module "wavstreamer" { 2 | import type AudioOut from "pins/audioout" 3 | import HTTPClient from "embedded:network/http/client"; 4 | type WavStreamerOptions = { 5 | http: typeof HTTPClient.constructor 6 | host: string, 7 | port: number, 8 | path: string, 9 | audio: { 10 | out: AudioOut, 11 | sampleRate?: number, 12 | stream: number, 13 | }, 14 | bufferDuration?: number, 15 | request?: any, 16 | waveHeaderBytes?: number, 17 | onPlayed?: (buffer: ArrayBuffer) => void 18 | onReady?: (state: boolean) => void 19 | onError?: (message: string) => void 20 | onDone?: () => void 21 | } 22 | export default class WavStreamer { 23 | constructor(options: WavStreamerOptions); 24 | close(): void; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /firmware/workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "../../../root/Projects/moddable" 8 | } 9 | ], 10 | "settings": {} 11 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test_stack_chan", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /web/flash/flash.css: -------------------------------------------------------------------------------- 1 | .app { 2 | font-size: 1.8em; 3 | justify-content:center; 4 | max-width: 300px; 5 | gap: .5em; 6 | margin: auto; 7 | } 8 | .select-target { 9 | max-height: 2.5em; 10 | width: 100%; 11 | } 12 | .button-flash-container { 13 | width: 100%; 14 | } 15 | .button-flash { 16 | width: 100%; 17 | } -------------------------------------------------------------------------------- /web/flash/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Flash firmware 9 | 10 | 11 | 12 | 22 | 23 | 24 | 25 |
26 | 32 | 33 | 36 | Ah snap, your browser doesn't work! 37 | Ah snap, you are not allowed to use this on HTTP! 38 | 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /web/flash/manifest_esp32_m5stack fire.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stack-chan", 3 | "version": "1.0.0", 4 | "builds": [ 5 | { 6 | "chipFamily": "ESP32", 7 | "parts": [ 8 | { 9 | "path": "tech.moddable.stackchan/com.m5stack.fire/bootloader.bin", 10 | "offset": 4096 11 | }, 12 | { 13 | "path": "tech.moddable.stackchan/com.m5stack.fire/partition-table.bin", 14 | "offset": 32768 15 | }, 16 | { 17 | "path": "tech.moddable.stackchan/com.m5stack.fire/xs_esp32.bin", 18 | "offset": 65536 19 | } 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /web/flash/manifest_esp32_m5stack.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stack-chan", 3 | "version": "1.0.0", 4 | "builds": [ 5 | { 6 | "chipFamily": "ESP32", 7 | "parts": [ 8 | { 9 | "path": "tech.moddable.stackchan/com.m5stack/bootloader.bin", 10 | "offset": 4096 11 | }, 12 | { 13 | "path": "tech.moddable.stackchan/com.m5stack/partition-table.bin", 14 | "offset": 32768 15 | }, 16 | { 17 | "path": "tech.moddable.stackchan/com.m5stack/xs_esp32.bin", 18 | "offset": 65536 19 | } 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /web/flash/manifest_esp32_m5stack_core2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stack-chan", 3 | "version": "1.0.0", 4 | "builds": [ 5 | { 6 | "chipFamily": "ESP32", 7 | "parts": [ 8 | { 9 | "path": "tech.moddable.stackchan/com.m5stack.core2/bootloader.bin", 10 | "offset": 4096 11 | }, 12 | { 13 | "path": "tech.moddable.stackchan/com.m5stack.core2/partition-table.bin", 14 | "offset": 32768 15 | }, 16 | { 17 | "path": "tech.moddable.stackchan/com.m5stack.core2/xs_esp32.bin", 18 | "offset": 65536 19 | } 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /web/flash/manifest_esp32_m5stack_cores3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Stack-chan", 3 | "version": "2.1.1", 4 | "builds": [ 5 | { 6 | "chipFamily": "ESP32-S3", 7 | "parts": [ 8 | { 9 | "path": "tech.moddable.stackchan/com.m5stack.cores3/bootloader.bin", 10 | "offset": 0 11 | }, 12 | { 13 | "path": "tech.moddable.stackchan/com.m5stack.cores3/partition-table.bin", 14 | "offset": 32768 15 | }, 16 | { 17 | "path": "tech.moddable.stackchan/com.m5stack.cores3/xs_esp32.bin", 18 | "offset": 65536 19 | } 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /web/global.css: -------------------------------------------------------------------------------- 1 | html,body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | body { 7 | font-family: 'Roboto', sans-serif; 8 | color: #212121; 9 | background-color: #f5f5f5; 10 | } 11 | 12 | .app { 13 | width: 100%; 14 | height: 100%; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: space-around; 19 | } 20 | 21 | .card { 22 | position: relative; 23 | background-color: white; 24 | padding: 1.4em; 25 | border-radius: 8px; 26 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); 27 | width: 100%; 28 | max-width: 600px; 29 | } 30 | 31 | button { 32 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26); 33 | } 34 | 35 | .card button { 36 | box-shadow: none; 37 | } 38 | 39 | .card h1,h2,h3,h4,h5,h6 { 40 | margin: 0.3em 0; 41 | } 42 | 43 | .form { 44 | display: flex; 45 | flex-direction: column; 46 | } 47 | 48 | .form-group { 49 | display: flex; 50 | align-items: center; 51 | justify-content: space-between; 52 | margin-bottom: 1em; 53 | } 54 | 55 | .form-group label { 56 | flex: 1; 57 | text-align: right; 58 | margin-right: 1em; 59 | } 60 | 61 | .form-group input,select,textarea { 62 | flex: 2; 63 | padding: 0.5em; 64 | } 65 | 66 | .card input,select,textarea { 67 | border: 1px solid #bdbdbd; 68 | border-radius: 4px; 69 | } 70 | 71 | button { 72 | background-color: #2196F3; 73 | color: white; 74 | padding: 0.7em 1em; 75 | border: none; 76 | border-radius: 4px; 77 | cursor: pointer; 78 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1); 79 | } 80 | 81 | button:hover { 82 | background-color: #1976D2; 83 | } 84 | 85 | .toast { 86 | position: absolute; 87 | font-size: 1.2em; 88 | padding: 0.4em; 89 | top: 0; 90 | right: 0; 91 | max-width: 400px; 92 | background-color: #388E3C; 93 | color: white; 94 | visibility: hidden; 95 | transform: translateY(-100px); 96 | } 97 | 98 | .toast.visible { 99 | visibility: visible; 100 | animation: toast 3s ease 1 normal; 101 | } 102 | 103 | @keyframes toast { 104 | 33% { 105 | transform: translateY(0); 106 | } 107 | 66% { 108 | transform: translateY(0); 109 | } 110 | } -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Stack-chan dev server 9 | 10 | 11 | 12 |

Stack-chan development page

13 |
    14 |
  • Flash: Flash Stack-chan firmware
  • 15 |
  • Preference: Set Stack-chan's preferences via BLE
  • 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stackchan-web", 3 | "version": "0.1.0", 4 | "description": "Stack-chan development server", 5 | "scripts": { 6 | "dev": "live-server --verbose" 7 | }, 8 | "keywords": [], 9 | "author": "Shinya Ishikawa", 10 | "devDependencies": { 11 | "live-server": "^1.2.2", 12 | "prettier": "^3.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /web/preference/ble-client.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple BLE UART Client 3 | */ 4 | function isBluetoothAvailable() { 5 | return navigator.bluetooth != null 6 | } 7 | 8 | const SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e' 9 | const RX_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e' 10 | const TX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e' 11 | 12 | class SimpleBLEClient { 13 | #deviceName 14 | #encoder = new TextEncoder() 15 | #decoder = new TextDecoder() 16 | 17 | #device 18 | #onCharacteristicValueChanged 19 | #tx_characteristic 20 | #rx_characteristic 21 | constructor({ deviceName, onCharacteristicValueChanged }) { 22 | this.#deviceName = deviceName 23 | this.#onCharacteristicValueChanged = onCharacteristicValueChanged 24 | } 25 | 26 | async connect() { 27 | if (!isBluetoothAvailable()) { 28 | throw 'Bluetooth not available' 29 | } 30 | if (this.#device != null) { 31 | await this.disconnect() 32 | } 33 | const device = (this.#device = await navigator.bluetooth.requestDevice({ 34 | acceptAllDevices: false, 35 | filters: [ 36 | { 37 | name: this.#deviceName, 38 | }, 39 | { 40 | services: [SERVICE_UUID], 41 | }, 42 | ], 43 | })) 44 | console.log('device found') 45 | if (device.gatt == null) { 46 | throw 'The device has no gatt property' 47 | } 48 | device.addEventListener('gattserverdisconnected', () => { 49 | console.warn('Disconnected') 50 | this.onDisconnected?.() 51 | }) 52 | 53 | const server = await device.gatt.connect() 54 | if (server == null) { 55 | throw 'Gatt connection failed' 56 | } 57 | 58 | const service = await server.getPrimaryService(SERVICE_UUID) 59 | this.#rx_characteristic = await service.getCharacteristic(RX_UUID) 60 | this.#tx_characteristic = await service.getCharacteristic(TX_UUID) 61 | this.#tx_characteristic.addEventListener('characteristicvaluechanged', (event) => { 62 | const value = event.target.value 63 | const str = this.#decoder.decode(value) 64 | const obj = JSON.parse(str) 65 | this.#onCharacteristicValueChanged?.(obj) 66 | }) 67 | await this.#tx_characteristic.startNotifications() 68 | } 69 | 70 | isConnected() { 71 | return this.#device?.gatt?.connected ?? false 72 | } 73 | 74 | async disconnect() { 75 | this.#device?.gatt?.disconnect() 76 | } 77 | 78 | async send(obj) { 79 | const buf = this.#encoder.encode(JSON.stringify(obj)) 80 | const chunkSize = 128 // Choose an appropriate chunk size 81 | 82 | for (let i = 0; i < buf.length; i += chunkSize) { 83 | const chunk = buf.slice(i, i + chunkSize) 84 | await this.#rx_characteristic?.writeValue(chunk).catch((reason) => { 85 | console.warn(`write failed: ${reason}`) 86 | }) 87 | } 88 | } 89 | } 90 | 91 | export default SimpleBLEClient 92 | -------------------------------------------------------------------------------- /web/preference/preference.css: -------------------------------------------------------------------------------- 1 | .accordion { 2 | display: none; 3 | } 4 | 5 | .accordion.active { 6 | display: block; 7 | } 8 | 9 | .ble-disconnect-button { 10 | position: absolute; 11 | right: 0.5em; 12 | top: 0.5em; 13 | font-size: 1.2em; 14 | color: #bdbdbd; 15 | cursor: pointer; 16 | } --------------------------------------------------------------------------------