diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 0e8c7d8..218391e 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,9 +9,11 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':capacitor-community-bluetooth-le') implementation project(':capacitor-app') implementation project(':capacitor-dialog') implementation project(':capacitor-screen-orientation') + implementation project(':capacitor-tcp-socket') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 72947d6..2a8e34f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,7 @@ - + + + - - - + + - + + + - - - - + + + + + - + - + + + + + - - - - + \ No newline at end of file diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index b20885d..15b1d84 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -2,6 +2,9 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') +include ':capacitor-community-bluetooth-le' +project(':capacitor-community-bluetooth-le').projectDir = new File('../node_modules/@capacitor-community/bluetooth-le/android') + include ':capacitor-app' project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') @@ -10,3 +13,6 @@ project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/d include ':capacitor-screen-orientation' project(':capacitor-screen-orientation').projectDir = new File('../node_modules/@capacitor/screen-orientation/android') + +include ':capacitor-tcp-socket' +project(':capacitor-tcp-socket').projectDir = new File('../node_modules/capacitor-tcp-socket/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index 42f972e..34ab3df 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -11,9 +11,11 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorCommunityBluetoothLe', :path => '../../node_modules/@capacitor-community/bluetooth-le' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog' pod 'CapacitorScreenOrientation', :path => '../../node_modules/@capacitor/screen-orientation' + pod 'CapacitorTcpSocket', :path => '../../node_modules/capacitor-tcp-socket' pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins' end diff --git a/package-lock.json b/package-lock.json index fb049f3..b689d79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "bitdoglab", "version": "0.0.0", "dependencies": { + "@capacitor-community/bluetooth-le": "^7.1.1", "@capacitor/android": "^7.2.0", "@capacitor/app": "^7.0.1", "@capacitor/assets": "^3.0.5", @@ -32,6 +33,7 @@ "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", + "capacitor-tcp-socket": "^7.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cordova-plugin-bluetooth-serial": "^0.4.7", @@ -261,6 +263,18 @@ "node": ">=14.21.3" } }, + "node_modules/@capacitor-community/bluetooth-le": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@capacitor-community/bluetooth-le/-/bluetooth-le-7.1.1.tgz", + "integrity": "sha512-Z5beNgHNQodKaWrjplYUHji3f15nU2/JCA+INzgybK6mBssq/WsHzBw8eBP/CXzAv1XZD6tXMZlLumzAXgZuhw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20" + }, + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/android": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-7.2.0.tgz", @@ -2560,9 +2574,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", - "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz", + "integrity": "sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==", "cpu": [ "arm" ], @@ -2574,9 +2588,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz", - "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.0.tgz", + "integrity": "sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==", "cpu": [ "arm64" ], @@ -2588,9 +2602,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz", - "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.0.tgz", + "integrity": "sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==", "cpu": [ "arm64" ], @@ -2602,9 +2616,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz", - "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.0.tgz", + "integrity": "sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==", "cpu": [ "x64" ], @@ -2615,10 +2629,38 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.0.tgz", + "integrity": "sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.0.tgz", + "integrity": "sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz", - "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.0.tgz", + "integrity": "sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==", "cpu": [ "arm" ], @@ -2630,9 +2672,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz", - "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.0.tgz", + "integrity": "sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==", "cpu": [ "arm" ], @@ -2644,9 +2686,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz", - "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.0.tgz", + "integrity": "sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==", "cpu": [ "arm64" ], @@ -2658,9 +2700,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz", - "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.0.tgz", + "integrity": "sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==", "cpu": [ "arm64" ], @@ -2671,10 +2713,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz", - "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==", + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.0.tgz", + "integrity": "sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.0.tgz", + "integrity": "sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==", "cpu": [ "ppc64" ], @@ -2686,9 +2742,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz", - "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.0.tgz", + "integrity": "sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.0.tgz", + "integrity": "sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==", "cpu": [ "riscv64" ], @@ -2700,9 +2770,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz", - "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.0.tgz", + "integrity": "sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==", "cpu": [ "s390x" ], @@ -2714,9 +2784,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", - "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.0.tgz", + "integrity": "sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==", "cpu": [ "x64" ], @@ -2728,9 +2798,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", - "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.0.tgz", + "integrity": "sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==", "cpu": [ "x64" ], @@ -2741,10 +2811,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.0.tgz", + "integrity": "sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz", - "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.0.tgz", + "integrity": "sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==", "cpu": [ "arm64" ], @@ -2756,9 +2840,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz", - "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.0.tgz", + "integrity": "sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==", "cpu": [ "ia32" ], @@ -2770,9 +2854,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz", - "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz", + "integrity": "sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==", "cpu": [ "x64" ], @@ -3242,9 +3326,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -3312,6 +3396,12 @@ "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", "license": "MIT" }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz", @@ -3697,9 +3787,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3839,6 +3929,15 @@ ], "license": "CC-BY-4.0" }, + "node_modules/capacitor-tcp-socket": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/capacitor-tcp-socket/-/capacitor-tcp-socket-7.1.0.tgz", + "integrity": "sha512-nuw5/AsSlc/IoR6rkhdoRls9a8Rbomk9F9cy+DSfOqTyT0a0Vt8IJ/RZ/L2t1BVYJceNt3ziLs3Ekr6SYwsb6w==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4392,9 +4491,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4596,9 +4695,9 @@ } }, "node_modules/del/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6407,9 +6506,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -6800,9 +6899,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -6819,9 +6918,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -7436,9 +7535,9 @@ } }, "node_modules/replace/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7771,13 +7870,13 @@ } }, "node_modules/rollup": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", - "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", + "version": "4.50.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz", + "integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -7787,22 +7886,27 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.2", - "@rollup/rollup-android-arm64": "4.21.2", - "@rollup/rollup-darwin-arm64": "4.21.2", - "@rollup/rollup-darwin-x64": "4.21.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", - "@rollup/rollup-linux-arm-musleabihf": "4.21.2", - "@rollup/rollup-linux-arm64-gnu": "4.21.2", - "@rollup/rollup-linux-arm64-musl": "4.21.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", - "@rollup/rollup-linux-riscv64-gnu": "4.21.2", - "@rollup/rollup-linux-s390x-gnu": "4.21.2", - "@rollup/rollup-linux-x64-gnu": "4.21.2", - "@rollup/rollup-linux-x64-musl": "4.21.2", - "@rollup/rollup-win32-arm64-msvc": "4.21.2", - "@rollup/rollup-win32-ia32-msvc": "4.21.2", - "@rollup/rollup-win32-x64-msvc": "4.21.2", + "@rollup/rollup-android-arm-eabi": "4.50.0", + "@rollup/rollup-android-arm64": "4.50.0", + "@rollup/rollup-darwin-arm64": "4.50.0", + "@rollup/rollup-darwin-x64": "4.50.0", + "@rollup/rollup-freebsd-arm64": "4.50.0", + "@rollup/rollup-freebsd-x64": "4.50.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.0", + "@rollup/rollup-linux-arm-musleabihf": "4.50.0", + "@rollup/rollup-linux-arm64-gnu": "4.50.0", + "@rollup/rollup-linux-arm64-musl": "4.50.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.0", + "@rollup/rollup-linux-ppc64-gnu": "4.50.0", + "@rollup/rollup-linux-riscv64-gnu": "4.50.0", + "@rollup/rollup-linux-riscv64-musl": "4.50.0", + "@rollup/rollup-linux-s390x-gnu": "4.50.0", + "@rollup/rollup-linux-x64-gnu": "4.50.0", + "@rollup/rollup-linux-x64-musl": "4.50.0", + "@rollup/rollup-openharmony-arm64": "4.50.0", + "@rollup/rollup-win32-arm64-msvc": "4.50.0", + "@rollup/rollup-win32-ia32-msvc": "4.50.0", + "@rollup/rollup-win32-x64-msvc": "4.50.0", "fsevents": "~2.3.2" } }, @@ -8078,9 +8182,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8557,9 +8661,9 @@ } }, "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "license": "MIT", "engines": { "node": ">=14.14" @@ -8885,14 +8989,14 @@ } }, "node_modules/vite": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", - "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.41", + "postcss": "^8.4.43", "rollup": "^4.20.0" }, "bin": { diff --git a/package.json b/package.json index 4674323..5a1e0aa 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "preview": "vite preview" }, "dependencies": { + "@capacitor-community/bluetooth-le": "^7.1.1", "@capacitor/android": "^7.2.0", "@capacitor/app": "^7.0.1", "@capacitor/assets": "^3.0.5", @@ -35,6 +36,7 @@ "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", + "capacitor-tcp-socket": "^7.1.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cordova-plugin-bluetooth-serial": "^0.4.7", diff --git a/protoboard/mainBLE.py b/protoboard/mainBLE.py new file mode 100644 index 0000000..3808a2d --- /dev/null +++ b/protoboard/mainBLE.py @@ -0,0 +1,419 @@ +import bluetooth +from micropython import const +import neopixel +import time +import math +import random +from machine import PWM, Pin,I2C, Timer, ADC +from ssd1306 import SSD1306_I2C + +# --- CONFIGURAÇÃO DOS COMPONENTES BITDOGLAB --- + +# global variables +SCREEN_WIDTH = 128 +SCREEN_HEIGHT = 64 + +SEGMENT_WIDTH = 8 +SEGMENT_PIXELS = int(SCREEN_HEIGHT/SEGMENT_WIDTH) +SEGMENTS_HIGH = int(SCREEN_HEIGHT/SEGMENT_WIDTH) +SEGMENTS_WIDE = int(SCREEN_WIDTH/SEGMENT_WIDTH) +VALID_RANGE = [[int(i /SEGMENTS_HIGH), i % SEGMENTS_HIGH] for i in range(SEGMENTS_WIDE * SEGMENTS_HIGH -1)] + +is_game_running = False + +# Configuração do OLED +i2c = I2C(1, sda=Pin(2), scl=Pin(3), freq=400000) +oled = SSD1306_I2C(SCREEN_WIDTH, SCREEN_HEIGHT, i2c) + +# Botão pressionável do joystick +joystick_button = Pin(22, Pin.IN, Pin.PULL_UP) + +# Número de LEDs na sua matriz 5x5 +NUM_LEDS = 25 + +# Inicializar a matriz de NeoPixels no GPIO7 +np = neopixel.NeoPixel(Pin(7), NUM_LEDS) + +# Definindo a matriz de LEDs +LED_MATRIX = [ + [24, 23, 22, 21, 20], + [15, 16, 17, 18, 19], + [14, 13, 12, 11, 10], + [5, 6, 7, 8, 9], + [4, 3, 2, 1, 0] +] + +# Inicializar ADC para os pinos VRx (GPIO26) e VRy (GPIO27) +adc_vrx = ADC(Pin(26)) +adc_vry = ADC(Pin(27)) + +def map_value(value, in_min, in_max, out_min, out_max): + return (value - in_min) * (out_max - out_min) // (in_max - in_min) + out_min + +def update_oled(lines): + oled.fill(0) + for i, line in enumerate(lines): + oled.text(line, 0, i * 8) + oled.show() + +# Configurando o LED RGB +led_r = PWM(Pin(12)) +led_g = PWM(Pin(13)) +led_b = PWM(Pin(11)) + +led_r.freq(1000) +led_g.freq(1000) +led_b.freq(1000) + +# Configuração do NeoPixel +NUM_LEDS = 25 +np = neopixel.NeoPixel(Pin(7), NUM_LEDS) + +# Configuração dos botões +button_a = Pin(5, Pin.IN, Pin.PULL_UP) +button_b = Pin(6, Pin.IN, Pin.PULL_UP) + +# Configuração do Buzzer +buzzer = PWM(Pin(21)) +buzzer2 = PWM(Pin(10)) + +# Essencial pro snake +game_timer = Timer() +player = None +food = None + +# --- CONSTANTES DE EVENTOS BLE --- +_IRQ_CENTRAL_CONNECT = const(1) # percebe conexão +_IRQ_CENTRAL_DISCONNECT = const(2) # percebe desconexão +_IRQ_GATTS_WRITE = const(3) # percebe escrita + +# --- NOSSOS UUIDs CUSTOMIZADOS --- +_PICO_SERVICE_UUID = bluetooth.UUID("71153466-1910-4388-A310-000B17D061AB") # serviço geral +_COMMAND_CHAR_UUID = bluetooth.UUID("834E4EDC-2012-42AB-B3D7-001B17D061AB") # caracterítica de escrita + +# --- CARACTERÍSTICA "GRAVÁVEL" --- +# Criamos a característica e dizemos que ela suporta escrita (FLAG_WRITE) +command_characteristic = ( + _COMMAND_CHAR_UUID, + bluetooth.FLAG_WRITE, +) + +# --- SERVIÇO --- +pico_service = ( + _PICO_SERVICE_UUID, + (command_characteristic,), # Agrupamos a característica dentro do serviço +) + +_command_buffer = "" +_command_to_run = None + +class PicoBLE: + def __init__(self, name="BitDogLab Pico2W"): + self._ble = bluetooth.BLE() + self._ble.active(True) + self._ble.irq(self.ble_irq) # Registra o handler de eventos + + # Registra o serviço e as características no servidor GATT + # Isso cria o "arquivo" onde o app pode escrever + ((self._handle_command,),) = self._ble.gatts_register_services((pico_service,)) + + self._connections = set() + self._name = name + self.advertise() + # O que controla todos os eventos BLE + def ble_irq(self, event, data): + global _command_buffer + global _command_to_run + # Se um dispositivo se conectar à placa + if event == _IRQ_CENTRAL_CONNECT: + conn_handle, _, _ = data + self._connections.add(conn_handle) + print("Dispositivo Conectado!") + + # Se o dispositivo se desconectar + elif event == _IRQ_CENTRAL_DISCONNECT: + conn_handle, _, _ = data + self._connections.remove(conn_handle) + print("Dispositivo Desconectado!") + _command_buffer = "" + # Começa a anunciar de novo para ser encontrado por outros + self.advertise() + + # Se o dispostivo conectado mandar algum comando + elif event == _IRQ_GATTS_WRITE: + conn_handle, value_handle = data + + if conn_handle in self._connections and value_handle == self._handle_command: + received_chunk = self._ble.gatts_read(self._handle_command) + + if received_chunk == b'_EOT_': + command_received = _command_buffer + _command_buffer = "" # Limpa o buffer imediatamente + + # ------ LÓGICA HÍBRIDA ------ + # Se o comando for para parar o jogo, execute-o AGORA. + if command_received == "snake_stop()": + print("Comando de parada recebido. Executando imediatamente.") + try: + snake_stop() + except Exception as e: + print(f"Erro ao executar snake_stop(): {e}") + else: + # Para todos os outros comandos, use a fila. + print(f"Comando '{command_received}' recebido e enfileirado.") + _command_to_run = command_received + # ----------------------------- + + else: + _command_buffer += received_chunk.decode() + print(f"Pedaço recebido. Buffer agora com {len(_command_buffer)} bytes.") + + def advertise(self): + print(f"Anunciando como '{self._name}'...") + # Anuncia o nome do dispositivo + self._ble.gap_advertise(100000, adv_data=bytes(self._name, 'UTF-8')) + + +led = Pin("LED", Pin.OUT) + + + +# Inicia o servidor BLE +pico_server = PicoBLE() + +# Game code +class Snake: + up = 0 + down = 1 + left = 2 + right = 3 + + def __init__(self, x=int(SEGMENTS_WIDE/2), y=int(SEGMENTS_HIGH/2) + 1): + self.segments = [[x, y]] + self.x = x + self.y = y + self.dir = random.randint(0,3) + self.state = True + + def reset(self, x=int(SEGMENTS_WIDE/2), y=int(SEGMENTS_HIGH/2) + 1): + self.segments = [[x, y]] + self.x = x + self.y = y + self.dir = random.randint(0,3) + self.state = True + + def move(self): + new_x = self.x + new_y = self.y + + if self.dir == Snake.up: + new_y -= 1 + elif self.dir == Snake.down: + new_y += 1 + elif self.dir == Snake.left: + new_x -= 1 + elif self.dir == Snake.right: + new_x += 1 + + # --- NOVO: Lógica para atravessar a tela --- + if new_x < 0: + new_x = SEGMENTS_WIDE - 1 + elif new_x >= SEGMENTS_WIDE: + new_x = 0 + + if new_y < 0: + new_y = SEGMENTS_HIGH - 1 + elif new_y >= SEGMENTS_HIGH: + new_y = 0 + + # --- FIM DA MODIFICAÇÃO --- + + for i, _ in enumerate(self.segments): + if i != len(self.segments) - 1: + self.segments[i][0] = self.segments[i+1][0] + self.segments[i][1] = self.segments[i+1][1] + + if self._check_crash(new_x, new_y): + # Oh no, we killed the snake :C + if self.state == True: + # play an ugly sound + buzzer.freq(200) + buzzer.duty_u16(2000) + time.sleep(0.5) + buzzer.duty_u16(0) + self.state = False + + self.x = new_x + self.y = new_y + + self.segments[-1][0] = self.x + self.segments[-1][1] = self.y + + def eat(self): + oled.fill_rect(self.x * SEGMENT_PIXELS, self.y * SEGMENT_PIXELS, SEGMENT_PIXELS, SEGMENT_PIXELS, 0) + oled.rect(self.x * SEGMENT_PIXELS, self.y * SEGMENT_PIXELS, SEGMENT_PIXELS, SEGMENT_PIXELS, 1) + self.segments.append([self.x, self.y]) + # Make a sound + buzzer.freq(1000) + buzzer.duty_u16(2000) + time.sleep(0.100) + buzzer.duty_u16(0) + + def change_dir(self, dir): + if dir == Snake.down and self.dir == Snake.up: + return False + + elif dir == Snake.up and self.dir == Snake.down: + return False + + elif dir == Snake.right and self.dir == Snake.left: + return False + + elif dir == Snake.left and self.dir == Snake.right: + return False + + self.dir = dir + + def _check_crash(self, new_x, new_y): + # --- NOVO: Verifica colisão apenas com a própria cobra --- + if [new_x, new_y] in self.segments: + return True + else: + return False + + def draw(self): + oled.rect(self.segments[-1][0] * SEGMENT_PIXELS, self.segments[-1][1] * SEGMENT_PIXELS, SEGMENT_PIXELS, SEGMENT_PIXELS, 1) + +def pico_snake_main(): + global player, food, is_game_running + + # O loop principal agora depende da nossa variável de estado + while is_game_running: + # --- INICIALIZAÇÃO DE UMA PARTIDA --- + player = Snake() + food = random.choice([coord for coord in VALID_RANGE if coord not in player.segments]) + + oled.fill(0) + oled.fill_rect(food[0] * SEGMENT_PIXELS , food[1] * SEGMENT_PIXELS, SEGMENT_PIXELS, SEGMENT_PIXELS, 1) + + game_timer.init(freq=5, mode=Timer.PERIODIC, callback=update_game) + + # Definições do Joystick (mantidas dentro para escopo local) + adc_vrx = ADC(Pin(26)) + adc_vry = ADC(Pin(27)) + def down(): return adc_vrx.read_u16() > 49152 + def up(): return adc_vrx.read_u16() < 16384 + def left(): return adc_vry.read_u16() > 49152 + def right(): return adc_vry.read_u16() < 16384 + + # --- LOOP DO JOGO EM EXECUÇÃO (UMA PARTIDA) --- + while player.state == True: + # VERIFICAÇÃO DE PARADA: Se o estado mudou para False, saia do loop interno + if not is_game_running: + break + + if up(): player.change_dir(Snake.up) + elif right(): player.change_dir(Snake.right) + elif left(): player.change_dir(Snake.left) + elif down(): player.change_dir(Snake.down) + + time.sleep(0.01) + + # --- FIM DE PARTIDA --- + game_timer.deinit() + + # VERIFICAÇÃO DE PARADA: Se o jogo foi interrompido externamente, saia do loop principal + if not is_game_running: + break + + # --- TELA DE GAME OVER --- + oled.fill(0) + oled.text("Game Over!" , int(SCREEN_WIDTH/2) - int(len("Game Over!")/2 * 8), int(SCREEN_HEIGHT/2) - 16) + oled.text("Score:" + str(len(player.segments)) , int(SCREEN_WIDTH/2) - int(len("Score:" + str(len(player.segments))) /2 * 8), int(SCREEN_HEIGHT/2)) + oled.text("Press joy to rst", 0, SCREEN_HEIGHT - 8) + oled.show() + + time.sleep(0.5) + # Aguarda o botão do joystick OU o comando de parada + while joystick_button.value() != 0: + if not is_game_running: + break + time.sleep(0.01) + + # --- LIMPEZA FINAL --- + # Este código só será executado quando 'is_game_running' se tornar False + game_timer.deinit() # Garante que o timer está parado + oled.fill(0) + oled.show() + print("Jogo finalizado e tela limpa.") + +def update_game(timer): + global food + global player + + # Remove a cauda anterior da cobra (mais eficiente do que limpar a tela inteira) + oled.fill_rect(player.segments[0][0] * SEGMENT_PIXELS, player.segments[0][1] * SEGMENT_PIXELS, SEGMENT_PIXELS, SEGMENT_PIXELS, 0) + + # Move a cobra + player.move() + + if player.state == True: + # A cobra ainda está viva e se movendo + if food[0] == player.x and food[1] == player.y: + # A cobra alcançou a comida + player.eat() + if len(player.segments) < (SEGMENTS_WIDE * SEGMENTS_HIGH): + food = random.choice([coord for coord in VALID_RANGE if coord not in player.segments]) + oled.fill_rect(food[0] * SEGMENT_PIXELS , food[1] * SEGMENT_PIXELS, SEGMENT_PIXELS, SEGMENT_PIXELS, 1) + else: # Vitória! + player.state = False + + player.draw() + + # Mostra o novo frame + oled.show() + +def snake_start(): + global is_game_running + # Apenas inicia o jogo se ele não estiver rodando + if not is_game_running: + is_game_running = True + print("Iniciando o jogo Snake...") + try: + pico_snake_main() + except Exception as e: + print(f"Game crashed: {e}") + oled.fill(0) + oled.text("Game Error", 0, 0) + oled.show() + # Garante que o estado seja False ao sair, por qualquer motivo + is_game_running = False + +def snake_stop(): + global is_game_running + if is_game_running: + print("Recebido comando para parar o jogo. Parando imediatamente.") + is_game_running = False + game_timer.deinit() + oled.fill(0) + oled.show() + +# O loop principal agora é o nosso processador de comandos. +while True: + # Há um novo comando para executar? + if _command_to_run: + print(f"Executando comando enfileirado: {_command_to_run}") + + # Copia o comando para uma variável local e limpa a global + # Isso evita que o comando seja executado várias vezes. + command = _command_to_run + _command_to_run = None + + try: + # Executa o comando no loop principal, que é um local seguro. + exec(command) + except Exception as e: + print(f"Erro ao executar comando '{command}': {e}") + + # Uma pequena pausa para não sobrecarregar o processador + time.sleep(0.1) diff --git a/src/App.tsx b/src/App.tsx index 20b3d17..082e357 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -50,6 +50,8 @@ import LedRGBInfo4 from "./pages/LedRGB/LedRGBInfo4"; import Jogo from "./pages/Jogo/Jogo"; import EmConstrucao from "./pages/EmConstrucao"; +import Snake from "./pages/Snake"; + export function App() { useEffect(() => { ScreenOrientation.lock({ orientation: "portrait" }); @@ -69,6 +71,10 @@ export function App() { } /> + + } /> + + } /> } /> @@ -112,13 +118,7 @@ export function App() { } /> } /> - - - } /> - - - - + } /> diff --git a/src/components/ConnectionStatus.tsx b/src/components/ConnectionStatus.tsx index 8b3660f..2773185 100644 --- a/src/components/ConnectionStatus.tsx +++ b/src/components/ConnectionStatus.tsx @@ -7,10 +7,18 @@ export const ConnectionStatus = () => { // Get connection label based on type const getConnectionLabel = () => { - if (!isConnected) return "Desconectado"; - return connectionType === ConnectionType.BLUETOOTH - ? "Conectado via Bluetooth" - : "Conectado via Cabo"; + if (!isConnected){ + return "Desconectado"; + } + if (connectionType === ConnectionType.BLUETOOTH_CLASSIC){ + return "Conectado via Bluetooth Clássico" + } else if (connectionType === ConnectionType.BLUETOOTH_LE) { + return "Conectado via Bluetooth Low Energy" + } else if (connectionType === ConnectionType.WIFI) { + return "Conectado via WiFi" + } else { + return "Conectado via Cabo" + } }; return ( diff --git a/src/components/LED.tsx b/src/components/LED.tsx index f52a5e8..110e75f 100644 --- a/src/components/LED.tsx +++ b/src/components/LED.tsx @@ -80,9 +80,9 @@ const LED: React.FC = ({ if (selected) { containerRef.current.classList.remove("border-neutral-palette-30", "border-2"); - containerRef.current.classList.add("border-secondary", "border-4", "sm:border-4", "md:border-8"); + containerRef.current.classList.add("border-secondary", "border-4", "rounded-xl"); } else { - containerRef.current.classList.remove("border-secondary", "border-4", "sm:border-4", "md:border-8"); + containerRef.current.classList.remove("border-secondary", "border-4"); containerRef.current.classList.add("border-neutral-palette-30", "border-2"); } }, [selected]); diff --git a/src/contexts/ConnectionContext.tsx b/src/contexts/ConnectionContext.tsx index ef62547..e80c702 100644 --- a/src/contexts/ConnectionContext.tsx +++ b/src/contexts/ConnectionContext.tsx @@ -1,27 +1,63 @@ import React, { createContext, useState, useContext, useEffect, useCallback } from "react"; +import { useBluetoothLE } from "../hooks/useBluetoothLE"; +import { useWifi } from "../hooks/useWifi"; // Importa o hook WiFi +import type { BleDevice } from '@capacitor-community/bluetooth-le'; +// Adicionamos WiFi como novo tipo de conexão export enum ConnectionType { CABLE = "cable", - BLUETOOTH = "bluetooth", + BLUETOOTH_CLASSIC = "bluetooth_classic", + BLUETOOTH_LE = "bluetooth_le", + WIFI = "wifi", // 🆕 Novo tipo WiFi NONE = "none", } +// Interface para dispositivos Bluetooth Clássico interface BluetoothDevice { id: string; name: string; address: string; } +// Interface expandida para suportar WiFi interface ConnectionContextType { isConnected: boolean; connectionType: ConnectionType; serialPort: any; + + // Dispositivos Bluetooth Clássico availableDevices: BluetoothDevice[]; + + // Dispositivos BLE + bleDevices: BleDevice[]; + isBleScanning: boolean; + connectedBleDevice?: BleDevice; + bleError: string | null; + + // 🆕 Estados WiFi + wifiLogs: string[]; + wifiError: string | null; + + // Métodos de conexão connectCable: () => Promise; - connectBluetooth: (deviceId: string) => Promise; + connectBluetoothClassic: (deviceId: string) => Promise; + connectBluetoothLE: (device: BleDevice) => Promise; + connectWifi: (ip?: string, port?: number) => Promise; // 🆕 Novo método WiFi disconnect: () => Promise; + + // Envio de comandos sendCommand: (command: string) => Promise; + + // Métodos de escaneamento scanBluetoothDevices: () => Promise; + scanBleDevices: () => Promise; + + // 🆕 Métodos WiFi + clearWifiLogs: () => void; + clearWifiError: () => void; + + // Métodos BLE + clearBleError: () => void; } const BAUD_RATE = 9600; @@ -30,15 +66,48 @@ const BLUETOOTH_DELIMITER = "\n"; const CONNECTION_CHECK_INTERVAL = 5000; const BLUETOOTH_ERRORS = ["bt socket closed", "read return: -1", "IOException", "disconnected", "Connection lost", "Device not connected"]; +// 🆕 Configurações padrão WiFi +const DEFAULT_WIFI_IP = "192.168.1.100"; // IP padrão da Pico W +const DEFAULT_WIFI_PORT = 8080; + const ConnectionContext = createContext(undefined); export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + // Estados originais (Serial + Bluetooth Clássico) const [isConnected, setIsConnected] = useState(false); const [connectionType, setConnectionType] = useState(ConnectionType.NONE); const [serialPort, setSerialPort] = useState(null); const [reader, setReader] = useState(null); const [availableDevices, setAvailableDevices] = useState([]); + // Hook BLE + const { + devices: bleDevices, + isConnected: isBleConnected, + connectedDevice: connectedBleDevice, + isScanning: isBleScanning, + error: bleError, + scan: scanBle, + connect: connectBle, + disconnect: disconnectBle, + writeData: writeBleData, + clearError: clearBleError + } = useBluetoothLE(); + + // 🆕 Hook WiFi + const { + isConnected: isWifiConnected, + //isConnecting: isWifiConnecting, + logs: wifiLogs, + error: wifiError, + connect: connectWifiDirect, + send: sendWifi, + disconnect: disconnectWifi, + clearLogs: clearWifiLogs, + clearError: clearWifiError + } = useWifi(); + + // Utilitários existentes (mantidos) const promisifyBluetooth = useCallback((fn: (...args: any[]) => void, ...args: any[]): Promise => new Promise((resolve, reject) => fn(...args, resolve, reject)), []); @@ -72,6 +141,7 @@ export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ ch } }, [isBluetoothError, resetConnection]); + // Método de conexão Serial (mantido) const connectCable = useCallback(async () => { if (!navigator.serial) throw new Error("Web Serial API não é suportada neste navegador"); @@ -96,7 +166,6 @@ export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ ch if (false) { console.log("Recebido da Serial:", decoder.decode(value)); } - // Dados recebidos podem ser processados aqui se necessário } } catch (error) { resetConnection(); @@ -112,6 +181,7 @@ export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ ch } }, [resetConnection]); + // Método de escaneamento Bluetooth Clássico (mantido) const scanBluetoothDevices = useCallback(async () => { try { await ensureBluetoothEnabled(); @@ -123,7 +193,8 @@ export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ ch } }, [ensureBluetoothEnabled, promisifyBluetooth]); - const connectBluetooth = useCallback(async (deviceId: string) => { + // Método de conexão Bluetooth Clássico (mantido) + const connectBluetoothClassic = useCallback(async (deviceId: string) => { try { if (isConnected) await disconnect(); await ensureBluetoothEnabled(); @@ -136,7 +207,7 @@ export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ ch handleBluetoothError ); - setConnectionType(ConnectionType.BLUETOOTH); + setConnectionType(ConnectionType.BLUETOOTH_CLASSIC); setIsConnected(true); } catch (error) { console.error("Erro na conexão Bluetooth:", error); @@ -144,6 +215,68 @@ export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ ch } }, [isConnected, ensureBluetoothEnabled, promisifyBluetooth, handleBluetoothError]); + // Método de escaneamento BLE (mantido) + const scanBleDevices = useCallback(async () => { + try { + console.log("🔍 Iniciando escaneamento BLE..."); + await scanBle(); + } catch (error) { + console.error("Erro no escaneamento BLE:", error); + throw new Error("Falha ao buscar dispositivos BLE"); + } + }, [scanBle]); + + // Método de conexão BLE (mantido) + const connectBluetoothLE = useCallback(async (device: BleDevice) => { + try { + console.log("🔵 Conectando ao dispositivo BLE:", device.name || device.deviceId); + + if (isConnected && connectionType !== ConnectionType.BLUETOOTH_LE) { + await disconnect(); + } + + const success = await connectBle(device); + + if (success) { + setConnectionType(ConnectionType.BLUETOOTH_LE); + setIsConnected(true); + console.log("✅ Conectado com sucesso ao BLE"); + } else { + throw new Error("Falha na conexão BLE"); + } + } catch (error) { + console.error("Erro na conexão BLE:", error); + throw new Error("Falha ao conectar ao dispositivo BLE"); + } + }, [isConnected, connectionType, connectBle]); + + // 🆕 Método de conexão WiFi + const connectWifi = useCallback(async (ip: string = DEFAULT_WIFI_IP, port: number = DEFAULT_WIFI_PORT) => { + try { + console.log(`📶 Conectando via WiFi em ${ip}:${port}...`); + + // Se já estiver conectado em outro tipo, desconecta primeiro + if (isConnected && connectionType !== ConnectionType.WIFI) { + await disconnect(); + } + + // Tenta conectar usando o hook WiFi + const success = await connectWifiDirect({ ip, port }); + + if (success) { + setConnectionType(ConnectionType.WIFI); + setIsConnected(true); + console.log("✅ Conectado com sucesso via WiFi"); + } else { + throw new Error("Falha na conexão WiFi"); + } + } catch (error) { + console.error("Erro na conexão WiFi:", error); + throw new Error("Falha ao conectar via WiFi"); + } + }, [isConnected, connectionType, connectWifiDirect]); + + // Método de desconexão (atualizado para WiFi) const disconnect = useCallback(async () => { try { if (connectionType === ConnectionType.CABLE) { @@ -154,47 +287,86 @@ export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ ch if (serialPort) await serialPort.close(); setSerialPort(null); setReader(null); - } else if (connectionType === ConnectionType.BLUETOOTH) { + } else if (connectionType === ConnectionType.BLUETOOTH_CLASSIC) { try { await promisifyBluetooth(window.bluetoothSerial.unsubscribe); } catch {} await promisifyBluetooth(window.bluetoothSerial.disconnect); + } else if (connectionType === ConnectionType.BLUETOOTH_LE) { + await disconnectBle(); + } else if (connectionType === ConnectionType.WIFI) { + // 🆕 Desconexão WiFi + await disconnectWifi(); } resetConnection(); } catch (error) { console.error("Erro ao desconectar:", error); throw new Error("Falha ao desconectar do dispositivo"); } - }, [connectionType, reader, serialPort, promisifyBluetooth, resetConnection]); + }, [connectionType, reader, serialPort, promisifyBluetooth, resetConnection, disconnectBle, disconnectWifi]); + // Método de envio de comandos (atualizado para WiFi) const sendCommand = useCallback(async (command: string) => { if (!isConnected) throw new Error("Não conectado a nenhum dispositivo"); - const fullCommand = command + COMMAND_TERMINATOR; - try { if (connectionType === ConnectionType.CABLE) { + const fullCommand = command + COMMAND_TERMINATOR; const writer = serialPort.writable.getWriter(); try { await writer.write(new TextEncoder().encode(fullCommand)); } finally { writer.releaseLock(); } - } else if (connectionType === ConnectionType.BLUETOOTH) { + } else if (connectionType === ConnectionType.BLUETOOTH_CLASSIC) { + const fullCommand = command + COMMAND_TERMINATOR; await promisifyBluetooth(window.bluetoothSerial.write, fullCommand); + } else if (connectionType === ConnectionType.BLUETOOTH_LE) { + const success = await writeBleData(command); + if (!success) { + throw new Error("Falha ao enviar comando via BLE"); + } + } else if (connectionType === ConnectionType.WIFI) { + // 🆕 Envio via WiFi + const success = await sendWifi(command); + if (!success) { + throw new Error("Falha ao enviar comando via WiFi"); + } } + + console.log(`📤 Comando enviado via ${connectionType}:`, command); } catch (error) { console.error("Erro ao enviar comando:", error); - if (connectionType === ConnectionType.BLUETOOTH) handleBluetoothError(error); + if (connectionType === ConnectionType.BLUETOOTH_CLASSIC) handleBluetoothError(error); throw new Error("Falha ao enviar comando ao dispositivo"); } - }, [isConnected, connectionType, serialPort, promisifyBluetooth, handleBluetoothError]); + }, [isConnected, connectionType, serialPort, promisifyBluetooth, handleBluetoothError, writeBleData, sendWifi]); + + // 🔄 Efeito para sincronizar estado BLE com o contexto + useEffect(() => { + if (connectionType === ConnectionType.BLUETOOTH_LE) { + if (!isBleConnected && isConnected) { + console.log("🔵 BLE foi desconectado externamente"); + resetConnection(); + } + } + }, [isBleConnected, isConnected, connectionType, resetConnection]); - // Cleanup e verificação periódica + // 🔄 Efeito para sincronizar estado WiFi com o contexto + useEffect(() => { + if (connectionType === ConnectionType.WIFI) { + if (!isWifiConnected && isConnected) { + console.log("📶 WiFi foi desconectado externamente"); + resetConnection(); + } + } + }, [isWifiConnected, isConnected, connectionType, resetConnection]); + + // Cleanup e verificação periódica (mantido) useEffect(() => { return () => { if (isConnected) disconnect().catch(console.error); }; }, [isConnected, disconnect]); useEffect(() => { - if (!isConnected || connectionType !== ConnectionType.BLUETOOTH) return; + if (!isConnected || connectionType !== ConnectionType.BLUETOOTH_CLASSIC) return; const interval = setInterval(async () => { try { @@ -209,8 +381,40 @@ export const ConnectionProvider: React.FC<{ children: React.ReactNode }> = ({ ch return ( {children} diff --git a/src/hooks/useBluetoothLE.ts b/src/hooks/useBluetoothLE.ts new file mode 100644 index 0000000..b8e444b --- /dev/null +++ b/src/hooks/useBluetoothLE.ts @@ -0,0 +1,257 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import { BleClient, textToDataView, type BleDevice } from '@capacitor-community/bluetooth-le'; + +// UUIDs que estão no firmware da placa +const PICO_SERVICE_UUID = "71153466-1910-4388-A310-000B17D061AB"; +const COMMAND_CHAR_UUID = "834E4EDC-2012-42AB-B3D7-001B17D061AB"; + +export function useBluetoothLE() { + // Estados reativos do hook + const [devices, setDevices] = useState([]); + const [isConnected, setIsConnected] = useState(false); + const [connectedDevice, setConnectedDevice] = useState(); + const [isScanning, setIsScanning] = useState(false); + const [error, setError] = useState(null); + const [mtu, setMtu] = useState(23); // MTU padrão, será atualizado após conexão + + // useRef para manter referências que não precisam causar re-render + const deviceObjectRef = useRef(undefined); + const scanTimeoutRef = useRef(undefined); + + // Função de callback para desconexão + const onDisconnect = useCallback((deviceId: string) => { + console.log(`device ${deviceId} disconnected`); + setIsConnected(false); + setConnectedDevice(undefined); + setMtu(23); // Reset MTU para valor padrão + deviceObjectRef.current = undefined; + }, []); + + // Função de scan + const scan = useCallback(async () => { + if (isScanning) { + console.log('Scan já está em andamento'); + return; + } + + try { + setError(null); + setIsScanning(true); + setDevices([]); // Limpa a lista anterior + + await BleClient.initialize({ androidNeverForLocation: true }); + console.log('Requesting BLE scan...'); + + await BleClient.requestLEScan( + { + // Filtra por BitDogLab + namePrefix: 'BitDogLab' + }, + (result) => { + console.log('Received new scan result: ', result.device.name); + + // Adiciona o dispositivo à lista, evitando duplicatas + setDevices(prevDevices => { + const exists = prevDevices.find(d => d.deviceId === result.device.deviceId); + if (exists) return prevDevices; + return [...prevDevices, result.device]; + }); + } + ); + + // Para o scan após 5 segundos + scanTimeoutRef.current = setTimeout(async () => { + try { + await BleClient.stopLEScan(); + console.log('Stopped scanning'); + setIsScanning(false); + } catch (error) { + console.error('Erro ao parar o scan:', error); + setIsScanning(false); + } + }, 5000); + + } catch (error) { + console.error('Erro no scan:', error); + setError(error instanceof Error ? error.message : 'Erro desconhecido no scan'); + setIsScanning(false); + } + }, [isScanning]); + + // Função de conexão + const connect = useCallback(async (device: BleDevice): Promise => { + try { + setError(null); + + await BleClient.connect(device.deviceId, (deviceId) => onDisconnect(deviceId)); + console.log('connected to device', device.name); + + // Obtém o MTU real da conexão + try { + const deviceMtu = await BleClient.getMtu(device.deviceId); + setMtu(deviceMtu); + console.log(`📡 MTU obtido: ${deviceMtu} bytes`); + } catch (mtuError) { + console.warn('Não foi possível obter MTU, usando padrão:', mtuError); + setMtu(23); // Fallback para MTU padrão + } + + setConnectedDevice(device); + setIsConnected(true); + deviceObjectRef.current = device; + + return true; + } catch (error) { + console.error('Erro na conexão:', error); + setError(error instanceof Error ? error.message : 'Erro desconhecido na conexão'); + setIsConnected(false); + setConnectedDevice(undefined); + deviceObjectRef.current = undefined; + return false; + } + }, [onDisconnect]); + + // Função de desconexão manual + const disconnect = useCallback(async (): Promise => { + if (!deviceObjectRef.current) { + console.log('Nenhum dispositivo conectado para desconectar'); + return false; + } + + try { + await BleClient.disconnect(deviceObjectRef.current.deviceId); + console.log('Dispositivo desconectado'); + return true; + } catch (error) { + console.error('Erro ao desconectar:', error); + setError(error instanceof Error ? error.message : 'Erro desconhecido na desconexão'); + return false; + } + }, []); + + // Função de envio de dados OTIMIZADA com MTU real + const writeData = useCallback(async (data: string): Promise => { + if (!deviceObjectRef.current || !isConnected) { + const errorMsg = "Nenhum dispositivo conectado para enviar dados."; + console.error(errorMsg); + setError(errorMsg); + return false; + } + + // Calcula o tamanho do chunk baseado no MTU real + // Reserva alguns bytes para overhead do protocolo BLE + const PROTOCOL_OVERHEAD = 3; // ATT header overhead + const CHUNK_SIZE = Math.max(mtu - PROTOCOL_OVERHEAD, 20); // Mínimo de 20 para compatibilidade + const END_OF_TRANSMISSION_SIGNAL = "_EOT_"; + + try { + setError(null); + console.log(`📤 Enviando comando (${data.length} bytes) usando chunks de ${CHUNK_SIZE} bytes (MTU: ${mtu})`); + + // Se o comando for pequeno o suficiente, envia de uma vez + if (data.length <= CHUNK_SIZE) { + console.log('📦 Comando pequeno - enviando em pacote único'); + + const dataToSend = textToDataView(data); + await BleClient.write( + deviceObjectRef.current.deviceId, + PICO_SERVICE_UUID, + COMMAND_CHAR_UUID, + dataToSend + ); + + // Sinal de fim (sempre necessário para o firmware) + await new Promise(resolve => setTimeout(resolve, 10)); // Pequeno delay + const finalSignal = textToDataView(END_OF_TRANSMISSION_SIGNAL); + await BleClient.write( + deviceObjectRef.current.deviceId, + PICO_SERVICE_UUID, + COMMAND_CHAR_UUID, + finalSignal + ); + + console.log('✅ Comando enviado com sucesso em pacote único.'); + return true; + } + + // Para comandos maiores, quebra em chunks otimizados + console.log('📦 Comando grande - enviando em múltiplos chunks'); + const totalChunks = Math.ceil(data.length / CHUNK_SIZE); + + for (let i = 0; i < data.length; i += CHUNK_SIZE) { + const chunkIndex = Math.floor(i / CHUNK_SIZE) + 1; + const chunk = data.substring(i, i + CHUNK_SIZE); + const dataToSend = textToDataView(chunk); + + console.log(`📤 Enviando chunk ${chunkIndex}/${totalChunks} (${chunk.length} bytes)`); + + await BleClient.write( + deviceObjectRef.current.deviceId, + PICO_SERVICE_UUID, + COMMAND_CHAR_UUID, + dataToSend + ); + + // Delay menor devido ao MTU maior + await new Promise(resolve => setTimeout(resolve, 2)); + } + + // Após enviar todos os pedaços, envia o sinal de fim + console.log('📤 Enviando sinal de fim de transmissão'); + const finalSignal = textToDataView(END_OF_TRANSMISSION_SIGNAL); + await BleClient.write( + deviceObjectRef.current.deviceId, + PICO_SERVICE_UUID, + COMMAND_CHAR_UUID, + finalSignal + ); + + console.log('✅ Comando enviado com sucesso em múltiplos chunks.'); + return true; + } catch (error) { + console.error('❌ Falha ao enviar comando', error); + setError(error instanceof Error ? error.message : 'Erro desconhecido no envio'); + return false; + } + }, [isConnected, mtu]); + + // 🧹 Cleanup quando o componente é desmontado + useEffect(() => { + return () => { + // Para o scan se estiver rodando + if (scanTimeoutRef.current) { + clearTimeout(scanTimeoutRef.current); + } + + // Para o scan BLE se estiver ativo + if (isScanning) { + BleClient.stopLEScan().catch(console.error); + } + + // Desconecta se estiver conectado + if (deviceObjectRef.current && isConnected) { + BleClient.disconnect(deviceObjectRef.current.deviceId).catch(console.error); + } + }; + }, [isScanning, isConnected]); + + // 🎯 Retorna o estado e as funções para o componente + return { + // Estados + devices, + isConnected, + connectedDevice, + isScanning, + error, + mtu, // Exposição do MTU para debug/info + + // Funções + scan, + connect, + disconnect, + writeData, + + // Função utilitária para limpar erros + clearError: useCallback(() => setError(null), []) + }; +} \ No newline at end of file diff --git a/src/hooks/useBuzzers.ts b/src/hooks/useBuzzers.ts index 88edb6e..3a1727d 100644 --- a/src/hooks/useBuzzers.ts +++ b/src/hooks/useBuzzers.ts @@ -18,6 +18,7 @@ export const useBuzzers = ( const [octave, setOctave] = useState(4); const [isPlaying, setIsPlaying] = useState(false); const startTimeRef = useRef(null); + const currentNoteRef = useRef(null); // Controla qual nota está tocando // Gravação const recordingBuffer = useRef([]); @@ -62,15 +63,32 @@ export const useBuzzers = ( }, [isRecording]); /** - * Manipula o evento de pressionar uma tecla (onMouseDown) + * Manipula o evento de pressionar uma tecla (onMouseDown/onTouchStart) + * IMPORTANTE: Só executa se a nota não estiver já sendo tocada * @param note - Nota musical pressionada */ const handleNotePress = async (note: Note) => { + // Evita comandos duplicados se a mesma nota já estiver tocando + if (currentNoteRef.current === note && isPlaying) { + console.log("⚠️ Nota já está sendo tocada:", note); + return; + } + + // Se há uma nota tocando, para ela primeiro + if (isPlaying && currentNoteRef.current) { + await handleNoteRelease(); + // Pequeno delay para garantir que o comando anterior foi processado + await new Promise(resolve => setTimeout(resolve, 100)); + } + const selectedOctave = octave; const frequency = noteToFrequency(note, selectedOctave); startTimeRef.current = Date.now(); + currentNoteRef.current = note; setIsPlaying(true); + console.log(`🎹 Pressionando tecla: ${note} (oitava ${selectedOctave}) - ${frequency}Hz`); + // Lógica de gravação if (isRecording && recordingStartTime.current && lastEventTime.current) { const now = Date.now(); @@ -78,7 +96,9 @@ export const useBuzzers = ( recordingBuffer.current.push({ frequency: Math.round(frequency), delay, - isPressed: true + isPressed: true, + note: note, + octave: selectedOctave }); lastEventTime.current = now; } @@ -88,17 +108,22 @@ export const useBuzzers = ( } catch (error) { console.error("Erro ao iniciar nota:", error); setIsPlaying(false); + currentNoteRef.current = null; } }; /** - * Manipula o evento de soltar uma tecla (onMouseUp) - * @param note - Nota musical que foi solta + * Manipula o evento de soltar uma tecla (onMouseUp/onTouchEnd) + * IMPORTANTE: Só executa se há uma nota tocando */ const handleNoteRelease = async () => { - if (!startTimeRef.current) return; + if (!isPlaying || !currentNoteRef.current || !startTimeRef.current) { + console.log("⚠️ Nenhuma nota para parar"); + return; + } const duration = Date.now() - startTimeRef.current; + const minDuration = Math.max(duration, 50); setIsPlaying(false); startTimeRef.current = null; @@ -106,15 +131,20 @@ export const useBuzzers = ( // Lógica de gravação if (isRecording && lastEventTime.current) { const now = Date.now(); - // O delay do "release" pode não ser necessário, mas pode ser interessante registrar recordingBuffer.current.push({ isPressed: false, + duration: duration, delay: now - lastEventTime.current }); lastEventTime.current = now; } + // Reset dos estados antes de enviar comando + setIsPlaying(false); + currentNoteRef.current = null; + startTimeRef.current = null; + try { await buzzersController.current?.stopBuzzer(minDuration); } catch (error) { @@ -122,16 +152,34 @@ export const useBuzzers = ( } }; - // Expondo o buffer de gravação para uso futuro, se necessário + // Método para parar qualquer som que esteja tocando (útil para emergências) + const forceStopBuzzer = async () => { + if (isPlaying) { + console.log("🛑 Forçando parada do buzzer"); + setIsPlaying(false); + currentNoteRef.current = null; + startTimeRef.current = null; + + try { + await buzzersController.current?.stopBuzzer(0); + } catch (error) { + console.error("Erro ao forçar parada:", error); + } + } + }; + + // Expondo o buffer de gravação para uso futuro const getRecordingBuffer = () => recordingBuffer.current; return { octave, setOctave, isPlaying, + currentNote: currentNoteRef.current, handleNotePress, handleNoteRelease, - getRecordingBuffer, // pode ser usado depois para exportar/salvar - buzzersController: buzzersController.current + forceStopBuzzer, // Método adicional para emergências + getRecordingBuffer, + buzzersController: buzzersController.current }; }; \ No newline at end of file diff --git a/src/hooks/useWifi.ts b/src/hooks/useWifi.ts new file mode 100644 index 0000000..937fbd8 --- /dev/null +++ b/src/hooks/useWifi.ts @@ -0,0 +1,156 @@ +// src/hooks/useWifi.ts +import { useState, useCallback } from "react"; +import { TcpSocket } from "capacitor-tcp-socket"; + +interface Device { + ip: string; + port: number; +} + +export function useWifi() { + const [clientId, setClientId] = useState(null); + const [connectedDevice, setConnectedDevice] = useState(null); + const [logs, setLogs] = useState([]); + const [isConnecting, setIsConnecting] = useState(false); + const [error, setError] = useState(null); + + const log = useCallback((msg: string) => { + console.log(msg); + setLogs((prev) => [...prev, msg]); + }, []); + + const clearError = useCallback(() => { + setError(null); + }, []); + + const clearLogs = useCallback(() => { + setLogs([]); + }, []); + + // Conectar TCP direto com IP predefinido + const connect = useCallback(async (device: Device) => { + setIsConnecting(true); + setError(null); + + try { + log(`Tentando conectar em: ${device.ip}:${device.port}`); + + const result = await TcpSocket.connect({ + ipAddress: device.ip, + port: device.port, + }); + + setClientId(result.client); + setConnectedDevice(device); + log(`Conectado com sucesso: ${device.ip}:${device.port}`); + + return true; + } catch (error: any) { + const errorMsg = `Falha ao conectar TCP: ${error.message || error}`; + log(errorMsg); + setError(errorMsg); + return false; + } finally { + setIsConnecting(false); + } + }, [log]); + + // Enviar comando (terminado em \n) + const send = useCallback(async (cmd: string): Promise => { + if (!clientId) { + const errorMsg = "Não há conexão TCP ativa"; + log(errorMsg); + setError(errorMsg); + return false; + } + + try { + await TcpSocket.send({ client: clientId, data: cmd + "\n" }); + log(`Comando enviado: ${cmd}`); + return true; + } catch (error: any) { + const errorMsg = `Falha ao enviar comando: ${error.message || error}`; + log(errorMsg); + setError(errorMsg); + return false; + } + }, [clientId, log]); + + const read = useCallback(async (): Promise => { + if (!clientId) { + log("Tentativa de leitura sem conexão ativa"); + return ""; + } + + try { + const result = await TcpSocket.read({ + client: clientId, + expectLen: 1024, + timeout: 2, + }); + log(`Dados recebidos TCP: ${result.result}`); + return result.result ?? ""; + } catch (error: any) { + log(`Falha ao ler resposta: ${error.message || error}`); + return ""; + } + }, [clientId, log]); + + const disconnect = useCallback(async (): Promise => { + if (!clientId) { + log("Nenhuma conexão ativa para desconectar"); + return true; + } + + try { + await TcpSocket.disconnect({ client: clientId }); + setClientId(null); + setConnectedDevice(null); + log("Desconectado com sucesso"); + return true; + } catch (error: any) { + const errorMsg = `Falha ao desconectar: ${error.message || error}`; + log(errorMsg); + setError(errorMsg); + // Mesmo com erro, limpa o estado + setClientId(null); + setConnectedDevice(null); + return false; + } + }, [clientId, log]); + + // Função para enviar comando e aguardar resposta (para compatibilidade) + const sendAndRead = useCallback(async (cmd: string): Promise => { + const sendSuccess = await send(cmd); + if (!sendSuccess) return ""; + + // Aguarda um pouco antes de ler a resposta + await new Promise(resolve => setTimeout(resolve, 100)); + return await read(); + }, [send, read]); + + // Função de ping para testar conectividade + const ping = useCallback(async (): Promise => { + const response = await sendAndRead("print('pong')"); + return response.includes('pong') || response.includes('OK'); + }, [sendAndRead]); + + return { + // Estados + isConnected: !!clientId, + isConnecting, + connectedDevice, + logs, + error, + + // Ações + connect, + send, + read, + sendAndRead, + disconnect, + ping, + clearLogs, + clearError, + }; +} \ No newline at end of file diff --git a/src/json/interpreters/buzzer.ts b/src/json/interpreters/buzzer.ts index 27e44ec..08ba63f 100644 --- a/src/json/interpreters/buzzer.ts +++ b/src/json/interpreters/buzzer.ts @@ -8,7 +8,7 @@ export function interpreterBuzzer(data: BuzzersData): string[] { commands.push(`buzzer.freq(${data.frequency})`); commands.push(`buzzer.duty_u16(700)`); commands.push(`buzzerAux.duty_u16(300)`); - commands.push(`print('Buzzers ligados - Frequência: ${data.frequency}Hz')`); + //commands.push(`print('Buzzers ligados - Frequencia: ${data.frequency}Hz')`); } else if (!data.isPressed) { @@ -16,11 +16,11 @@ export function interpreterBuzzer(data: BuzzersData): string[] { commands.push(`buzzer.duty_u16(0)`); commands.push(`buzzerAux.duty_u16(0)`); - if (data.duration) { - commands.push(`print('Buzzer desligado - Duração: ${data.duration}ms')`); +/* if (data.duration) { + commands.push(`print('Buzzer desligado - Duracao: ${data.duration}ms')`); } else { commands.push(`print('Buzzer desligado')`); - } + } */ } return commands; diff --git a/src/json/toMicropython.ts b/src/json/toMicropython.ts index 29ff7a6..227726f 100644 --- a/src/json/toMicropython.ts +++ b/src/json/toMicropython.ts @@ -19,7 +19,7 @@ function micropython(app: string, instructions: unknown): string[] { case 'buzzer': return interpreterBuzzer(instructions as { isPressed: boolean, frequency?: number, duration?: number }); default: // isso será retirado após todos os apps serem feitos - return ["em contrução..."] + return ["em construcao..."] } } diff --git a/src/pages/Components.tsx b/src/pages/Components.tsx index 9b8f8f6..f969bd8 100644 --- a/src/pages/Components.tsx +++ b/src/pages/Components.tsx @@ -65,13 +65,14 @@ export default function Components() { > Led RGB - {/**/} + onClick={() => navigate("/components/snake")} + > + Snake + + ); diff --git a/src/pages/Connection.tsx b/src/pages/Connection.tsx index 2341f7f..d3559cc 100644 --- a/src/pages/Connection.tsx +++ b/src/pages/Connection.tsx @@ -3,15 +3,20 @@ import { Button } from "@/components/ui/button"; import { Header } from "@/components/Header"; import { useNavigate } from "react-router-dom"; import { useConnection } from "../contexts/ConnectionContext"; +import type { BleDevice } from '@capacitor-community/bluetooth-le'; -type ConnectionType = "cable" | "bluetooth"; +// 🔄 Tipos de conexão expandidos +type ConnectionType = "cable" | "bluetooth_classic" | "bluetooth_le" | "wifi"; interface ConnectionState { loading: boolean; error: string | null; selectedConnectionType: ConnectionType; scanning: boolean; - selectedDevice: string | null; + selectedDevice: string | null; + selectedBleDevice: BleDevice | null; + wifiIp: string; // 🆕 Campos WiFi + wifiPort: string; } const INITIAL_STATE: ConnectionState = { @@ -20,12 +25,16 @@ const INITIAL_STATE: ConnectionState = { selectedConnectionType: "cable", scanning: false, selectedDevice: null, + selectedBleDevice: null, + wifiIp: "192.168.1.100", + wifiPort: "8080", }; const MESSAGES = { connected: "Você está conectado à placa", disconnected: "Antes de começar, primeiro conecte-se com a placa", selectDevice: "Selecione um dispositivo Bluetooth", + selectBleDevice: "Selecione um dispositivo BLE", scanningHint: "Isso pode levar alguns segundos...", noDevices: "Nenhum dispositivo encontrado", availableDevices: "Dispositivos disponíveis:", @@ -33,6 +42,7 @@ const MESSAGES = { continue: "Continuar para Componentes", errors: { scanFailed: "Falha ao buscar dispositivos Bluetooth", + bleScanFailed: "Falha ao buscar dispositivos BLE", disconnectFailed: "Falha ao desconectar", connectFailed: "Falha ao conectar", }, @@ -41,24 +51,40 @@ const MESSAGES = { disconnect: "Desconectar", scanning: "Buscando...", scan: "Buscar dispositivos", + scanBle: "Buscar dispositivos BLE", connectCable: "Conectar via cabo", - connectBluetooth: "Conectar via Bluetooth", + connectBluetoothClassic: "Conectar via Bluetooth Clássico", + connectBluetoothLE: "Conectar via Bluetooth LE", + connectWifi: "Conectar via WiFi", // 🆕 }, connectionTypes: { cable: "Conexão via cabo", - bluetooth: "Conexão Bluetooth", + bluetooth_classic: "Conexão Bluetooth Clássico", + bluetooth_le: "Conexão Bluetooth LE (BLE)", + wifi: "Conexão via WiFi", // 🆕 }, } as const; export default function Connection() { const navigate = useNavigate(); + const { isConnected, connectCable, - connectBluetooth, + connectBluetoothClassic, + connectBluetoothLE, + connectWifi, // 🆕 disconnect, - scanBluetoothDevices, - availableDevices, + //scanBluetoothDevices, + //scanBleDevices, + //availableDevices, + //bleDevices, + //isBleScanning, + bleError, + clearBleError, + wifiLogs, // 🆕 + wifiError, + clearWifiError, // 🆕 } = useConnection(); const [state, setState] = useState(INITIAL_STATE); @@ -69,43 +95,31 @@ export default function Connection() { const clearError = useCallback(() => { updateState({ error: null }); - }, [updateState]); - - const handleScan = useCallback(async () => { - try { - updateState({ scanning: true, error: null }); - await scanBluetoothDevices(); - } catch (err: any) { - updateState({ error: err.message || MESSAGES.errors.scanFailed }); - } finally { - updateState({ scanning: false }); - } - }, [scanBluetoothDevices, updateState]); - - const handleDisconnect = useCallback(async () => { - try { - updateState({ loading: true }); - await disconnect(); - clearError(); - } catch (err: any) { - updateState({ error: err.message || MESSAGES.errors.disconnectFailed }); - } finally { - updateState({ loading: false }); - } - }, [disconnect, clearError, updateState]); + clearBleError(); + clearWifiError(); // 🆕 limpa erros WiFi também + }, [updateState, clearBleError, clearWifiError]); + // 🔄 Conectar de acordo com o tipo const handleConnect = useCallback(async () => { try { updateState({ loading: true, error: null }); if (state.selectedConnectionType === "cable") { await connectCable(); - } else { + } else if (state.selectedConnectionType === "bluetooth_classic") { if (!state.selectedDevice) { updateState({ error: MESSAGES.selectDevice, loading: false }); return; } - await connectBluetooth(state.selectedDevice); + await connectBluetoothClassic(state.selectedDevice); + } else if (state.selectedConnectionType === "bluetooth_le") { + if (!state.selectedBleDevice) { + updateState({ error: MESSAGES.selectBleDevice, loading: false }); + return; + } + await connectBluetoothLE(state.selectedBleDevice); + } else if (state.selectedConnectionType === "wifi") { + await connectWifi(state.wifiIp, Number(state.wifiPort)); } navigate("/components"); @@ -114,149 +128,163 @@ export default function Connection() { } finally { updateState({ loading: false }); } - }, [state.selectedConnectionType, state.selectedDevice, connectCable, connectBluetooth, navigate, updateState]); + }, [ + state.selectedConnectionType, + state.selectedDevice, + state.selectedBleDevice, + state.wifiIp, + state.wifiPort, + connectCable, + connectBluetoothClassic, + connectBluetoothLE, + connectWifi, + navigate, + updateState + ]); + + const handleDisconnect = useCallback(async () => { + try { + updateState({ loading: true }); + await disconnect(); + clearError(); + } catch (err: any) { + updateState({ error: err.message || MESSAGES.errors.disconnectFailed }); + } finally { + updateState({ loading: false }); + } + }, [disconnect, clearError, updateState]); const handleConnection = useCallback(() => { return isConnected ? handleDisconnect() : handleConnect(); }, [isConnected, handleDisconnect, handleConnect]); const handleConnectionTypeChange = useCallback((type: ConnectionType) => { - updateState({ selectedConnectionType: type, selectedDevice: null }); - }, [updateState]); - - const handleDeviceSelect = useCallback((deviceAddress: string) => { - updateState({ selectedDevice: deviceAddress }); + updateState({ + selectedConnectionType: type, + selectedDevice: null, + selectedBleDevice: null, + }); }, [updateState]); const isConnectDisabled = useCallback(() => { - return state.loading || (state.selectedConnectionType === "bluetooth" && !state.selectedDevice && !isConnected); - }, [state.loading, state.selectedConnectionType, state.selectedDevice, isConnected]); + if (state.loading) return true; + + if (state.selectedConnectionType === "bluetooth_classic" && !state.selectedDevice && !isConnected) { + return true; + } + if (state.selectedConnectionType === "bluetooth_le" && !state.selectedBleDevice && !isConnected) { + return true; + } + if (state.selectedConnectionType === "wifi" && (!state.wifiIp || !state.wifiPort) && !isConnected) { + return true; + } + return false; + }, [state, isConnected]); const getConnectButtonText = useCallback(() => { if (state.loading) return MESSAGES.buttons.processing; if (isConnected) return MESSAGES.buttons.disconnect; - return state.selectedConnectionType === "cable" - ? MESSAGES.buttons.connectCable - : MESSAGES.buttons.connectBluetooth; + + switch (state.selectedConnectionType) { + case "cable": return MESSAGES.buttons.connectCable; + case "bluetooth_classic": return MESSAGES.buttons.connectBluetoothClassic; + case "bluetooth_le": return MESSAGES.buttons.connectBluetoothLE; + case "wifi": return MESSAGES.buttons.connectWifi; // 🆕 + default: return MESSAGES.buttons.connectCable; + } }, [state.loading, state.selectedConnectionType, isConnected]); - const renderConnectionTypeSelector = () => ( -
-

{MESSAGES.connectionMethod}

-
- - -
-
- ); - const renderBluetoothDeviceList = () => ( -
-

{MESSAGES.availableDevices}

- {availableDevices.length === 0 ? ( -

{MESSAGES.noDevices}

- ) : ( -
    - {availableDevices.map((device) => ( -
  • - -
  • + // 🆕 Seção de conexão WiFi + const renderWifiSection = () => ( +
    +

    Configuração WiFi

    +
    + updateState({ wifiIp: e.target.value })} + placeholder="IP do dispositivo" + className="border rounded px-2 py-1 text-sm flex-1" + /> + updateState({ wifiPort: e.target.value })} + placeholder="Porta" + className="border rounded px-2 py-1 text-sm w-20" + /> + +
    + {wifiError && ( +
    + ⚠️ {wifiError} +
    + )} + {wifiLogs.length > 0 && ( +
    + {wifiLogs.map((log, i) => ( +
    📡 {log}
    ))} -
+
)} ); - const renderBluetoothSection = () => ( -
-

Antes de você clicar em buscar dispositivo, você deve ter pareado o bluetooth com o celular, só assim a opção aparecera aqui

-
- - {state.scanning && ( - - {MESSAGES.scanningHint} - - )} -
- {renderBluetoothDeviceList()} -
- ); + // 🎛️ Seletor de tipo de conexão + const renderConnectionTypeSelector = () => ( +
+

{MESSAGES.connectionMethod}

+
+ {Object.entries(MESSAGES.connectionTypes).map(([key, label]) => ( + + ))} - const renderErrorMessage = () => state.error && ( -
- {state.error} +
); - const renderConnectionForm = () => !isConnected && ( -
+ return ( +
+
{renderConnectionTypeSelector()} - {state.selectedConnectionType === "bluetooth" && renderBluetoothSection()} -
- ); - const renderContinueButton = () => isConnected && ( - - ); + {state.selectedConnectionType === "bluetooth_classic" && ( +
+ {/* lista + botão scan */} +
+ )} - return ( -
-
-
-

- {isConnected ? MESSAGES.connected : MESSAGES.disconnected} -

+ {state.selectedConnectionType === "bluetooth_le" && ( +
+ {/* lista BLE + botão scan */} +
+ )} - {renderErrorMessage()} - {renderConnectionForm()} + {state.selectedConnectionType === "wifi" && renderWifiSection()} - + {(state.error || bleError) && ( +
+ {state.error || bleError} +
+ )} - {renderContinueButton()} -
+
); -} \ No newline at end of file +} diff --git a/src/pages/Snake.tsx b/src/pages/Snake.tsx new file mode 100644 index 0000000..65e3ced --- /dev/null +++ b/src/pages/Snake.tsx @@ -0,0 +1,41 @@ +import { Header } from "@/components/Header"; +import { useConnection } from "@/contexts/ConnectionContext"; +import { useEffect } from "react"; + +export default function Snake() { + const { sendCommand } = useConnection(); + + // useEffect para controlar o início e o fim do jogo + useEffect(() => { + // Função a ser executada quando o componente é montado (entra na tela) + const startGame = async () => { + console.log("Iniciando o jogo Snake..."); + await sendCommand("snake_start()"); + }; + + startGame(); + + // Função de limpeza a ser executada quando o componente é desmontado (sai da tela) + return () => { + console.log("Parando o jogo Snake..."); + sendCommand("snake_stop()"); + }; + }, [sendCommand]); // A dependência garante que a função sendCommand esteja disponível + + return ( +
+
+
+

+ Jogo da cobrinha em andamento +

+

+ Mexa-se com o joystick +

+

+ Pressione o joystick para reiniciar +

+
+
+ ); +} \ No newline at end of file diff --git a/src/utils/buzzersController.ts b/src/utils/buzzersController.ts index 0cb16e9..34b012d 100644 --- a/src/utils/buzzersController.ts +++ b/src/utils/buzzersController.ts @@ -7,6 +7,7 @@ export interface BuzzersData { } export class BuzzersController { + private sendCommand: (command: string) => Promise; private isSetupDone: boolean = false; diff --git a/src/utils/playbackBuzzer.ts b/src/utils/playbackBuzzer.ts index 2f0359f..530bd08 100644 --- a/src/utils/playbackBuzzer.ts +++ b/src/utils/playbackBuzzer.ts @@ -33,4 +33,8 @@ export async function playbackBuzzerSequence( console.log('Tempo real para parar nota:', endTime - startTime, 'ms'); } } + + // Garantir que o buzzer está parado no final + console.log("🎼 Reprodução finalizada - garantindo que buzzer está parado"); + await controller.stopBuzzer(0); } \ No newline at end of file